├── .flake8 ├── .github ├── CODEOWNERS ├── release-drafter.yml └── workflows │ ├── ack.yml │ ├── push.yml │ ├── release.yml │ └── tox.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .yamllint ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bindep.txt ├── molecule ├── molecule_vagrant ├── __init__.py ├── cookiecutter │ ├── cookiecutter.json │ └── {{cookiecutter.molecule_directory}} │ │ └── {{cookiecutter.scenario_name}} │ │ ├── INSTALL.rst │ │ └── converge.yml ├── driver.py ├── modules │ ├── __init__.py │ └── vagrant.py ├── playbooks │ ├── create.yml │ ├── destroy.yml │ └── prepare.yml └── test │ ├── __init__.py │ ├── functional │ ├── __init__.py │ ├── conftest.py │ └── test_func.py │ ├── scenarios │ └── molecule │ │ ├── config_options │ │ ├── converge.yml │ │ ├── molecule.yml │ │ └── verify.yml │ │ ├── default-compat │ │ ├── converge.yml │ │ ├── create.yml │ │ ├── destroy.yml │ │ └── molecule.yml │ │ ├── default │ │ ├── converge.yml │ │ └── molecule.yml │ │ ├── hostname │ │ ├── converge.yml │ │ ├── molecule.yml │ │ └── verify.yml │ │ ├── invalid │ │ ├── converge.yml │ │ └── molecule.yml │ │ ├── multi-node │ │ ├── converge.yml │ │ ├── molecule.yml │ │ └── verify.yml │ │ ├── network │ │ ├── converge.yml │ │ ├── molecule.yml │ │ └── verify.yml │ │ ├── provider_config_options │ │ ├── converge.yml │ │ ├── molecule.yml │ │ └── verify.yml │ │ └── vagrant_root │ │ ├── converge.yml │ │ ├── molecule.yml │ │ └── verify.yml │ └── test_driver.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── tools ├── Vagrantfile ├── create_testbox.sh └── test-setup.sh ├── tox.ini └── zuul.d └── layout.yaml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # do not add excludes for files in repo 3 | exclude = .venv/,.tox/,dist/,build/,.eggs/ 4 | format = pylint 5 | # E203: https://github.com/python/black/issues/315 6 | ignore = E741,W503,W504,H,E501,E203 7 | # 88 is official black default: 8 | max-line-length = 88 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ssbarnea @apatard 2 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # see https://github.com/ansible-community/devtools 2 | _extends: ansible-community/devtools 3 | -------------------------------------------------------------------------------- /.github/workflows/ack.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/ansible-community/devtools/blob/main/.github/workflows/ack.yml 2 | name: ack 3 | on: 4 | pull_request_target: 5 | types: [opened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | ack: 9 | uses: ansible-community/devtools/.github/workflows/ack.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/ansible-community/devtools/blob/main/.github/workflows/push.yml 2 | name: push 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'releases/**' 8 | - 'stable/**' 9 | 10 | jobs: 11 | ack: 12 | uses: ansible-community/devtools/.github/workflows/push.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | pypi: 10 | name: Publish to PyPI registry 11 | environment: release 12 | runs-on: ubuntu-22.04 13 | 14 | env: 15 | FORCE_COLOR: 1 16 | PY_COLORS: 1 17 | TOXENV: pkg 18 | 19 | steps: 20 | - name: Switch to using Python 3.9 by default 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.9 24 | - name: Install tox 25 | run: python3 -m pip install --user "tox>=4.0.0" 26 | - name: Check out src from Git 27 | uses: actions/checkout@v3 28 | with: 29 | fetch-depth: 0 # needed by setuptools-scm 30 | - name: Build dists 31 | run: python -m tox 32 | - name: Publish to pypi.org 33 | if: >- # "create" workflows run separately from "push" & "pull_request" 34 | github.event_name == 'release' 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | password: ${{ secrets.pypi_password }} 38 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | name: tox 2 | 3 | on: 4 | create: # is used for publishing to PyPI and TestPyPI 5 | tags: # any tag regardless of its name, no branches 6 | - "**" 7 | push: # only publishes pushes to the main branch to TestPyPI 8 | branches: # any integration branch but not tag 9 | - "main" 10 | pull_request: 11 | release: 12 | types: 13 | - published # It seems that you can publish directly without creating 14 | schedule: 15 | - cron: 1 0 * * * # Run daily at 0:01 UTC 16 | 17 | jobs: 18 | build: 19 | name: ${{ matrix.tox_env }} 20 | # macos is the only gh action platform with support for vagrant/virtualbox 21 | # According to: 22 | # https://github.com/actions/runner-images/blob/main/images/macos/macos-11-Readme.md 23 | # https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md 24 | # -> macos-12 has them but not macos-11 25 | # -> don't use macos-latest for safety 26 | runs-on: ${{ matrix.platform }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - tox_env: lint 32 | platform: ubuntu-latest 33 | skip_vagrant: true 34 | - tox_env: py39,py39-devel 35 | PREFIX: PYTEST_REQPASS=11 36 | platform: macos-12 37 | python_version: "3.9" 38 | - tox_env: py310,py310-devel 39 | PREFIX: PYTEST_REQPASS=11 40 | platform: macos-12 41 | python_version: "3.10" 42 | - tox_env: pkg 43 | platform: ubuntu-latest 44 | skip_vagrant: true 45 | 46 | steps: 47 | - name: Check vagrant presence 48 | run: | 49 | vagrant version 50 | vagrant plugin list 51 | if: ${{ ! matrix.skip_vagrant }} 52 | 53 | - name: Check out src from Git 54 | uses: actions/checkout@v3 55 | with: 56 | fetch-depth: 0 # needed by setuptools-scm 57 | 58 | - name: Enable vagrant box caching 59 | uses: actions/cache@v3 60 | if: ${{ ! matrix.skip_vagrant }} 61 | with: 62 | path: | 63 | ~/.vagrant.d/boxes 64 | key: ${{ runner.os }}-${{ hashFiles('tools/Vagrantfile') }} 65 | 66 | # Be sure to install the version of python needed by a specific test, if necessary 67 | - name: Set up Python version 68 | uses: actions/setup-python@v4 69 | with: 70 | python-version: ${{ matrix.python_version || '3.9' }} 71 | cache: pip 72 | 73 | - name: Install dependencies 74 | run: | 75 | python -m pip install -U pip 76 | pip install tox 77 | 78 | - name: Create test box 79 | run: | 80 | ./tools/create_testbox.sh 81 | if: ${{ ! matrix.skip_vagrant }} 82 | 83 | - name: Run tox -e ${{ matrix.tox_env }} 84 | run: | 85 | echo "${{ matrix.PREFIX }} tox -e ${{ matrix.tox_env }}" 86 | ${{ matrix.PREFIX }} tox -e ${{ matrix.tox_env }} 87 | 88 | check: 89 | needs: 90 | - build 91 | 92 | runs-on: ubuntu-latest 93 | 94 | steps: 95 | - name: Report success of the test matrix 96 | run: >- 97 | print("All's good") 98 | shell: python 99 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 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 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | pip-wheel-metadata 106 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | skip: 4 | # https://github.com/pre-commit-ci/issues/issues/55 5 | - ansible-lint 6 | default_language_version: 7 | python: python3 8 | minimum_pre_commit_version: "1.14.0" 9 | repos: 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.4.0 12 | hooks: 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | - id: mixed-line-ending 16 | - id: check-byte-order-marker 17 | - id: check-executables-have-shebangs 18 | - id: check-merge-conflict 19 | - id: debug-statements 20 | - id: check-yaml 21 | files: .*\.(yaml|yml)$ 22 | # https://github.com/pre-commit/pre-commit-hooks/issues/273 23 | args: ["--unsafe"] 24 | - repo: https://github.com/PyCQA/doc8.git 25 | rev: v1.0.0 26 | hooks: 27 | - id: doc8 28 | - repo: https://github.com/psf/black 29 | rev: 22.12.0 30 | hooks: 31 | - id: black 32 | language_version: python3 33 | - repo: https://github.com/PyCQA/flake8 34 | rev: 6.0.0 35 | hooks: 36 | - id: flake8 37 | additional_dependencies: 38 | - flake8-black 39 | - repo: https://github.com/codespell-project/codespell.git 40 | rev: v2.2.2 41 | hooks: 42 | - id: codespell 43 | name: codespell 44 | description: Checks for common misspellings in text files. 45 | entry: codespell 46 | language: python 47 | types: [text] 48 | args: [] 49 | require_serial: false 50 | additional_dependencies: [] 51 | - repo: https://github.com/adrienverge/yamllint.git 52 | rev: v1.28.0 53 | hooks: 54 | - id: yamllint 55 | files: \.(yaml|yml)$ 56 | types: [file, yaml] 57 | entry: yamllint --strict -f parsable 58 | - repo: https://github.com/openstack-dev/bashate.git 59 | rev: 2.1.1 60 | hooks: 61 | - id: bashate 62 | entry: bashate --error . --ignore=E006,E040 63 | # Run bashate check for all bash scripts 64 | # Ignores the following rules: 65 | # E006: Line longer than 79 columns (as many scripts use jinja 66 | # templating, this is very difficult) 67 | # E040: Syntax error determined using `bash -n` (as many scripts 68 | # use jinja templating, this will often fail and the syntax 69 | # error will be discovered in execution anyway) 70 | - repo: https://github.com/ansible/ansible-lint.git 71 | rev: v6.10.0 72 | hooks: 73 | - id: ansible-lint 74 | always_run: true 75 | pass_filenames: false 76 | # do not add file filters here as ansible-lint does not give reliable 77 | # results when called with individual files. 78 | # https://github.com/ansible/ansible-lint/issues/611 79 | verbose: true 80 | entry: bash -c "ANSIBLE_LIBRARY=molecule_vagrant/modules ansible-lint --force-color -v" 81 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | ignore: | 3 | */cookiecutter/ 4 | .github/workflows/ 5 | .tox 6 | 7 | rules: 8 | braces: 9 | max-spaces-inside: 1 10 | level: error 11 | brackets: 12 | max-spaces-inside: 1 13 | level: error 14 | document-start: disable 15 | line-length: disable 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Sorin Sbarnea 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | 4 | recursive-exclude * __pycache__ 5 | recursive-exclude * *.py[co] 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | *********************** 2 | Molecule Vagrant Plugin 3 | *********************** 4 | 5 | .. image:: https://badge.fury.io/py/molecule-vagrant.svg 6 | :target: https://badge.fury.io/py/molecule-vagrant 7 | :alt: PyPI Package 8 | 9 | .. image:: https://zuul-ci.org/gated.svg 10 | :target: https://dashboard.zuul.ansible.com/t/ansible/builds?project=ansible-community/molecule-vagrant 11 | 12 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 13 | :target: https://github.com/python/black 14 | :alt: Python Black Code Style 15 | 16 | .. image:: https://img.shields.io/badge/Code%20of%20Conduct-silver.svg 17 | :target: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html 18 | :alt: Ansible Code of Conduct 19 | 20 | .. image:: https://img.shields.io/badge/Mailing%20lists-silver.svg 21 | :target: https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information 22 | :alt: Ansible mailing lists 23 | 24 | .. image:: https://img.shields.io/badge/license-MIT-brightgreen.svg 25 | :target: LICENSE 26 | :alt: Repository License 27 | 28 | Molecule Vagrant is designed to allow use of Vagrant for provisioning of test 29 | resources. 30 | 31 | Supported Platforms 32 | =================== 33 | 34 | This driver relies on vagrant command line which is known to be problematic 35 | to install on several platforms. We do our best to perform CI/CD testing on 36 | multiple platforms but some are disabled due to known bugs. 37 | 38 | * ✅ MacOS with VirtualBox - GitHub Actions 39 | * ✅ Fedora 32 with libvirt - Zuul 40 | * ✅ Ubuntu Bionic (18.04) with libvirt - Zuul 41 | * ❌ CentOS 8 with libvirt - Zuul DISABLED due to 1127_ and 11020_ 42 | 43 | Please **do not file bugs for unsupported platforms**. You are welcomed to 44 | create PRs that fix untested platform, as long they do not break existing ones. 45 | 46 | .. _`1127`: https://github.com/vagrant-libvirt/vagrant-libvirt/issues/1127 47 | .. _`11020`: https://github.com/hashicorp/vagrant/issues/11020 48 | 49 | 50 | Documentation 51 | ============= 52 | 53 | To use this plugin, you'll need to set the ``driver`` and ``platform`` 54 | variables in your ``molecule.yml``. Here's a simple example using the 55 | `fedora/32-cloud-base`_ box: 56 | 57 | .. code-block:: yaml 58 | 59 | driver: 60 | name: vagrant 61 | 62 | platforms: 63 | - name: instance 64 | box: fedora/32-cloud-base 65 | memory: 512 66 | cpus: 1 67 | 68 | Here's a full example with the libvirt provider: 69 | 70 | .. code-block:: yaml 71 | 72 | driver: 73 | name: vagrant 74 | provider: 75 | # Can be any supported provider (virtualbox, parallels, libvirt, etc) 76 | # Defaults to virtualbox 77 | name: libvirt 78 | # Run vagrant up with --provision. 79 | # Defaults to --no-provision) 80 | provision: no 81 | # vagrant-cachier configuration 82 | # Defaults to 'machine' 83 | # Any value different from 'machine' or 'box' will disable it 84 | cachier: machine 85 | # If set to false, set VAGRANT_NO_PARALLEL to '1' 86 | # Defaults to true 87 | parallel: true 88 | # vagrant box to use by default 89 | # Defaults to 'generic/alpine316' 90 | default_box: 'generic/alpine316' 91 | 92 | platforms: 93 | - name: instance 94 | # If specified, set host name to hostname, unless it's set to False and 95 | # the host name won't be set. In all other cases (including default) use 96 | # 'name' as host name. 97 | hostname: foo.bar.com 98 | # List of dictionaries mapped to `config.vm.network` 99 | interfaces: 100 | # `network_name` is the required identifier, all other keys map to 101 | # arguments. 102 | - auto_config: true 103 | network_name: private_network 104 | type: dhcp 105 | - network_name: private_network 106 | ip: 192.168.123.3 107 | - network_name: forwarded_port 108 | guest: 80 109 | host: 8080 110 | # List of raw Vagrant `config` options 111 | instance_raw_config_args: 112 | # use single quotes to avoid YAML parsing as dict due to ':' 113 | - 'vm.synced_folder ".", "/vagrant", type: "rsync"' 114 | # Run 'uname' a provisionning step **needs 'provision: true' to work** 115 | - 'vm.provision :shell, inline: "uname"' 116 | # Dictionary of `config` options. Note that string values need to be 117 | # explicitly enclosed in quotes. 118 | config_options: 119 | ssh.keep_alive: yes 120 | ssh.remote_user: 'vagrant' 121 | synced_folder: true 122 | box: fedora/32-cloud-base 123 | box_version: 32.20200422.0 124 | box_url: 125 | memory: 512 126 | cpus: 1 127 | # Dictionary of options passed to the provider 128 | provider_options: 129 | video_type: 'vga' 130 | # List of raw provider options 131 | provider_raw_config_args: 132 | - cpuset = '1-4,^3,6' 133 | 134 | .. _`fedora/32-cloud-base`: https://app.vagrantup.com/fedora/boxes/32-cloud-base 135 | 136 | 137 | More examples may be found in the ``molecule`` `scenarios directory`_. 138 | They're the scenarios used by the CI. 139 | 140 | 141 | .. _get-involved: 142 | 143 | Get Involved 144 | ============ 145 | 146 | * Join us in the ``#ansible-devtools`` channel on `Libera`_. 147 | * Join the discussion in `molecule-users Forum`_. 148 | * Join the community working group by checking the `wiki`_. 149 | * Want to know about releases, subscribe to `ansible-announce list`_. 150 | * For the full list of Ansible email Lists, IRC channels see the 151 | `communication page`_. 152 | 153 | .. _`Libera`: https://web.libera.chat/?channel=#ansible-devtools 154 | .. _`molecule-users Forum`: https://groups.google.com/forum/#!forum/molecule-users 155 | .. _`wiki`: https://github.com/ansible/community/wiki/Molecule 156 | .. _`ansible-announce list`: https://groups.google.com/group/ansible-announce 157 | .. _`communication page`: https://docs.ansible.com/ansible/latest/community/communication.html 158 | .. _`scenarios directory`: https://github.com/ansible-community/molecule-vagrant/tree/main/molecule_vagrant/test/scenarios/molecule 159 | .. _authors: 160 | 161 | Authors 162 | ======= 163 | 164 | Molecule Vagrant Plugin was created by Sorin Sbarnea based on code from 165 | Molecule. 166 | 167 | .. _license: 168 | 169 | License 170 | ======= 171 | 172 | The `MIT`_ License. 173 | 174 | .. _`MIT`: https://github.com/ansible-community/molecule-vagrant/blob/main/LICENSE 175 | 176 | The logo is licensed under the `Creative Commons NoDerivatives 4.0 License`_. 177 | 178 | If you have some other use in mind, contact us. 179 | 180 | .. _`Creative Commons NoDerivatives 4.0 License`: https://creativecommons.org/licenses/by-nd/4.0/ 181 | -------------------------------------------------------------------------------- /bindep.txt: -------------------------------------------------------------------------------- 1 | # This is a cross-platform list tracking distribution packages needed by tests; 2 | # see https://docs.openstack.org/infra/bindep/ for additional information. 3 | 4 | build-essential [platform:dpkg] 5 | dnsmasq-base [platform:dpkg] 6 | ebtables [platform:dpkg] 7 | gcc [test platform:rpm] 8 | gcc-c++ [test platform:rpm] 9 | libselinux-python [platform:centos-7] 10 | libvirt [platform:rpm] 11 | libvirt-clients [platform:dpkg] 12 | libvirt-daemon [platform:dpkg] 13 | libvirt-daemon-kvm [platform:rpm] 14 | libvirt-daemon-system [platform:dpkg] 15 | libvirt-dev [platform:dpkg] 16 | libvirt-devel [platform:rpm] 17 | libguestfs-tools [platform:dpkg platform:rpm] 18 | libxml2-dev [platform:dpkg] 19 | libxslt-dev [platform:dpkg] 20 | make [platform:centos-7 platform:centos-8] 21 | pkg-config [platform:dpkg] 22 | python3 [test platform:dpkg platform:rpm !platform:centos-7] 23 | python3-devel [test platform:rpm !platform:centos-7] 24 | python3-dev [test platform:dpkg] 25 | python3-libselinux [test platform:rpm !platform:centos-7] 26 | python3-libvirt [test platform:rpm !platform:centos-7 platform:dpkg] 27 | python3-netifaces [test !platform:centos-7 platform:rpm] 28 | python36 [test !platform:dpkg !platform:centos-7 !platform:fedora-28] 29 | qemu-system [platform:dpkg] 30 | qemu-utils [platform:dpkg] 31 | qemu-kvm [platform:rpm] 32 | ruby-dev [platform:dpkg] 33 | ruby-devel [!platform:centos-7 !platform:centos-8 platform:rpm] 34 | ruby-libvirt [platform:dpkg] 35 | vagrant [platform:dpkg] 36 | zlib1g-dev [platform:dpkg] 37 | -------------------------------------------------------------------------------- /molecule: -------------------------------------------------------------------------------- 1 | molecule_vagrant/test/scenarios/molecule/ -------------------------------------------------------------------------------- /molecule_vagrant/__init__.py: -------------------------------------------------------------------------------- 1 | """Plugin exports.""" 2 | 3 | __name__ = __name__.split("_")[-1] 4 | -------------------------------------------------------------------------------- /molecule_vagrant/cookiecutter/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "molecule_directory": "molecule", 3 | "role_name": "OVERRIDDEN", 4 | "scenario_name": "OVERRIDDEN" 5 | } 6 | -------------------------------------------------------------------------------- /molecule_vagrant/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ********************************* 2 | Vagrant driver installation guide 3 | ********************************* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Vagrant 9 | * Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop 10 | 11 | Install 12 | ======= 13 | 14 | Please refer to the `Virtual environment`_ documentation for installation best 15 | practices. If not using a virtual environment, please consider passing the 16 | widely recommended `'--user' flag`_ when invoking ``pip``. 17 | 18 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 19 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 20 | 21 | .. code-block:: bash 22 | 23 | $ pip install 'molecule_vagrant' 24 | -------------------------------------------------------------------------------- /molecule_vagrant/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_vagrant/driver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import os 22 | from shutil import which 23 | 24 | from molecule import logger 25 | from molecule import util 26 | from molecule.api import Driver 27 | 28 | 29 | LOG = logger.get_logger(__name__) 30 | 31 | 32 | class Vagrant(Driver): 33 | """ 34 | The class responsible for managing `Vagrant`_ instances. `Vagrant`_ is 35 | `not` the default driver used in Molecule. 36 | 37 | Molecule leverages Molecule's own :ref:`molecule_vagrant_module`, by 38 | mapping variables from ``molecule.yml`` into ``create.yml`` and ``destroy.yml``. 39 | 40 | .. important:: 41 | 42 | This driver is alpha quality software. Do not perform any additional 43 | tasks inside the ``create`` playbook. Molecule does not know about the 44 | Vagrant instances' configuration until the ``converge`` playbook is 45 | executed. 46 | 47 | The ``create`` playbook boots instances, then the instance data is 48 | written to disk. The instance data is then added to Molecule's Ansible 49 | inventory on the next execution of `molecule.command.create`, which 50 | happens to be the ``converge`` playbook. 51 | 52 | This is an area needing improvement. Gluing togher Ansible playbook 53 | return data and molecule is clunky. Moving the playbook execution 54 | from ``sh`` to python is less than ideal, since the playbook's return 55 | data needs handled by an internal callback plugin. 56 | 57 | Operating this far inside Ansible's internals doesn't feel right. Nor 58 | does orchestrating Ansible's CLI with Molecule. Ansible is throwing 59 | pieces over the wall, which Molecule is picking up and reconstructing. 60 | 61 | .. code-block:: yaml 62 | 63 | driver: 64 | name: vagrant 65 | platforms: 66 | - name: instance-1 67 | instance_raw_config_args: 68 | - "vm.network 'forwarded_port', guest: 80, host: 8080" 69 | interfaces: 70 | - auto_config: true 71 | network_name: private_network 72 | type: dhcp 73 | config_options: 74 | synced_folder: True 75 | box: debian/jessie64 76 | box_version: 10.1 77 | box_url: http://repo.example.com/images/postmerge/debian.json 78 | box_download_checksum: 6d016aa287990152548f0a414048c2edeea7f84b48293cab21506f86649f79b8 79 | box_download_checksum_type: sha256 80 | memory: 1024 81 | cpus: 1 82 | provider_options: 83 | gui: True 84 | provider_raw_config_args: 85 | - "customize ['modifyvm', :id, '--cpuexecutioncap', '50']" 86 | provider_override_args: 87 | - "vm.synced_folder './', '/vagrant', disabled: true, type: 'nfs'" 88 | provision: True 89 | 90 | .. code-block:: bash 91 | 92 | $ pip install molecule-vagrant 93 | 94 | Change the provider passed to Vagrant. 95 | 96 | .. code-block:: yaml 97 | 98 | driver: 99 | name: vagrant 100 | provider: 101 | name: parallels 102 | 103 | Change the options passed to the ssh client. 104 | 105 | .. code-block:: yaml 106 | 107 | driver: 108 | name: vagrant 109 | ssh_connection_options: 110 | - '-o ControlPath=~/.ansible/cp/%r@%h-%p' 111 | 112 | .. important:: 113 | 114 | Molecule does not merge lists, when overriding the developer must 115 | provide all options. 116 | 117 | Provide a list of files Molecule will preserve, relative to the scenario 118 | ephemeral directory, after any ``destroy`` subcommand execution. 119 | 120 | .. code-block:: yaml 121 | 122 | driver: 123 | name: vagrant 124 | safe_files: 125 | - foo 126 | 127 | .. _`Vagrant`: https://www.vagrantup.com 128 | """ # noqa 129 | 130 | def __init__(self, config=None): 131 | super(Vagrant, self).__init__(config) 132 | self._name = "vagrant" 133 | 134 | @property 135 | def name(self): 136 | return self._name 137 | 138 | @name.setter 139 | def name(self, value): 140 | self._name = value 141 | 142 | @property 143 | def testinfra_options(self): 144 | return { 145 | "connection": "ansible", 146 | "ansible-inventory": self._config.provisioner.inventory_file, 147 | } 148 | 149 | @property 150 | def login_cmd_template(self): 151 | connection_options = " ".join(self.ssh_connection_options) 152 | 153 | return ( 154 | "ssh {{address}} " 155 | "-l {{user}} " 156 | "-p {{port}} " 157 | "-i {{identity_file}} " 158 | "{}" 159 | ).format(connection_options) 160 | 161 | @property 162 | def default_safe_files(self): 163 | return [ 164 | self.vagrantfile, 165 | self.instance_config, 166 | os.path.join(self._config.scenario.ephemeral_directory, ".vagrant"), 167 | os.path.join(self._config.scenario.ephemeral_directory, "vagrant-*.out"), 168 | os.path.join(self._config.scenario.ephemeral_directory, "vagrant-*.err"), 169 | ] 170 | 171 | @property 172 | def default_ssh_connection_options(self): 173 | return self._get_ssh_connection_options() 174 | 175 | def login_options(self, instance_name): 176 | d = {"instance": instance_name} 177 | 178 | return util.merge_dicts(d, self._get_instance_config(instance_name)) 179 | 180 | def ansible_connection_options(self, instance_name): 181 | try: 182 | d = self._get_instance_config(instance_name) 183 | 184 | return { 185 | "ansible_user": d["user"], 186 | "ansible_host": d["address"], 187 | "ansible_port": d["port"], 188 | "ansible_private_key_file": d["identity_file"], 189 | "connection": "ssh", 190 | "ansible_ssh_common_args": " ".join(self.ssh_connection_options), 191 | } 192 | except StopIteration: 193 | return {} 194 | except IOError: 195 | # Instance has yet to be provisioned , therefore the 196 | # instance_config is not on disk. 197 | return {} 198 | 199 | @property 200 | def vagrantfile(self): 201 | return os.path.join(self._config.scenario.ephemeral_directory, "Vagrantfile") 202 | 203 | def _get_instance_config(self, instance_name): 204 | instance_config_dict = util.safe_load_file(self._config.driver.instance_config) 205 | 206 | return next( 207 | item for item in instance_config_dict if item["instance"] == instance_name 208 | ) 209 | 210 | def sanity_checks(self): 211 | if not which("vagrant"): 212 | util.sysexit_with_message("vagrant executable was not found!") 213 | 214 | # TODO(ssbarnea): Replace code below with variant that check if ansible 215 | # has vagrant module available. 216 | # try: 217 | # import vagrant # noqa 218 | # except ImportError: 219 | # util.sysexit_with_message( 220 | # "Unable to import python vagrant module. Running " 221 | # "'pip instgt .all python-vagrant' should fix it." 222 | # ) 223 | 224 | def template_dir(self): 225 | """Return path to its own cookiecutterm templates. It is used by init 226 | command in order to figure out where to load the templates from. 227 | """ 228 | return os.path.join(os.path.dirname(__file__), "cookiecutter") 229 | 230 | def modules_dir(self): 231 | return os.path.join(os.path.dirname(__file__), "modules") 232 | -------------------------------------------------------------------------------- /molecule_vagrant/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-vagrant/353b7e38d8da7605b9b93c57a3fa2c089d3821d2/molecule_vagrant/modules/__init__.py -------------------------------------------------------------------------------- /molecule_vagrant/modules/vagrant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | # DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | from __future__ import absolute_import, division, print_function 26 | 27 | __metaclass__ = type 28 | 29 | from ansible.module_utils.basic import AnsibleModule 30 | import contextlib 31 | import datetime 32 | import io 33 | import os 34 | import subprocess 35 | import sys 36 | 37 | import molecule 38 | import molecule.util 39 | 40 | try: 41 | import vagrant 42 | except ImportError: 43 | sys.exit("ERROR: Driver missing, install python-vagrant.") 44 | ANSIBLE_METADATA = { 45 | "metadata_version": "0.1", 46 | "status": ["preview"], 47 | "supported_by": "community", 48 | } 49 | 50 | DOCUMENTATION = """ 51 | --- 52 | module: vagrant 53 | short_description: Manage Vagrant instances 54 | description: 55 | - Manage the life cycle of Vagrant instances. 56 | version_added: 2.0 57 | author: 58 | - Cisco Systems, Inc. 59 | options: 60 | instances: 61 | description: 62 | - list of instances to handle 63 | required: False 64 | default: [] 65 | instance_name: 66 | description: 67 | - Assign a name to a new instance or match an existing instance. 68 | required: False 69 | default: None 70 | instance_interfaces: 71 | description: 72 | - Assign interfaces to a new instance. 73 | required: False 74 | default: [] 75 | instance_raw_config_args: 76 | description: 77 | - Additional Vagrant options not explicitly exposed by this module. 78 | required: False 79 | default: None 80 | config_options: 81 | description: 82 | - Additional config options not explicitly exposed by this module. 83 | required: False 84 | default: {} 85 | platform_box: 86 | description: 87 | - Name of Vagrant box. 88 | required: True 89 | default: None 90 | platform_box_version: 91 | description: 92 | - Explicit version of Vagrant box to use. 93 | required: False 94 | default: None 95 | platform_box_url: 96 | description: 97 | - The URL to a Vagrant box. 98 | required: False 99 | default: None 100 | platform_box_download_checksum: 101 | description: 102 | - The checksum of the box specified by platform_box_url. 103 | required: False 104 | default: None 105 | platform_box_download_checksum_type: 106 | description: 107 | - The type of checksum specified by platform_box_download_checksum (if any). 108 | required: False 109 | default: None 110 | provider_name: 111 | description: 112 | - Name of the Vagrant provider to use. 113 | required: False 114 | default: virtualbox 115 | provider_memory: 116 | description: 117 | - Amount of memory to allocate to the instance. 118 | required: False 119 | default: 512 120 | provider_cpus: 121 | description: 122 | - Number of CPUs to allocate to the instance. 123 | required: False 124 | default: 2 125 | provider_options: 126 | description: 127 | - Additional provider options not explicitly exposed by this module. 128 | required: False 129 | default: {} 130 | provider_override_args: 131 | description: 132 | - Additional override options not explicitly exposed by this module. 133 | required: False 134 | default: None 135 | provider_raw_config_args: 136 | description: 137 | - Additional Vagrant options not explicitly exposed by this module. 138 | required: False 139 | default: None 140 | force_stop: 141 | description: 142 | - Force halt the instance, then destroy the instance. 143 | required: False 144 | default: False 145 | state: 146 | description: 147 | - The desired state of the instance. 148 | required: True 149 | choices: ['up', 'halt', 'destroy'] 150 | default: None 151 | workdir: 152 | description: 153 | - vagrant working directory 154 | required: False 155 | default: content of MOLECULE_EPHEMERAL_DIRECTORY environment variable 156 | default_box: 157 | description: 158 | - Default vagrant box to use when not specified in instance configuration 159 | required: False 160 | default: None 161 | cachier: 162 | description: 163 | - Cachier scope configuration. Can be "machine" or "box". Any other value 164 | will disable cachier. 165 | required: False 166 | default: "machine" 167 | parallel: 168 | description: 169 | - Enable or disable parallelism when bringing up the VMs by 170 | setting VAGRANT_NO_PARALLEL environment variable. 171 | required: False 172 | default: True 173 | 174 | requirements: 175 | - python >= 2.6 176 | - python-vagrant 177 | - vagrant 178 | """ 179 | 180 | EXAMPLES = """ 181 | See doc/source/configuration.rst 182 | """ 183 | 184 | VAGRANTFILE_TEMPLATE = """ 185 | {%- macro ruby_format(value) -%} 186 | {%- if value is boolean -%} 187 | {{ value | string | lower }} 188 | {%- elif value is string -%} 189 | {# "Bug" compat. To be removed later #} 190 | {%- if value[0] == value[-1] and value.startswith(("'", '"')) -%} 191 | {{ value }} 192 | {%- else -%} 193 | "{{ value }}" 194 | {%- endif -%} 195 | {%- else -%} 196 | {{ value }} 197 | {%- endif -%} 198 | {%- endmacro -%} 199 | 200 | {%- macro dict2args(dictionary) -%} 201 | {% set sep = joiner(", ") %} 202 | {%- for key, value in dictionary.items() -%} 203 | {{ sep() }}{{ key }}: {{ ruby_format(value) }} 204 | {%- endfor -%} 205 | {%- endmacro -%} 206 | 207 | Vagrant.configure('2') do |config| 208 | if Vagrant.has_plugin?('vagrant-cachier') 209 | {% if cachier is not none and cachier in [ "machine", "box" ] %} 210 | config.cache.scope = '{{ cachier }}' 211 | {% else %} 212 | config.cache.disable! 213 | {% endif %} 214 | end 215 | 216 | {% for instance in instances %} 217 | config.vm.define "{{ instance.name }}" do |c| 218 | ## 219 | # Box definition 220 | ## 221 | c.vm.box = "{{ instance.box }}" 222 | {{ 'c.vm.box_version = "{}"'.format(instance.box_version) if instance.box_version }} 223 | {{ 'c.vm.box_url = "{}"'.format(instance.box_url) if instance.box_url }} 224 | {{ 'c.vm.box_download_checksum = "{}"'.format(instance.box_download_checksum) if instance.box_download_checksum }} 225 | {{ 'c.vm.box_download_checksum_type = "{}"'.format(instance.box_download_checksum_type) if instance.box_download_checksum_type }} 226 | 227 | ## 228 | # Config options 229 | ## 230 | {% if instance.config_options['synced_folder'] is sameas false %} 231 | c.vm.synced_folder ".", "/vagrant", disabled: true 232 | {% endif %} 233 | 234 | {% for k,v in instance.config_options.items() %} 235 | {% if k not in ['synced_folder', 'cachier'] %}c.{{ k }} = {{ ruby_format(v) }}{% endif %} 236 | {% endfor %} 237 | 238 | {% if instance.hostname is boolean and instance.hostname is sameas false %} 239 | # c.vm.hostname not set 240 | {% elif instance.hostname is string %} 241 | c.vm.hostname = "{{ instance.hostname }}" 242 | {% else %} 243 | c.vm.hostname = "{{ instance.name }}" 244 | {% endif %} 245 | 246 | ## 247 | # Network 248 | ## 249 | {% for n in instance.networks %} 250 | c.vm.network "{{ n.name }}", {{ dict2args(n.options) }} 251 | {% endfor %} 252 | 253 | ## 254 | # instance_raw_config_args 255 | ## 256 | {% if instance.instance_raw_config_args is not none %}{% for arg in instance.instance_raw_config_args -%} 257 | c.{{ arg }} 258 | {% endfor %}{% endif %} 259 | 260 | ## 261 | # Provider 262 | ## 263 | c.vm.provider "{{ instance.provider }}" do |{{ instance.provider | lower }}, override| 264 | {% if instance.provider == "vsphere" %} 265 | {{ instance.provider | lower }}.memory_mb = {{ instance.memory }} 266 | {{ instance.provider | lower }}.cpu_count = {{ instance.cpus }} 267 | {% elif instance.provider.startswith('vmware_') %} 268 | {{ instance.provider | lower }}.vmx['memsize'] = {{ instance.memory }} 269 | {{ instance.provider | lower }}.vmx['numvcpus'] = {{ instance.cpus }} 270 | {% else %} 271 | {{ instance.provider | lower }}.memory = {{ instance.memory }} 272 | {{ instance.provider | lower }}.cpus = {{ instance.cpus }} 273 | {% endif %} 274 | 275 | {% for option, value in instance.provider_options.items() %} 276 | {{ instance.provider | lower }}.{{ option }} = {{ ruby_format(value) }} 277 | {% endfor %} 278 | 279 | {% if instance.provider_raw_config_args is not none %} 280 | {% for arg in instance.provider_raw_config_args %} 281 | {{ instance.provider | lower }}.{{ arg }} 282 | {% endfor %} 283 | {% endif %} 284 | 285 | {% if instance.provider_override_args is not none %} 286 | {% for arg in instance.provider_override_args -%} 287 | override.{{ arg }} 288 | {% endfor %} 289 | {% endif %} 290 | 291 | {% if instance.provider == 'virtualbox' %} 292 | {% if 'linked_clone' not in instance.provider_options %} 293 | virtualbox.linked_clone = true 294 | {% endif %} 295 | {% endif %} 296 | {% if instance.provider == 'libvirt' %} 297 | {% if no_kvm is sameas true and 'driver' not in instance.provider_options %} 298 | libvirt.driver='qemu' 299 | {% endif %} 300 | {% set libvirt_use_qemu = no_kvm %} 301 | {% if 'driver' in instance.provider_options and 'qemu' in instance.provider_options['driver'] %} 302 | {% set libvirt_use_qemu = true %} 303 | {% endif %} 304 | {% if libvirt_use_qemu is sameas true %} 305 | {% if 'cpu_mode' not in instance.provider_options %} 306 | # When using qemu instead of kvm, some libvirt systems 307 | # will use EPYC as vCPU model inside the new VM. 308 | # This will lead to process hang with ssh-keygen -A on alpine. 309 | # Not sure were the bug is (libvirt or qemu or alpine or all of them) but using QEMU64 310 | # cpu model will work around this. Hopefully, by checking 'cpu_mode' option, it will 311 | # allow people to override the model to use. 312 | libvirt.cpu_mode = 'custom' 313 | libvirt.cpu_model = 'qemu64' 314 | {% endif %} 315 | {% endif %} 316 | {% endif %} 317 | end 318 | end 319 | {% endfor %} 320 | end 321 | """.strip() # noqa 322 | 323 | RETURN = r""" 324 | rc: 325 | description: The command return code (0 means success) 326 | returned: always 327 | type: int 328 | cmd: 329 | description: The command executed by the task 330 | returned: always 331 | type: str 332 | stdout: 333 | description: The command standard output 334 | returned: changed 335 | type: str 336 | stderr: 337 | description: Output on stderr 338 | returned: changed 339 | type: str 340 | """ 341 | 342 | 343 | class VagrantClient(object): 344 | def __init__(self, module): 345 | self._module = module 346 | self.provision = self._module.params["provision"] 347 | self.cachier = self._module.params["cachier"] 348 | 349 | # compat 350 | if self._module.params["instance_name"] is not None: 351 | self._module.warn( 352 | "Please convert your playbook to use the instances parameter. Compat layer will be removed later." 353 | ) 354 | self.instances = [ 355 | { 356 | "name": self._module.params["instance_name"], 357 | "interfaces": self._module.params["instance_interfaces"], 358 | "instance_raw_config_args": self._module.params[ 359 | "instance_raw_config_args" 360 | ], 361 | "config_options": self._module.params["config_options"], 362 | "box": self._module.params["platform_box"], 363 | "box_version": self._module.params["platform_box_version"], 364 | "box_url": self._module.params["platform_box_url"], 365 | "box_download_checksum": self._module.params[ 366 | "platform_box_download_checksum" 367 | ], 368 | "box_download_checksum_type": self._module.params[ 369 | "platform_box_download_checksum_type" 370 | ], 371 | "memory": self._module.params["provider_memory"], 372 | "cpus": self._module.params["provider_cpus"], 373 | "provider_options": self._module.params["provider_options"], 374 | "provider_override_args": self._module.params[ 375 | "provider_override_args" 376 | ], 377 | "provider_raw_config_args": self._module.params[ 378 | "provider_raw_config_args" 379 | ], 380 | } 381 | ] 382 | else: 383 | self.instances = self._module.params["instances"] 384 | 385 | self._config = self._get_config() 386 | self._vagrantfile = self._config["vagrantfile"] 387 | self._vagrant = self._get_vagrant() 388 | self._write_configs() 389 | self._has_error = None 390 | self._datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 391 | self.result = {} 392 | 393 | @contextlib.contextmanager 394 | def stdout_cm(self): 395 | """Redirect the stdout to a log file.""" 396 | with open(self._get_stdout_log(), "a+") as fh: 397 | msg = "### {} ###\n".format(self._datetime) 398 | fh.write(msg) 399 | fh.flush() 400 | 401 | yield fh 402 | 403 | @contextlib.contextmanager 404 | def stderr_cm(self): 405 | """Redirect the stderr to a log file.""" 406 | with open(self._get_stderr_log(), "a+") as fh: 407 | msg = "### {} ###\n".format(self._datetime) 408 | fh.write(msg) 409 | fh.flush() 410 | 411 | try: 412 | yield fh 413 | except subprocess.CalledProcessError as e: 414 | self._has_error = True 415 | # msg = "CMD: {} returned {}\n{}".format( 416 | # e.cmd, e.returncode, e.output or "" 417 | # ) 418 | self.result["cmd"] = e.cmd 419 | self.result["rc"] = e.returncode 420 | self.result["stderr"] = e.output or "" 421 | 422 | # fh.write(msg) 423 | # raise 424 | except Exception as e: 425 | self._has_error = True 426 | if hasattr(e, "message"): 427 | fh.write(e.message) 428 | else: 429 | fh.write(e) 430 | fh.flush() 431 | raise 432 | 433 | def up(self): 434 | changed = False 435 | if self._running() != len(self.instances): 436 | changed = True 437 | provision = self.provision 438 | try: 439 | self._vagrant.up(provision=provision) 440 | except Exception: 441 | # NOTE(retr0h): Ignore the exception since python-vagrant 442 | # passes the actual error as a no-argument ContextManager. 443 | pass 444 | 445 | # NOTE(retr0h): Ansible wants only one module return `fail_json` 446 | # or `exit_json`. 447 | if not self._has_error: 448 | # compat 449 | if self._module.params["instance_name"] is not None: 450 | self._module.exit_json( 451 | changed=changed, log=self._get_stdout_log(), **self._conf()[0] 452 | ) 453 | self._module.exit_json( 454 | changed=changed, log=self._get_stdout_log(), results=self._conf() 455 | ) 456 | 457 | msg = "Failed to start the VM(s): See log file '{}'".format( 458 | self._get_stderr_log() 459 | ) 460 | with io.open(self._get_stderr_log(), "r", encoding="utf-8") as f: 461 | self.result["stderr"] = f.read() 462 | self._module.fail_json(msg=msg, **self.result) 463 | 464 | def destroy(self): 465 | changed = False 466 | if self._created() > 0: 467 | changed = True 468 | if self._module.params["force_stop"]: 469 | self._vagrant.halt(force=True) 470 | self._vagrant.destroy() 471 | 472 | self._module.exit_json(changed=changed) 473 | 474 | def halt(self): 475 | changed = False 476 | if self._running() > 0: 477 | changed = True 478 | self._vagrant.halt(force=self._module.params["force_stop"]) 479 | 480 | self._module.exit_json(changed=changed) 481 | 482 | def _conf_instance(self, instance_name): 483 | try: 484 | return self._vagrant.conf(vm_name=instance_name) 485 | except Exception: 486 | msg = "Failed to get vagrant config for {}: See log file '{}'".format( 487 | instance_name, self._get_stderr_log() 488 | ) 489 | with io.open(self._get_stderr_log(), "r", encoding="utf-8") as f: 490 | self.result["stderr"] = f.read() 491 | self._module.fail_json(msg=msg, **self.result) 492 | 493 | def _status_instance(self, instance_name): 494 | try: 495 | s = self._vagrant.status(vm_name=instance_name)[0] 496 | 497 | return {"name": s.name, "state": s.state, "provider": s.provider} 498 | except Exception: 499 | msg = "Failed to get status for {}: See log file '{}'".format( 500 | instance_name, self._get_stderr_log() 501 | ) 502 | with io.open(self._get_stderr_log(), "r", encoding="utf-8") as f: 503 | self.result["stderr"] = f.read() 504 | self._module.fail_json(msg=msg, **self.result) 505 | 506 | def _conf(self): 507 | conf = [] 508 | 509 | for i in self.instances: 510 | instance_name = i["name"] 511 | c = self._conf_instance(instance_name) 512 | if c: 513 | conf.append(c) 514 | 515 | return conf 516 | 517 | def _status(self): 518 | vms_status = [] 519 | 520 | for i in self.instances: 521 | instance_name = i["name"] 522 | s = self._status_instance(instance_name) 523 | if s: 524 | vms_status.append(s) 525 | 526 | return vms_status 527 | 528 | def _created(self): 529 | status = self._status() 530 | if len(status) == 0: 531 | return 0 532 | 533 | count = sum(map(lambda s: s["state"] == "not_created", status)) 534 | return len(status) - count 535 | 536 | def _running(self): 537 | status = self._status() 538 | if len(status) == 0: 539 | return 0 540 | 541 | count = sum(map(lambda s: s["state"] == "running", status)) 542 | return count 543 | 544 | def _get_config(self): 545 | conf = dict() 546 | conf["workdir"] = os.getenv("MOLECULE_EPHEMERAL_DIRECTORY") 547 | if self._module.params["workdir"] is not None: 548 | conf["workdir"] = self._module.params["workdir"] 549 | if conf["workdir"] is None: 550 | self._module.fail_json( 551 | msg="Either workdir parameter or MOLECULE_EPHEMERAL_DIRECTORY env variable has to be set" 552 | ) 553 | conf["vagrantfile"] = os.path.join(conf["workdir"], "Vagrantfile") 554 | return conf 555 | 556 | def _write_vagrantfile(self): 557 | instances = self._get_vagrant_config_dict() 558 | template = molecule.util.render_template( 559 | VAGRANTFILE_TEMPLATE, 560 | instances=instances, 561 | cachier=self.cachier, 562 | no_kvm=not os.path.exists("/dev/kvm"), 563 | ) 564 | molecule.util.write_file(self._vagrantfile, template) 565 | 566 | def _write_configs(self): 567 | self._write_vagrantfile() 568 | try: 569 | self._vagrant.validate(self._config["workdir"]) 570 | except subprocess.CalledProcessError as e: 571 | self._module.fail_json( 572 | msg=f"Failed to validate generated Vagrantfile: {e.stderr}" 573 | ) 574 | 575 | def _get_vagrant(self): 576 | vagrant_env = os.environ.copy() 577 | if self._module.params["parallel"] is False: 578 | vagrant_env["VAGRANT_NO_PARALLEL"] = "1" 579 | v = vagrant.Vagrant( 580 | out_cm=self.stdout_cm, 581 | err_cm=self.stderr_cm, 582 | root=self._config["workdir"], 583 | env=vagrant_env, 584 | ) 585 | 586 | return v 587 | 588 | def _get_instance_vagrant_config_dict(self, instance): 589 | 590 | checksum = instance.get("box_download_checksum") 591 | checksum_type = instance.get("box_download_checksum_type") 592 | if bool(checksum) ^ bool(checksum_type): 593 | self._module.fail_json( 594 | msg="box_download_checksum and box_download_checksum_type must be used together" 595 | ) 596 | 597 | networks = [] 598 | if "interfaces" in instance: 599 | for iface in instance["interfaces"]: 600 | net = dict() 601 | net["name"] = iface["network_name"] 602 | iface.pop("network_name") 603 | net["options"] = iface 604 | networks.append(net) 605 | 606 | # compat 607 | provision = instance.get("provision") 608 | if provision is not None: 609 | self.provision = self.provision or provision 610 | self._module.warn( 611 | "Please convert your molecule.yml to move provision parameter to driver:. Compat layer will be removed later." 612 | ) 613 | d = { 614 | "name": instance.get("name"), 615 | "hostname": instance.get("hostname", instance.get("name")), 616 | "memory": instance.get("memory", 512), 617 | "cpus": instance.get("cpus", 2), 618 | "networks": networks, 619 | "instance_raw_config_args": instance.get("instance_raw_config_args", None), 620 | "config_options": { 621 | # NOTE(retr0h): `synced_folder` does not represent the 622 | # actual key used by Vagrant. Is used as a flag to 623 | # simply enable/disable shared folder. 624 | "synced_folder": False, 625 | "ssh.insert_key": True, 626 | }, 627 | "box": instance.get("box", self._module.params["default_box"]), 628 | "box_version": instance.get("box_version"), 629 | "box_url": instance.get("box_url"), 630 | "box_download_checksum": checksum, 631 | "box_download_checksum_type": checksum_type, 632 | "provider": self._module.params["provider_name"], 633 | "provider_options": {}, 634 | "provider_raw_config_args": instance.get("provider_raw_config_args", None), 635 | "provider_override_args": instance.get("provider_override_args", None), 636 | } 637 | 638 | d["config_options"].update( 639 | molecule.util.merge_dicts( 640 | d["config_options"], instance.get("config_options", {}) 641 | ) 642 | ) 643 | if "cachier" in d["config_options"]: 644 | self.cachier = d["config_options"]["cachier"] 645 | self._module.warn( 646 | "Please convert your molecule.yml to move cachier parameter to driver:. Compat layer will be removed later." 647 | ) 648 | 649 | d["provider_options"].update( 650 | molecule.util.merge_dicts( 651 | d["provider_options"], instance.get("provider_options", {}) 652 | ) 653 | ) 654 | 655 | return d 656 | 657 | def _get_vagrant_config_dict(self): 658 | config_list = [] 659 | for instance in self.instances: 660 | config_list.append(self._get_instance_vagrant_config_dict(instance)) 661 | return config_list 662 | 663 | def _get_stdout_log(self): 664 | return self._get_vagrant_log("out") 665 | 666 | def _get_stderr_log(self): 667 | return self._get_vagrant_log("err") 668 | 669 | def _get_vagrant_log(self, __type): 670 | return os.path.join(self._config["workdir"], "vagrant.{}".format(__type)) 671 | 672 | 673 | def main(): 674 | module = AnsibleModule( 675 | argument_spec=dict( 676 | instances=dict(type="list", required=False), 677 | instance_name=dict(type="str", required=False, default=None), 678 | instance_interfaces=dict(type="list", default=[]), 679 | instance_raw_config_args=dict(type="list", default=None), 680 | config_options=dict(type="dict", default={}), 681 | platform_box=dict(type="str", required=False), 682 | platform_box_version=dict(type="str"), 683 | platform_box_url=dict(type="str"), 684 | platform_box_download_checksum=dict(type="str"), 685 | platform_box_download_checksum_type=dict(type="str"), 686 | provider_memory=dict(type="int", default=512), 687 | provider_cpus=dict(type="int", default=2), 688 | provider_options=dict(type="dict", default={}), 689 | provider_override_args=dict(type="list", default=None), 690 | provider_raw_config_args=dict(type="list", default=None), 691 | provider_name=dict(type="str", default="virtualbox"), 692 | default_box=dict(type="str", default=None), 693 | provision=dict(type="bool", default=False), 694 | force_stop=dict(type="bool", default=False), 695 | cachier=dict(type="str", default="machine"), 696 | state=dict(type="str", default="up", choices=["up", "destroy", "halt"]), 697 | workdir=dict(type="str"), 698 | parallel=dict(type="bool", default=True), 699 | ), 700 | required_together=[ 701 | ("platform_box_download_checksum", "platform_box_download_checksum_type"), 702 | ("instances", "default_box"), 703 | ], 704 | supports_check_mode=False, 705 | ) 706 | 707 | if not (bool(module.params["instances"]) ^ bool(module.params["instance_name"])): 708 | module.fail_json( 709 | msg="Either instances or instance_name parameters should be used and not at the same time" 710 | ) 711 | 712 | v = VagrantClient(module) 713 | 714 | if module.params["state"] == "up": 715 | v.up() 716 | 717 | if module.params["state"] == "destroy": 718 | v.destroy() 719 | 720 | if module.params["state"] == "halt": 721 | v.halt() 722 | 723 | module.fail_json(msg="Unknown error", **v.result) 724 | 725 | 726 | if __name__ == "__main__": 727 | main() 728 | -------------------------------------------------------------------------------- /molecule_vagrant/playbooks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | tasks: 8 | - name: Create molecule instance(s) # noqa fqcn[action] 9 | vagrant: 10 | instances: "{{ molecule_yml.platforms }}" 11 | default_box: "{{ molecule_yml.driver.default_box | default('generic/alpine316') }}" 12 | provider_name: "{{ molecule_yml.driver.provider.name | default(omit, true) }}" 13 | provision: "{{ molecule_yml.driver.provision | default(omit) }}" 14 | cachier: "{{ molecule_yml.driver.cachier | default(omit) }}" 15 | parallel: "{{ molecule_yml.driver.parallel | default(omit) }}" 16 | state: up 17 | register: server 18 | no_log: false 19 | 20 | # NOTE(retr0h): Vagrant/VirtualBox sucks and parallelizing instance creation 21 | # causes issues. 22 | 23 | # Mandatory configuration for Molecule to function. 24 | 25 | - name: Create molecule instances configuration 26 | when: server is changed # noqa no-handler 27 | block: 28 | 29 | - name: Populate instance config dict 30 | ansible.builtin.set_fact: 31 | instance_conf_dict: { 32 | 'instance': "{{ item.Host }}", 33 | 'address': "{{ item.HostName }}", 34 | 'user': "{{ item.User }}", 35 | 'port': "{{ item.Port }}", 36 | 'identity_file': "{{ item.IdentityFile }}", } 37 | with_items: "{{ server.results }}" 38 | register: instance_config_dict 39 | 40 | - name: Convert instance config dict to a list 41 | ansible.builtin.set_fact: 42 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 43 | 44 | - name: Dump instance config 45 | ansible.builtin.copy: 46 | content: "{{ instance_conf | to_json | from_json | to_yaml }}" 47 | dest: "{{ molecule_instance_config }}" 48 | mode: 0600 49 | -------------------------------------------------------------------------------- /molecule_vagrant/playbooks/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | tasks: 8 | - name: Destroy molecule instance(s) # noqa fqcn[action] 9 | vagrant: 10 | instances: "{{ molecule_yml.platforms }}" 11 | default_box: "{{ molecule_yml.driver.default_box | default('generic/alpine316') }}" 12 | provider_name: "{{ molecule_yml.driver.provider.name | default(omit, true) }}" 13 | cachier: "{{ molecule_yml.driver.cachier | default(omit) }}" 14 | force_stop: "{{ item.force_stop | default(true) }}" 15 | state: destroy 16 | register: server 17 | no_log: false 18 | 19 | # NOTE(retr0h): Vagrant/VirtualBox sucks and parallelizing instance deletion 20 | # causes issues. 21 | 22 | # Mandatory configuration for Molecule to function. 23 | 24 | - name: Populate instance config 25 | ansible.builtin.set_fact: 26 | instance_conf: {} 27 | 28 | - name: Dump instance config # noqa no-handler 29 | ansible.builtin.copy: 30 | content: | 31 | # Molecule managed 32 | {{ instance_conf | to_json | from_json | to_yaml }} 33 | dest: "{{ molecule_instance_config }}" 34 | mode: 0600 35 | when: server.changed | bool 36 | -------------------------------------------------------------------------------- /molecule_vagrant/playbooks/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Bootstrap python for Ansible 7 | ansible.builtin.raw: | 8 | command -v python3 python || ( 9 | command -v apk >/dev/null && sudo apk add --no-progress --update python3 || 10 | (test -e /usr/bin/dnf && sudo dnf install -y python3) || 11 | (test -e /usr/bin/apt && (apt -y update && apt install -y python3-minimal)) || 12 | (test -e /usr/bin/yum && sudo yum -y -qq install python3) || 13 | (test -e /usr/sbin/pkg && sudo env ASSUME_ALWAYS_YES=yes pkg update && sudo env ASSUME_ALWAYS_YES=yes pkg install python3) || 14 | (test -e /usr/sbin/pkg_add && sudo /usr/sbin/pkg_add -U -I -x python%3.9) || 15 | echo "Warning: Python not bootstrapped due to unknown platform." 16 | ) 17 | become: true 18 | changed_when: false 19 | -------------------------------------------------------------------------------- /molecule_vagrant/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-vagrant/353b7e38d8da7605b9b93c57a3fa2c089d3821d2/molecule_vagrant/test/__init__.py -------------------------------------------------------------------------------- /molecule_vagrant/test/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-vagrant/353b7e38d8da7605b9b93c57a3fa2c089d3821d2/molecule_vagrant/test/functional/__init__.py -------------------------------------------------------------------------------- /molecule_vagrant/test/functional/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # Copyright (c) 2018 Red Hat, Inc. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to 6 | # deal in the Software without restriction, including without limitation the 7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | # sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | from molecule.test.conftest import * # noqa 24 | -------------------------------------------------------------------------------- /molecule_vagrant/test/functional/test_func.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # Copyright (c) 2018 Red Hat, Inc. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to 6 | # deal in the Software without restriction, including without limitation the 7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | # sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | 22 | import pytest 23 | import os 24 | import vagrant 25 | 26 | from molecule import util 27 | from molecule import logger 28 | from molecule.scenario import ephemeral_directory 29 | from molecule.util import run_command 30 | from molecule.test.conftest import change_dir_to 31 | 32 | LOG = logger.get_logger(__name__) 33 | 34 | 35 | # @pytest.mark.xfail(reason="need to fix template path") 36 | def test_command_init_scenario(temp_dir): 37 | with change_dir_to(temp_dir): 38 | os.makedirs(os.path.join(temp_dir, "molecule", "default")) 39 | scenario_directory = os.path.join(temp_dir, "molecule", "test-scenario") 40 | cmd = [ 41 | "molecule", 42 | "init", 43 | "scenario", 44 | "test-scenario", 45 | "--driver-name", 46 | "vagrant", 47 | ] 48 | result = run_command(cmd) 49 | assert result.returncode == 0 50 | 51 | assert os.path.isdir(scenario_directory) 52 | confpath = os.path.join(scenario_directory, "molecule.yml") 53 | conf = util.safe_load_file(confpath) 54 | env = os.environ 55 | if "TESTBOX" in env: 56 | conf["platforms"][0]["box"] = env["TESTBOX"] 57 | if "vagrant-libvirt" in list( 58 | map(lambda x: x.name, vagrant.Vagrant().plugin_list()) 59 | ): 60 | conf["driver"]["provider"] = {"name": "libvirt"} 61 | util.write_file(confpath, util.safe_dump(conf)) 62 | 63 | cmd = ["molecule", "--debug", "test", "-s", "test-scenario"] 64 | result = run_command(cmd) 65 | assert result.returncode == 0 66 | 67 | 68 | def test_invalide_settings(temp_dir): 69 | 70 | scenario_directory = os.path.join( 71 | os.path.dirname(util.abs_path(__file__)), os.path.pardir, "scenarios" 72 | ) 73 | 74 | with change_dir_to(scenario_directory): 75 | cmd = ["molecule", "create", "--scenario-name", "invalid"] 76 | result = run_command(cmd) 77 | assert result.returncode == 2 78 | 79 | assert "Failed to validate generated Vagrantfile" in result.stdout 80 | 81 | 82 | @pytest.mark.parametrize( 83 | "scenario", 84 | [ 85 | ("vagrant_root"), 86 | ("config_options"), 87 | ("provider_config_options"), 88 | ("default"), 89 | ("default-compat"), 90 | ("network"), 91 | ("hostname"), 92 | ], 93 | ) 94 | def test_vagrant_root(temp_dir, scenario): 95 | 96 | scenario_directory = os.path.join( 97 | os.path.dirname(util.abs_path(__file__)), os.path.pardir, "scenarios" 98 | ) 99 | 100 | with change_dir_to(scenario_directory): 101 | cmd = ["molecule", "test", "--scenario-name", scenario] 102 | result = run_command(cmd) 103 | assert result.returncode == 0 104 | 105 | 106 | def test_multi_node(temp_dir): 107 | 108 | scenario_directory = os.path.join( 109 | os.path.dirname(util.abs_path(__file__)), os.path.pardir, "scenarios" 110 | ) 111 | 112 | with change_dir_to(scenario_directory): 113 | cmd = ["molecule", "test", "--scenario-name", "multi-node"] 114 | result = run_command(cmd) 115 | assert result.returncode == 0 116 | 117 | molecule_eph_directory = ephemeral_directory() 118 | vagrantfile = os.path.join( 119 | molecule_eph_directory, "scenarios", "multi-node", "Vagrantfile" 120 | ) 121 | with open(vagrantfile) as f: 122 | content = f.read() 123 | assert "instance-1" in content 124 | assert "instance-2" in content 125 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/config_options/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/config_options/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance 10 | config_options: 11 | synced_folder: true 12 | box: ${TESTBOX:-centos/7} 13 | instance_raw_config_args: 14 | - 'vm.synced_folder ".", "/vagrant", type: "rsync"' 15 | provisioner: 16 | name: ansible 17 | verifier: 18 | name: ansible 19 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/config_options/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | tasks: 5 | - name: Look for /vagrant 6 | ansible.builtin.stat: 7 | path: /vagrant 8 | register: vagrantdir 9 | 10 | - name: Make sure there's a /vagrant 11 | ansible.builtin.assert: 12 | that: 13 | - vagrantdir.stat.exists | bool 14 | - vagrantdir.stat.isdir is defined 15 | - vagrantdir.stat.isdir | bool 16 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/default-compat/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.command: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/default-compat/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | tasks: 8 | - name: Create molecule instance(s) # noqa fqcn[action] 9 | vagrant: 10 | instance_name: "{{ item.name }}" 11 | instance_interfaces: "{{ item.interfaces | default(omit) }}" 12 | instance_raw_config_args: "{{ item.instance_raw_config_args | default(omit) }}" 13 | 14 | config_options: "{{ item.config_options | default(omit) }}" 15 | 16 | platform_box: "{{ item.box | default('generic/alpine316') }}" 17 | platform_box_version: "{{ item.box_version | default(omit) }}" 18 | platform_box_url: "{{ item.box_url | default(omit) }}" 19 | 20 | provider_name: "{{ molecule_yml.driver.provider.name | default(omit, true) }}" 21 | provider_memory: "{{ item.memory | default(omit) }}" 22 | provider_cpus: "{{ item.cpus | default(omit) }}" 23 | provider_options: "{{ item.provider_options | default(omit) }}" 24 | provider_raw_config_args: "{{ item.provider_raw_config_args | default(omit) }}" 25 | provider_override_args: "{{ item.provider_override_args | default(omit) }}" 26 | 27 | provision: "{{ item.provision | default(omit) }}" 28 | 29 | state: up 30 | register: server 31 | with_items: "{{ molecule_yml.platforms }}" 32 | loop_control: 33 | label: "{{ item.name }}" 34 | no_log: false 35 | 36 | # NOTE(retr0h): Vagrant/VBox sucks and parallelizing instance creation 37 | # causes issues. 38 | 39 | # Mandatory configuration for Molecule to function. 40 | 41 | - name: Create molecule instances configuration 42 | when: server is changed # noqa no-handler 43 | block: 44 | 45 | - name: Populate instance config dict 46 | ansible.builtin.set_fact: 47 | instance_conf_dict: { 48 | 'instance': "{{ item.Host }}", 49 | 'address': "{{ item.HostName }}", 50 | 'user': "{{ item.User }}", 51 | 'port': "{{ item.Port }}", 52 | 'identity_file': "{{ item.IdentityFile }}", } 53 | with_items: "{{ server.results }}" 54 | register: instance_config_dict 55 | 56 | - name: Convert instance config dict to a list 57 | ansible.builtin.set_fact: 58 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 59 | 60 | - name: Dump instance config 61 | ansible.builtin.copy: 62 | content: "{{ instance_conf | to_json | from_json | to_yaml }}" 63 | dest: "{{ molecule_instance_config }}" 64 | mode: 0600 65 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/default-compat/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | tasks: 8 | - name: Destroy molecule instance(s) # noqa fqcn[action] 9 | vagrant: 10 | instance_name: "{{ item.name }}" 11 | platform_box: "{{ item.box | default(omit) }}" 12 | provider_name: "{{ molecule_yml.driver.provider.name | default(omit, true) }}" 13 | provider_options: "{{ item.provider_options | default(omit) }}" 14 | provider_raw_config_args: "{{ item.provider_raw_config_args | default(omit) }}" 15 | force_stop: "{{ item.force_stop | default(true) }}" 16 | 17 | state: destroy 18 | register: server 19 | with_items: "{{ molecule_yml.platforms }}" 20 | loop_control: 21 | label: "{{ item.name }}" 22 | no_log: false 23 | 24 | # NOTE(retr0h): Vagrant/VBox sucks and parallelizing instance deletion 25 | # causes issues. 26 | 27 | # Mandatory configuration for Molecule to function. 28 | 29 | - name: Populate instance config 30 | ansible.builtin.set_fact: 31 | instance_conf: {} 32 | 33 | - name: Dump instance config # noqa no-handler 34 | ansible.builtin.copy: 35 | content: | 36 | # Molecule managed 37 | {{ instance_conf | to_json | from_json | to_yaml }} 38 | dest: "{{ molecule_instance_config }}" 39 | mode: 0600 40 | when: server.changed | bool 41 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/default-compat/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance 10 | provisioner: 11 | name: ansible 12 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance 10 | provisioner: 11 | name: ansible 12 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/hostname/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/hostname/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance-1 10 | box: ${TESTBOX:-debian/jessie64} 11 | memory: 256 12 | cpus: 1 13 | - name: instance-2 14 | hostname: instance.example.com 15 | box: ${TESTBOX:-debian/jessie64} 16 | memory: 256 17 | cpus: 1 18 | - name: instance-3 19 | hostname: false 20 | box: ${TESTBOX:-debian/jessie64} 21 | memory: 256 22 | cpus: 1 23 | provisioner: 24 | name: ansible 25 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/hostname/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check instance-1 3 | hosts: instance-1 4 | gather_facts: true 5 | gather_subset: 6 | - min 7 | tasks: 8 | - name: Ensure that host name is instance-1 9 | ansible.builtin.assert: 10 | that: 11 | - ansible_fqdn == "instance-1" 12 | 13 | - name: Check instance-2 14 | hosts: instance-2 15 | gather_facts: true 16 | gather_subset: 17 | - min 18 | tasks: 19 | - name: Ensure that host name is instance.example.com 20 | ansible.builtin.assert: 21 | that: 22 | - ansible_fqdn == "instance.example.com" 23 | 24 | - name: Check instance-3 25 | hosts: instance-3 26 | gather_facts: true 27 | gather_subset: 28 | - min 29 | tasks: 30 | - name: Ensure that host name is not instance-3 31 | ansible.builtin.assert: 32 | that: 33 | - ansible_fqdn != "instance-3" 34 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/invalid/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/invalid/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance/foo 10 | provisioner: 11 | name: ansible 12 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/multi-node/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/multi-node/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance-1 10 | box: ${TESTBOX:-debian/jessie64} 11 | interfaces: 12 | - network_name: private_network 13 | ip: 192.168.56.2 14 | groups: 15 | - foo 16 | - bar 17 | memory: 256 18 | cpus: 1 19 | provider_options: 20 | # using session with network leads to troubles 21 | qemu_use_session: false 22 | config_options: 23 | synced_folder: true 24 | instance_raw_config_args: 25 | - 'vm.synced_folder ".", "/vagrant", type: "rsync"' 26 | - name: instance-2 27 | box: ${TESTBOX:-centos/7} 28 | interfaces: 29 | - network_name: private_network 30 | ip: 192.168.56.3 31 | groups: 32 | - foo 33 | - baz 34 | memory: 256 35 | cpus: 2 36 | provider_options: 37 | # using session with network leads to troubles 38 | qemu_use_session: false 39 | instance_raw_config_args: 40 | - 'vm.synced_folder ".", "/vagrant", type: "rsync"' 41 | provisioner: 42 | name: ansible 43 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/multi-node/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check instance-1 3 | hosts: instance-1 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Ping instance-2 # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: ping -c3 192.168.56.3 10 | changed_when: false 11 | 12 | - name: Change instance-2 13 | hosts: instance-2 14 | gather_facts: false 15 | become: true 16 | tasks: 17 | - name: Ping instance-1 # noqa command-instead-of-shell 18 | ansible.builtin.shell: 19 | cmd: ping -c3 192.168.56.2 20 | changed_when: false 21 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/network/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/network/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance 10 | box: ${TESTBOX:-centos/7} 11 | provider_options: 12 | # using session with network leads to troubles 13 | qemu_use_session: false 14 | interfaces: 15 | - network_name: private_network 16 | ip: 192.168.56.4 17 | auto_config: true 18 | provisioner: 19 | name: ansible 20 | verifier: 21 | name: ansible 22 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/network/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | gather_facts: true 5 | gather_subset: 6 | - network 7 | tasks: 8 | - name: Check that there are 3 interfaces 9 | ansible.builtin.assert: 10 | that: 11 | - "{{ ansible_interfaces | length == 3 }}" 12 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/provider_config_options/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/provider_config_options/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | platforms: 9 | - name: instance 10 | provider_options: 11 | nic_model_type: e1000 12 | box: ${TESTBOX:-centos/7} 13 | provisioner: 14 | name: ansible 15 | verifier: 16 | name: ansible 17 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/provider_config_options/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | gather_facts: true 5 | gather_subset: 6 | - network 7 | tasks: 8 | - name: Set interface dict name 9 | ansible.builtin.set_fact: 10 | iface: "{{ ansible_default_ipv4.interface }}" 11 | 12 | - name: Check network card pci infos 13 | ansible.builtin.assert: 14 | that: 15 | - "ansible_facts[iface].module == 'e1000'" 16 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/vagrant_root/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Sample task # noqa command-instead-of-shell 8 | ansible.builtin.shell: 9 | cmd: uname 10 | changed_when: false 11 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/vagrant_root/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: libvirt 8 | provision: true 9 | platforms: 10 | - name: instance 11 | box: ${TESTBOX:-centos/7} 12 | instance_raw_config_args: 13 | - "vm.provision :shell, inline: \"echo #{Dir.pwd} > /tmp/workdir\"" 14 | provisioner: 15 | name: ansible 16 | verifier: 17 | name: ansible 18 | -------------------------------------------------------------------------------- /molecule_vagrant/test/scenarios/molecule/vagrant_root/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | tasks: 5 | - name: Look for /tmp/workdir 6 | ansible.builtin.stat: 7 | path: /tmp/workdir 8 | register: workdir 9 | 10 | - name: Make sure there's a /vagrant 11 | ansible.builtin.assert: 12 | that: 13 | - workdir.stat.exists | bool 14 | 15 | - name: Get /tmp/workdir file content 16 | ansible.builtin.command: 17 | cmd: cat /tmp/workdir 18 | changed_when: false 19 | register: workdir_content 20 | 21 | - name: Print molecule ephemeral directory 22 | ansible.builtin.debug: 23 | msg: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}" 24 | 25 | - name: Print workdir file content 26 | ansible.builtin.debug: 27 | var: workdir_content.stdout 28 | 29 | - name: Check /tmp/workdir content 30 | ansible.builtin.assert: 31 | that: 32 | - "workdir_content.stdout == lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY')" 33 | -------------------------------------------------------------------------------- /molecule_vagrant/test/test_driver.py: -------------------------------------------------------------------------------- 1 | from molecule import api 2 | 3 | 4 | def test_driver_is_detected(): 5 | assert "vagrant" in [str(d) for d in api.drivers()] 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 45.0.0", # required by pyproject+setuptools_scm integration 4 | "setuptools_scm[toml] >= 7.0.0", # required for "no-local-version" scheme 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.pytest] 9 | addopts = "-v -rxXs --doctest-modules --durations 10 --no-cov-on-fail --cov=molecule_vagrant --cov-report term-missing:skip-covered" 10 | doctest_optionflags = "ALLOW_UNICODE ELLIPSIS" 11 | junit_suite_name = "molecule_test_suite" 12 | norecursedirs = "dist doc build .tox .eggs test/scenarios test/resources" 13 | 14 | [tool.setuptools_scm] 15 | local_scheme = "no-local-version" 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | molecule >= 3.4.1 2 | pyyaml >= 5.1 3 | Jinja2 >= 2.11.3 4 | selinux 5 | python-vagrant >= 1.0.0 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = molecule-vagrant 3 | url = https://github.com/ansible-community/molecule-vagrant 4 | project_urls = 5 | Bug Tracker = https://github.com/ansible-community/molecule-vagrant/issues 6 | Release Management = https://github.com/ansible-community/molecule-vagrant/releases 7 | CI: Zuul = https://dashboard.zuul.ansible.com/t/ansible/builds?project=ansible-community/molecule-vagrant 8 | Source Code = https://github.com/ansible-community/molecule-vagrant 9 | description = Vagrant Molecule Plugin :: run molecule tests using Vagrant 10 | long_description = file: README.rst 11 | long_description_content_type = text/x-rst 12 | author = Sorin Sbarnea 13 | author_email = sorin.sbarnea@gmail.com 14 | maintainer = Sorin Sbarnea 15 | maintainer_email = sorin.sbarnea@gmail.com 16 | license = MIT 17 | license_file = LICENSE 18 | classifiers = 19 | Development Status :: 5 - Production/Stable 20 | 21 | Environment :: Console 22 | 23 | Intended Audience :: Developers 24 | Intended Audience :: Information Technology 25 | Intended Audience :: System Administrators 26 | 27 | License :: OSI Approved :: MIT License 28 | Natural Language :: English 29 | Operating System :: OS Independent 30 | 31 | Programming Language :: Python :: 3 32 | Programming Language :: Python :: 3.9 33 | Programming Language :: Python :: 3.10 34 | 35 | Topic :: System :: Systems Administration 36 | Topic :: Utilities 37 | 38 | keywords = 39 | ansible 40 | roles 41 | testing 42 | molecule 43 | plugin 44 | vagrant 45 | 46 | [options] 47 | use_scm_version = True 48 | python_requires = >=3.9 49 | packages = find: 50 | include_package_data = True 51 | zip_safe = False 52 | 53 | # These are required in actual runtime: 54 | install_requires = 55 | # do not use ceiling unless you already know that newer version breaks 56 | # do not use pre-release versions 57 | molecule >= 3.4.1 58 | pyyaml >= 5.1 59 | Jinja2 >= 2.11.3 60 | selinux 61 | python-vagrant >= 1.0.0 62 | 63 | [options.extras_require] 64 | test = 65 | molecule[test] 66 | 67 | [options.entry_points] 68 | molecule.driver = 69 | vagrant = molecule_vagrant.driver:Vagrant 70 | 71 | [options.packages.find] 72 | where = . 73 | -------------------------------------------------------------------------------- /tools/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "generic/alpine316" 3 | config.vm.synced_folder '.', '/vagrant', disabled: true 4 | config.vm.provider :libvirt do |l| 5 | l.memory = 512 6 | l.nic_model_type = "e1000" 7 | l.driver = "qemu" 8 | l.cpu_mode = 'custom' 9 | l.cpu_model = 'qemu64' 10 | end 11 | config.vm.provision :shell, inline: "sudo apk add --no-progress --update python3 rsync" 12 | config.ssh.insert_key = false 13 | end 14 | -------------------------------------------------------------------------------- /tools/create_testbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | # Used to test that Vagrant is usable and also to pre-download the image 7 | # we will use during testing. 8 | cd "${DIR}" 9 | 10 | vagrant box list | grep -qw testbox && exit 0 11 | 12 | rm -f testbox.box 13 | vagrant up --no-tty --debug 14 | vagrant halt 15 | vagrant package --output testbox.box 16 | vagrant box add testbox.box --name testbox 17 | vagrant destroy -f 18 | -------------------------------------------------------------------------------- /tools/test-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | # Used by Zuul CI to perform extra bootstrapping 4 | 5 | sudo dd if=/dev/zero of=/swap.img bs=1024 count=1048576 6 | sudo chmod 600 /swap.img 7 | sudo losetup -f /swap.img 8 | sudo mkswap "$(sudo losetup --associated /swap.img|sed 's,:.*,,')" 9 | sudo swapon "$(sudo losetup --associated /swap.img|sed 's,:.*,,')" 10 | 11 | # Platforms coverage: 12 | # Fedora 30 : has vagrant-libvirt no compilation needed 13 | # CentOS 7 : install upstream vagrant rpm and compiles plugin (broken runtime) 14 | # CentOS 8 : install upstream vagrant rpm and compiles plugin (broken runtime) 15 | 16 | # Bumping system tox because version from CentOS 7 is too old 17 | # We are not using pip --user due to few bugs in tox role which does not allow 18 | # us to override how is called. Once these are addressed we will switch back 19 | # non-sudo 20 | command -v python3 python 21 | 22 | PYTHON=$(command -v python3 python|head -n1) 23 | PKG_CMD=$(command -v dnf yum apt-get|head -n1) 24 | 25 | sudo "${PYTHON}" -m pip install -U tox 26 | 27 | # === LIBVIRT SETUP === 28 | sudo systemctl enable --now libvirtd 29 | sudo sed \ 30 | -e 's!^[# ]*unix_sock_rw_perms = .*$!unix_sock_rw_perms = "0777"!g' \ 31 | -e 's!^[# ]*auth_unix_rw = .*$!auth_unix_rw = "polkit"!g' -i /etc/libvirt/libvirtd.conf 32 | # on fedora, looks like virbr0 iface stays there after a restart of libvirt but the network is not 33 | # started back, leading to network failure when the script tries to start the network 34 | if sudo virsh net-list --name | grep -qw default; then 35 | sudo virsh net-destroy default 36 | fi 37 | sudo systemctl restart libvirtd 38 | sudo usermod --append --groups libvirt "$(whoami)" 39 | # on some dists, it's auto-started, on some others, it's not 40 | if virsh -c qemu:///system net-list --name --inactive | grep -qw default; then 41 | virsh -c qemu:///system net-start default 42 | fi 43 | 44 | # only info about the virtualisation is wanted, so no error please. 45 | sudo virt-host-validate qemu || true 46 | 47 | # === VAGRANT SETUP === 48 | # Install Vagrant using their questionable practices, see locked ticket: 49 | # https://github.com/hashicorp/vagrant/issues/11070 50 | 51 | # 2.2.10 minimum otherwise setting config.vm.hostname won't work correctly with alpine boxes. 52 | VAGRANT_VERSION=2.2.19 53 | 54 | which vagrant || \ 55 | sudo "${PKG_CMD}" install -y vagrant-libvirt || { 56 | sudo "${PKG_CMD}" install -y https://releases.hashicorp.com/vagrant/${VAGRANT_VERSION}/vagrant_${VAGRANT_VERSION}_x86_64.rpm 57 | } 58 | 59 | if [ -f /etc/os-release ]; then 60 | source /etc/os-release 61 | case "$NAME" in 62 | Ubuntu) 63 | case "$VERSION_ID" in 64 | 18.04) 65 | # ubuntu xenial vagrant is too old so it doesn't support triggers, used by the alpine box 66 | sudo apt-get remove --purge -y vagrant 67 | wget --no-show-progress https://releases.hashicorp.com/vagrant/${VAGRANT_VERSION}/vagrant_${VAGRANT_VERSION}_x86_64.deb 68 | sudo dpkg -i vagrant_${VAGRANT_VERSION}_x86_64.deb 69 | ;; 70 | *) 71 | ;; 72 | esac 73 | ;; 74 | Fedora) 75 | case "$VERSION_ID" in 76 | 31) 77 | # https://bugzilla.redhat.com/show_bug.cgi?id=1839651 78 | sudo "${PKG_CMD}" upgrade -y --enablerepo=updates-testing --advisory=FEDORA-2020-09c472786c 79 | ;; 80 | *) 81 | ;; 82 | esac 83 | ;; 84 | CentOS*) 85 | # https://github.com/hashicorp/vagrant/issues/11020 86 | if grep -qi '^CentOS Linux release 8.2.*' /etc/centos-release ; then 87 | # https://bugs.centos.org/view.php?id=17120 88 | relver="$(grep -v '^#' /etc/centos-release | awk '{print $4}')" 89 | sudo sed -i /etc/yum.repos.d/CentOS-Sources.repo -e 's,$contentdir/,,g' 90 | sudo sed -i /etc/yum.repos.d/CentOS-Sources.repo -e "s,\$releasever,$relver,g" 91 | 92 | sudo dnf install -y rpm-build autoconf libselinux-devel pam-devel bison byacc 93 | mkdir -p "$HOME/rpmbuild/SOURCES" 94 | cd "$HOME/rpmbuild/SOURCES" 95 | # download as root to avoid the "error: [Errno 13] Permission denied: '/var/cache/dnf/expired_repos.json'" 96 | sudo dnf download --enablerepo=BaseOS-source --disablerepo=epel-source --disablerepo=epel --source krb5-libs 97 | rpm2cpio krb5-1.17-*.src.rpm | cpio -idv 98 | # remove patch making incompatible with the openssl bundled with vagrant 99 | sed -i ./krb5.spec -e 's,Patch.*Use-backported-version-of-OpenSSL-3-KDF-interface.patch,,' 100 | # depends on previous patch 101 | sed -i ./krb5.spec -e 's,Patch.*krb5-1.17post2-DES-3DES-fixups.patch,,' 102 | # not sure why but makes the build fail 103 | sed -i ./krb5.spec -e 's,Patch.*krb5-1.17post6-FIPS-with-PRNG-and-RADIUS-and-MD4.patch,,' 104 | rpmbuild -bp krb5.spec --nodeps 105 | cd ../BUILD/krb5-1.17/src 106 | # Some flags are missing compared to the spec but these ones seem to be enough 107 | export CFLAGS="-I/opt/vagrant/embedded/include/ -fPIC -fno-strict-aliasing -fstack-protector-all" 108 | export LDFLAGS=-L/opt/vagrant/embedded/lib64/ 109 | ./configure --prefix=/opt/vagrant/embedded/ 110 | make 111 | sudo cp -a lib/crypto/libk5crypto.so.3* /opt/vagrant/embedded/lib64/ 112 | fi 113 | ;; 114 | *) 115 | ;; 116 | esac 117 | fi 118 | 119 | vagrant plugin list | grep vagrant-libvirt || { 120 | export CONFIGURE_ARGS="with-libvirt-include=/usr/include/libvirt with-libvirt-lib=/usr/lib64" 121 | if [ -x /opt/vagrant/bin/vagrant ]; then 122 | # command line from https://github.com/vagrant-libvirt/vagrant-libvirt#installation 123 | export GEM_HOME=~/.vagrant.d/gems 124 | export GEM_PATH=$GEM_HOME:/opt/vagrant/embedded/gems 125 | export PATH=/opt/vagrant/embedded/bin:$PATH 126 | export CONFIGURE_ARGS='with-ldflags=-L/opt/vagrant/embedded/lib with-libvirt-include=/usr/include/libvirt with-libvirt-lib=/usr/lib' 127 | fi 128 | vagrant plugin install vagrant-libvirt 129 | } 130 | 131 | if [ -f /etc/debian_version ]; then 132 | dpkg -l | grep libselinux 133 | [ -x /usr/bin/aa-enabled ] && echo "Apparmor: $(/usr/bin/aa-enabled)" 134 | else 135 | rpm -qa | grep libselinux 136 | fi 137 | 138 | vagrant version 139 | vagrant global-status 140 | 141 | vagrant plugin list | tee >(grep -q "No plugins installed." && { 142 | echo "FATAL: Vagrant is not usable without any provider plugins." 143 | exit 1 144 | }) 145 | 146 | timeout 600 ./tools/create_testbox.sh 147 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.io/en/latest/ 2 | [tox] 3 | minversion = 4.0.16 4 | envlist = 5 | lint 6 | pkg 7 | py 8 | py-devel 9 | isolated_build = True 10 | requires = 11 | pip >= 21.3.1 12 | 13 | [testenv] 14 | description = 15 | Unit testing 16 | extras = 17 | test 18 | deps = 19 | --editable . 20 | ansible-core 21 | py{39,310,311,312}: molecule[test] 22 | py{39,310,311,312}-{devel}: git+https://github.com/ansible-community/molecule.git@main#egg=molecule[test] 23 | commands = 24 | pytest --collect-only 25 | # -s is added in order to allow live output on long running functional tests 26 | pytest --color=yes -s 27 | setenv = 28 | ANSIBLE_DISPLAY_FAILED_STDERR=1 29 | ANSIBLE_VERBOSITY=1 30 | ANSIBLE_FORCE_COLOR={env:ANSIBLE_FORCE_COLOR:1} 31 | ANSIBLE_INVENTORY={toxinidir}/tests/hosts.ini 32 | ANSIBLE_CONFIG={toxinidir}/ansible.cfg 33 | ANSIBLE_NOCOWS=1 34 | ANSIBLE_RETRY_FILES_ENABLED=0 35 | ANSIBLE_GATHERING={env:ANSIBLE_GATHERING:explicit} 36 | ANSIBLE_VERBOSITY={env:ANSIBLE_VERBOSITY:0} 37 | PIP_DISABLE_PIP_VERSION_CHECK=1 38 | PY_COLORS={env:PY_COLORS:1} 39 | # pip: Avoid 2020-01-01 warnings: https://github.com/pypa/pip/issues/6207 40 | PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command 41 | PYTHONDONTWRITEBYTECODE=1 42 | # This should pass these args to molecule, no effect here as this is the default 43 | # but it validates that it accepts extra params. 44 | MOLECULE_OPTS=--destroy always 45 | MOLECULE_NO_LOG=false 46 | _EXTRAS=-l --html={envlogdir}/reports.html --self-contained-html 47 | PYTEST_ADDOPTS={env:_EXTRAS} {env:PYTEST_ADDOPTS:} 48 | TESTBOX={env:TESTBOX:testbox} 49 | passenv = 50 | CI 51 | CURL_CA_BUNDLE 52 | DOCKER_* 53 | HOME 54 | PYTEST_* 55 | REQUESTS_CA_BUNDLE 56 | SSH_AUTH_SOCK 57 | SSL_CERT_FILE 58 | TOXENV 59 | TRAVIS 60 | TRAVIS_* 61 | TWINE_* 62 | VAGRANT_HOME 63 | whitelist_externals = 64 | bash 65 | twine 66 | pytest 67 | pre-commit 68 | 69 | [testenv:pkg] 70 | description = 71 | Build package, verify metadata, install package and assert behavior when ansible is missing. 72 | deps = 73 | build >= 0.7.0, < 0.8.0 74 | twine 75 | skip_install = true 76 | # Ref: https://twitter.com/di_codes/status/1044358639081975813 77 | commands = 78 | # build wheel and sdist using PEP-517 79 | {envpython} -c 'import os.path, shutil, sys; \ 80 | dist_dir = os.path.join("{toxinidir}", "dist"); \ 81 | os.path.isdir(dist_dir) or sys.exit(0); \ 82 | print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \ 83 | shutil.rmtree(dist_dir)' 84 | {envpython} -m build \ 85 | --outdir {toxinidir}/dist/ \ 86 | {toxinidir} 87 | # Validate metadata using twine 88 | twine check --strict {toxinidir}/dist/* 89 | 90 | [testenv:lint] 91 | description = Performs linting, style checks 92 | skip_install = true 93 | deps = 94 | pre-commit 95 | commands = 96 | pre-commit run -a 97 | -------------------------------------------------------------------------------- /zuul.d/layout.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # zuul.d/layout.yaml 3 | 4 | - job: 5 | name: molecule-vagrant-fedora-py310 6 | description: Run py310 tox environment 7 | parent: ansible-tox-py310 8 | nodeset: fedora-latest-1vcpu 9 | attempts: 2 10 | vars: 11 | tox_envlist: py310 12 | timeout: 5400 # 1.5h 13 | 14 | - job: 15 | name: molecule-vagrant-fedora-py39 16 | description: Run py39 tox environment 17 | parent: ansible-tox-py39 18 | nodeset: fedora-latest-1vcpu 19 | attempts: 2 20 | vars: 21 | tox_envlist: py39 22 | timeout: 5400 # 1.5h 23 | 24 | - project: 25 | check: 26 | jobs: &defaults 27 | - molecule-vagrant-fedora-py310 28 | - molecule-vagrant-fedora-py39 29 | --------------------------------------------------------------------------------