├── .ansible-lint ├── .flake8 ├── .github ├── FUNDING.yml ├── labels.yml ├── release-drafter.yml └── workflows │ └── tox.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .yamllint ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bindep.txt ├── conftest.py ├── molecule_ec2 ├── __init__.py ├── cookiecutter │ ├── cookiecutter.json │ └── {{cookiecutter.molecule_directory}} │ │ └── {{cookiecutter.scenario_name}} │ │ ├── INSTALL.rst │ │ ├── converge.yml │ │ ├── create.yml │ │ ├── destroy.yml │ │ └── prepare.yml ├── driver.py └── test │ ├── __init__.py │ ├── functional │ ├── .ansible-lint │ ├── __init__.py │ ├── conftest.py │ └── test_ec2.py │ ├── scenarios │ └── driver │ │ └── ec2 │ │ └── molecule │ │ ├── default │ │ ├── converge.yml │ │ ├── molecule.yml │ │ ├── prepare.yml │ │ └── tests │ │ │ └── test_default.py │ │ └── multi-node │ │ ├── converge.yml │ │ ├── molecule.yml │ │ ├── prepare.yml │ │ └── tests │ │ ├── __init__.py │ │ └── test_default.py │ └── test_driver.py ├── platforms.rst ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.cfg ├── setup.py ├── test_requirements.yml ├── tools └── test-setup.sh └── tox.ini /.ansible-lint: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - molecule_ec2/cookiecutter/{{cookiecutter.molecule_directory}} 3 | -------------------------------------------------------------------------------- /.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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ssbarnea 4 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | # Format and labels used aim to match those used by Ansible project 2 | # https://github.com/marketplace/actions/github-labeler 3 | - name: bug 4 | color: "fbca04" 5 | description: "This issue/PR relates to a bug." 6 | - name: deprecated 7 | color: "fef2c0" 8 | description: "This issue/PR relates to a deprecated module." 9 | - name: docs 10 | color: "4071a5" 11 | description: "This issue/PR relates to or includes documentation." 12 | - name: enhancement 13 | color: "ededed" 14 | description: "This issue/PR relates to a feature request." 15 | - name: feature 16 | color: "006b75" 17 | description: "This issue/PR relates to a feature request." 18 | - name: major 19 | color: "c6476b" 20 | description: "Marks an important and likely breaking change." 21 | - name: packaging 22 | color: "4071a5" 23 | description: "Packaging category" 24 | - name: performance 25 | color: "555555" 26 | description: "Relates to product or testing performance." 27 | - name: skip-changelog 28 | color: "eeeeee" 29 | description: "Can be missed from the changelog." 30 | - name: stale 31 | color: "eeeeee" 32 | description: "Not updated in long time, will be closed soon." 33 | - name: wontfix 34 | color: "eeeeee" 35 | description: "This will not be worked on" 36 | - name: test 37 | color: "0e8a16" 38 | description: "This PR relates to tests, QA, CI." 39 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: 'Features' 3 | labels: 4 | - 'feature' 5 | - 'enhancement' 6 | - title: 'Bug Fixes' 7 | labels: 8 | - 'fix' 9 | - 'bugfix' 10 | - 'bug' 11 | - title: 'Maintenance' 12 | label: 'chore' 13 | exclude-labels: 14 | - 'skip-changelog' 15 | template: | 16 | ## Changes 17 | 18 | $CHANGES 19 | -------------------------------------------------------------------------------- /.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 | - "master" 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 | runs-on: ubuntu-latest 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | include: 25 | - tox_env: lint 26 | - tox_env: py38 27 | PREFIX: PYTEST_REQPASS=2 28 | - tox_env: py39 29 | PREFIX: PYTEST_REQPASS=2 30 | - tox_env: py310 31 | PREFIX: PYTEST_REQPASS=2 32 | - tox_env: py310-devel 33 | PREFIX: PYTEST_REQPASS=2 34 | - tox_env: packaging 35 | steps: 36 | - uses: actions/checkout@v1 37 | - name: Find python version 38 | id: py_ver 39 | shell: python 40 | if: ${{ contains(matrix.tox_env, 'py') }} 41 | run: | 42 | v = '${{ matrix.tox_env }}'.split('-')[0].lstrip('py') 43 | print('::set-output name=version::{0}.{1}'.format(v[0],v[1:])) 44 | # Even our lint and other envs need access to tox 45 | - name: Install a default Python 46 | uses: actions/setup-python@v2 47 | if: ${{ ! contains(matrix.tox_env, 'py') }} 48 | # Be sure to install the version of python needed by a specific test, if necessary 49 | - name: Set up Python version 50 | uses: actions/setup-python@v2 51 | if: ${{ contains(matrix.tox_env, 'py') }} 52 | with: 53 | python-version: ${{ steps.py_ver.outputs.version }} 54 | - name: Install dependencies 55 | run: | 56 | python -m pip install -U pip 57 | pip install tox 58 | - name: Run tox -e ${{ matrix.tox_env }} 59 | run: | 60 | echo "${{ matrix.PREFIX }} tox -e ${{ matrix.tox_env }}" 61 | ${{ matrix.PREFIX }} tox -e ${{ matrix.tox_env }} 62 | 63 | publish: 64 | name: Publish to PyPI registry 65 | needs: 66 | - build 67 | runs-on: ubuntu-latest 68 | 69 | env: 70 | PY_COLORS: 1 71 | TOXENV: packaging 72 | 73 | steps: 74 | - name: Switch to using Python 3.6 by default 75 | uses: actions/setup-python@v2 76 | with: 77 | python-version: 3.6 78 | - name: Install tox 79 | run: python -m pip install --user tox 80 | - name: Check out src from Git 81 | uses: actions/checkout@v2 82 | with: 83 | # Get shallow Git history (default) for release events 84 | # but have a complete clone for any other workflows. 85 | # Both options fetch tags but since we're going to remove 86 | # one from HEAD in non-create-tag workflows, we need full 87 | # history for them. 88 | fetch-depth: >- 89 | ${{ 90 | ( 91 | ( 92 | github.event_name == 'create' && 93 | github.event.ref_type == 'tag' 94 | ) || 95 | github.event_name == 'release' 96 | ) && 97 | 1 || 0 98 | }} 99 | - name: Drop Git tags from HEAD for non-tag-create and non-release events 100 | if: >- 101 | ( 102 | github.event_name != 'create' || 103 | github.event.ref_type != 'tag' 104 | ) && 105 | github.event_name != 'release' 106 | run: >- 107 | git tag --points-at HEAD 108 | | 109 | xargs git tag --delete 110 | - name: Build dists 111 | run: python -m tox 112 | - name: Publish to test.pypi.org 113 | if: >- 114 | ( 115 | github.event_name == 'push' && 116 | github.ref == format( 117 | 'refs/heads/{0}', github.event.repository.default_branch 118 | ) 119 | ) || 120 | ( 121 | github.event_name == 'create' && 122 | github.event.ref_type == 'tag' 123 | ) 124 | uses: pypa/gh-action-pypi-publish@master 125 | with: 126 | password: ${{ secrets.testpypi_password }} 127 | repository_url: https://test.pypi.org/legacy/ 128 | - name: Publish to pypi.org 129 | if: >- # "create" workflows run separately from "push" & "pull_request" 130 | github.event_name == 'release' 131 | uses: pypa/gh-action-pypi-publish@master 132 | with: 133 | password: ${{ secrets.pypi_password }} 134 | -------------------------------------------------------------------------------- /.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 | 107 | # IDE stuff 108 | .vscode 109 | .idea 110 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default_language_version: 3 | python: python3 4 | minimum_pre_commit_version: "1.14.0" 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.3.0 8 | hooks: 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - id: mixed-line-ending 12 | - id: check-byte-order-marker 13 | - id: check-executables-have-shebangs 14 | - id: check-merge-conflict 15 | - id: debug-statements 16 | - repo: https://github.com/PyCQA/doc8.git 17 | rev: 0.11.2 18 | hooks: 19 | - id: doc8 20 | - repo: https://github.com/psf/black 21 | rev: 22.6.0 22 | hooks: 23 | - id: black 24 | language_version: python3 25 | - repo: https://github.com/PyCQA/flake8 26 | rev: 4.0.1 27 | hooks: 28 | - id: flake8 29 | additional_dependencies: 30 | - flake8-black 31 | - repo: https://github.com/codespell-project/codespell.git 32 | rev: v2.1.0 33 | hooks: 34 | - id: codespell 35 | name: codespell 36 | description: Checks for common misspellings in text files. 37 | entry: codespell 38 | language: python 39 | types: [text] 40 | args: [] 41 | require_serial: false 42 | additional_dependencies: [] 43 | - repo: https://github.com/adrienverge/yamllint.git 44 | rev: v1.27.1 45 | hooks: 46 | - id: yamllint 47 | files: \.(yaml|yml)$ 48 | types: [file, yaml] 49 | entry: yamllint --strict -f parsable 50 | - repo: https://github.com/openstack-dev/bashate.git 51 | rev: 2.1.0 52 | hooks: 53 | - id: bashate 54 | entry: bashate --error . --ignore=E006,E040 55 | verbose: false 56 | # Run bashate check for all bash scripts 57 | # Ignores the following rules: 58 | # E006: Line longer than 79 columns (as many scripts use jinja 59 | # templating, this is very difficult) 60 | # E040: Syntax error determined using `bash -n` (as many scripts 61 | # use jinja templating, this will often fail and the syntax 62 | # error will be discovered in execution anyway) 63 | - repo: https://github.com/ansible/ansible-lint.git 64 | rev: v6.3.0 65 | hooks: 66 | - id: ansible-lint 67 | always_run: true 68 | pass_filenames: false 69 | # do not add file filters here as ansible-lint does not give reliable 70 | # results when called with individual files. 71 | # https://github.com/ansible/ansible-lint/issues/611 72 | entry: env ANSIBLE_LIBRARY=molecule_vagrant/modules ansible-lint --force-color -p -v 73 | -------------------------------------------------------------------------------- /.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 EC2 Plugin 3 | ******************* 4 | 5 | .. image:: https://badge.fury.io/py/molecule-ec2.svg 6 | :target: https://badge.fury.io/py/molecule-ec2 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-ec2 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 EC2 is designed to allow use of AWS EC2 for provisioning of test 29 | resources. 30 | 31 | .. _quickstart: 32 | 33 | Quickstart 34 | ========== 35 | 36 | Installation 37 | ------------ 38 | .. code-block:: bash 39 | 40 | pip install molecule-ec2 41 | 42 | Create a scenario 43 | ----------------- 44 | 45 | With a new role 46 | ^^^^^^^^^^^^^^^ 47 | .. code-block:: bash 48 | 49 | molecule init role -d ec2 my-role 50 | 51 | This will create a new folder *my-role* containing a bare-bone generated 52 | role like you would do with ``ansible-galaxy init`` command. 53 | It will also contain a molecule folder with a default scenario 54 | using the ec2 driver (using ansible community.aws.ec2_instance collection). 55 | Install the collection using 56 | `ansible-galaxy install -r test_requirements.yml`. 57 | 58 | In a pre-existing role 59 | ^^^^^^^^^^^^^^^^^^^^^^ 60 | .. code-block:: bash 61 | 62 | molecule init scenario -d ec2 63 | 64 | This will create a default scenario with the ec2 driver in a molecule folder, 65 | located in the current working directory. 66 | 67 | Example 68 | ------- 69 | This is a molecule.yml example file 70 | 71 | .. code-block:: yaml 72 | 73 | dependency: 74 | name: galaxy 75 | driver: 76 | name: ec2 77 | platforms: 78 | - name: instance 79 | image_owner: "099720109477" 80 | image_name: ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-* 81 | instance_type: t2.micro 82 | vpc_subnet_id: 83 | tags: 84 | Name: molecule_instance 85 | provisioner: 86 | name: ansible 87 | verifier: 88 | name: ansible 89 | 90 | All you need to do is fill in the subnet-id you want 91 | to create your test instance into. 92 | Then run 93 | 94 | .. code-block:: bash 95 | 96 | molecule test 97 | 98 | .. note:: 99 | To make this work, you need to export your AWS credentials, as well as the AWS region you want to use, in your environment. 100 | 101 | .. code-block:: bash 102 | 103 | export AWS_ACCESS_KEY_ID=ACCESS_API_KEY 104 | export AWS_SECRET_KEY=SECRET_API_KEY 105 | export AWS_REGION=us-east-1 106 | 107 | You can read more about managing AWS credentials with Ansible modules 108 | in the official documentation of the `Ansible AWS modules `_ 109 | 110 | Documentation 111 | ============= 112 | 113 | Details on the parameters for the platforms section are detailed in 114 | ``__. 115 | 116 | Read the molecule documentation and more at https://molecule.readthedocs.io/. 117 | 118 | .. _get-involved: 119 | 120 | Get Involved 121 | ============ 122 | 123 | * Join us in the ``#ansible-molecule`` channel on `Freenode`_. 124 | * Join the discussion in `molecule-users Forum`_. 125 | * Join the community working group by checking the `wiki`_. 126 | * Want to know about releases, subscribe to `ansible-announce list`_. 127 | * For the full list of Ansible email Lists, IRC channels see the 128 | `communication page`_. 129 | 130 | .. _`Freenode`: https://freenode.net 131 | .. _`molecule-users Forum`: https://groups.google.com/forum/#!forum/molecule-users 132 | .. _`wiki`: https://github.com/ansible/community/wiki/Molecule 133 | .. _`ansible-announce list`: https://groups.google.com/group/ansible-announce 134 | .. _`communication page`: https://docs.ansible.com/ansible/latest/community/communication.html 135 | 136 | .. _authors: 137 | 138 | Authors 139 | ======= 140 | 141 | Molecule EC2 Plugin was created by Sorin Sbarnea based on code from 142 | Molecule. 143 | 144 | .. _license: 145 | 146 | License 147 | ======= 148 | 149 | The `MIT`_ License. 150 | 151 | .. _`MIT`: https://github.com/ansible/molecule/blob/master/LICENSE 152 | 153 | The logo is licensed under the `Creative Commons NoDerivatives 4.0 License`_. 154 | 155 | If you have some other use in mind, contact us. 156 | 157 | .. _`Creative Commons NoDerivatives 4.0 License`: https://creativecommons.org/licenses/by-nd/4.0/ 158 | -------------------------------------------------------------------------------- /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 | gcc [test platform:rpm] 5 | gcc-c++ [test platform:rpm] 6 | libselinux-python [platform:centos-7 !platform:fedora] 7 | python3 [test platform:rpm !platform:centos-7] 8 | python3-devel [test platform:rpm !platform:centos-7] 9 | python3-libselinux [test platform:rpm !platform:centos-7] 10 | python3 [test !platform:centos-7 platform:rpm] 11 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-ec2/08e1e57bff0d77e8100570e6d9b782171e831dc2/conftest.py -------------------------------------------------------------------------------- /molecule_ec2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-ec2/08e1e57bff0d77e8100570e6d9b782171e831dc2/molecule_ec2/__init__.py -------------------------------------------------------------------------------- /molecule_ec2/cookiecutter/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "molecule_directory": "molecule", 3 | "role_name": "OVERRIDDEN", 4 | "scenario_name": "OVERRIDDEN" 5 | } 6 | -------------------------------------------------------------------------------- /molecule_ec2/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ********************************************* 2 | Amazon Web Services driver installation guide 3 | ********************************************* 4 | 5 | Requirements 6 | ============ 7 | 8 | * An AWS credentials rc file 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ pip install 'molecule-ec2' 23 | -------------------------------------------------------------------------------- /molecule_ec2/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 | include_role: 7 | name: "{{ cookiecutter.role_name }}" 8 | -------------------------------------------------------------------------------- /molecule_ec2/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | --- 3 | - name: Create 4 | hosts: localhost 5 | connection: local 6 | gather_facts: false 7 | no_log: "{{ molecule_no_log }}" 8 | collections: 9 | - community.aws 10 | - community.crypto 11 | vars: 12 | # Run config handling 13 | default_run_id: "{{ lookup('password', '/dev/null chars=ascii_lowercase length=5') }}" 14 | default_run_config: 15 | run_id: "{{ default_run_id }}" 16 | 17 | run_config_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/run-config.yml" 18 | run_config_from_file: "{{ (lookup('file', run_config_path, errors='ignore') or '{}') | from_yaml }}" 19 | run_config: '{{ default_run_config | combine(run_config_from_file) }}' 20 | 21 | # Platform settings handling 22 | default_assign_public_ip: true 23 | default_aws_profile: "{{ lookup('env', 'AWS_PROFILE') }}" 24 | default_boot_wait_seconds: 120 25 | default_instance_type: t3a.medium 26 | default_key_inject_method: cloud-init # valid values: [cloud-init, ec2] 27 | default_key_name: "molecule-{{ run_config.run_id }}" 28 | default_private_key_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/id_rsa" 29 | default_public_key_path: "{{ default_private_key_path }}.pub" 30 | default_ssh_user: ansible 31 | default_ssh_port: 22 32 | default_user_data: '' 33 | 34 | default_security_group_name: "molecule-{{ run_config.run_id }}" 35 | default_security_group_description: Ephemeral security group for Molecule instances 36 | default_security_group_rules: 37 | - proto: tcp 38 | from_port: "{{ default_ssh_port }}" 39 | to_port: "{{ default_ssh_port }}" 40 | cidr_ip: "0.0.0.0/0" 41 | - proto: icmp 42 | from_port: 8 43 | to_port: -1 44 | cidr_ip: "0.0.0.0/0" 45 | default_security_group_rules_egress: 46 | - proto: -1 47 | from_port: 0 48 | to_port: 0 49 | cidr_ip: "0.0.0.0/0" 50 | 51 | platform_defaults: 52 | assign_public_ip: "{{ default_assign_public_ip }}" 53 | aws_profile: "{{ default_aws_profile }}" 54 | boot_wait_seconds: "{{ default_boot_wait_seconds }}" 55 | instance_type: "{{ default_instance_type }}" 56 | key_inject_method: "{{ default_key_inject_method }}" 57 | key_name: "{{ default_key_name }}" 58 | private_key_path: "{{ default_private_key_path }}" 59 | public_key_path: "{{ default_public_key_path }}" 60 | security_group_name: "{{ default_security_group_name }}" 61 | security_group_description: "{{ default_security_group_description }}" 62 | security_group_rules: "{{ default_security_group_rules }}" 63 | security_group_rules_egress: "{{ default_security_group_rules_egress }}" 64 | ssh_user: "{{ default_ssh_user }}" 65 | ssh_port: "{{ default_ssh_port }}" 66 | cloud_config: {} 67 | image: "" 68 | image_name: "" 69 | image_owner: [self] 70 | name: "" 71 | region: "" 72 | security_groups: [] 73 | tags: {} 74 | volumes: [] 75 | vpc_id: "" 76 | vpc_subnet_id: "" 77 | 78 | # Merging defaults into a list of dicts is, it turns out, not straightforward 79 | platforms: >- 80 | {{ [platform_defaults | dict2items] 81 | | product(molecule_yml.platforms | map('dict2items') | list) 82 | | map('flatten', levels=1) 83 | | list 84 | | map('items2dict') 85 | | list }} 86 | pre_tasks: 87 | - name: Validate platform configurations 88 | assert: 89 | that: 90 | - platforms | length > 0 91 | - platform.name is string and platform.name | length > 0 92 | - platform.assign_public_ip is boolean 93 | - platform.aws_profile is string 94 | - platform.boot_wait_seconds is integer and platform.boot_wait_seconds >= 0 95 | - platform.cloud_config is mapping 96 | - platform.image is string 97 | - platform.image_name is string 98 | - platform.image_owner is sequence or (platform.image_owner is string and platform.image_owner | length > 0) 99 | - platform.instance_type is string and platform.instance_type | length > 0 100 | - platform.key_inject_method is in ["cloud-init", "ec2"] 101 | - platform.key_name is string and platform.key_name | length > 0 102 | - platform.private_key_path is string and platform.private_key_path | length > 0 103 | - platform.public_key_path is string and platform.public_key_path | length > 0 104 | - platform.region is string 105 | - platform.security_group_name is string and platform.security_group_name | length > 0 106 | - platform.security_group_description is string and platform.security_group_description | length > 0 107 | - platform.security_group_rules is sequence 108 | - platform.security_group_rules_egress is sequence 109 | - platform.security_groups is sequence 110 | - platform.ssh_user is string and platform.ssh_user | length > 0 111 | - platform.ssh_port is integer and platform.ssh_port in range(1, 65536) 112 | - platform.tags is mapping 113 | - platform.volumes is sequence 114 | - platform.vpc_id is string 115 | - platform.vpc_subnet_id is string and platform.vpc_subnet_id | length > 0 116 | quiet: true 117 | loop: '{{ platforms }}' 118 | loop_control: 119 | loop_var: platform 120 | label: "{{ platform.name }}" 121 | tasks: 122 | - name: Write run config to file 123 | copy: 124 | dest: "{{ run_config_path }}" 125 | content: "{{ run_config | to_yaml }}" 126 | 127 | - name: Generate local key pairs 128 | openssh_keypair: 129 | path: "{{ item.private_key_path }}" 130 | type: rsa 131 | size: 2048 132 | regenerate: never 133 | loop: "{{ platforms }}" 134 | loop_control: 135 | label: "{{ item.name }}" 136 | register: local_keypairs 137 | 138 | - name: Look up EC2 AMI(s) by owner and name (if image not set) 139 | ec2_ami_info: 140 | owners: "{{ item.image_owner }}" 141 | filters: "{{ item.image_filters | default({}) | combine(image_name_map) }}" 142 | vars: 143 | image_name_map: "{% if item.image_name is defined and item.image_name | length > 0 %}{{ { 'name': item.image_name } }}{% else %}{}{% endif %}" 144 | loop: "{{ platforms }}" 145 | loop_control: 146 | label: "{{ item.name }}" 147 | when: not item.image 148 | register: ami_info 149 | 150 | - name: Look up subnets to determine VPCs (if needed) 151 | ec2_vpc_subnet_info: 152 | subnet_ids: "{{ item.vpc_subnet_id }}" 153 | loop: "{{ platforms }}" 154 | loop_control: 155 | label: "{{ item.name }}" 156 | when: not item.vpc_id 157 | register: subnet_info 158 | 159 | - name: Validate discovered information 160 | assert: 161 | that: 162 | - platform.image or (ami_info.results[index].images | length > 0) 163 | - platform.vpc_id or (subnet_info.results[index].subnets | length > 0) 164 | quiet: true 165 | loop: "{{ platforms }}" 166 | loop_control: 167 | loop_var: platform 168 | index_var: index 169 | label: "{{ platform.name }}" 170 | 171 | - name: Create ephemeral EC2 keys (if needed) 172 | ec2_key: 173 | profile: "{{ item.aws_profile | default(omit) }}" 174 | region: "{{ item.region | default(omit) }}" 175 | name: "{{ item.key_name }}" 176 | key_material: "{{ local_keypair.public_key }}" 177 | vars: 178 | local_keypair: "{{ local_keypairs.results[index] }}" 179 | loop: "{{ platforms }}" 180 | loop_control: 181 | index_var: index 182 | label: "{{ item.name }}" 183 | when: item.key_inject_method == "ec2" 184 | register: ec2_keys 185 | 186 | - name: Create ephemeral security groups (if needed) 187 | ec2_group: 188 | profile: "{{ item.aws_profile | default(omit) }}" 189 | region: "{{ item.region | default(omit) }}" 190 | vpc_id: "{{ item.vpc_id or vpc_subnet.vpc_id }}" 191 | name: "{{ item.security_group_name }}" 192 | description: "{{ item.security_group_description }}" 193 | rules: "{{ item.security_group_rules }}" 194 | rules_egress: "{{ item.security_group_rules_egress }}" 195 | vars: 196 | vpc_subnet: "{{ subnet_info.results[index].subnets[0] }}" 197 | loop: "{{ platforms }}" 198 | loop_control: 199 | index_var: index 200 | label: "{{ item.name }}" 201 | when: item.security_groups | length == 0 202 | 203 | - name: Create ephemeral EC2 instance(s) 204 | ec2_instance: 205 | profile: "{{ item.aws_profile | default(omit) }}" 206 | region: "{{ item.region | default(omit) }}" 207 | filters: "{{ platform_filters }}" 208 | instance_type: "{{ item.instance_type }}" 209 | image_id: "{{ platform_image_id }}" 210 | vpc_subnet_id: "{{ item.vpc_subnet_id }}" 211 | security_groups: "{{ platform_security_groups }}" 212 | network: 213 | assign_public_ip: "{{ item.assign_public_ip }}" 214 | volumes: "{{ item.volumes }}" 215 | key_name: "{{ (item.key_inject_method == 'ec2') | ternary(item.key_name, omit) }}" 216 | tags: "{{ platform_tags }}" 217 | user_data: "{{ platform_user_data }}" 218 | state: "started" 219 | wait: true 220 | vars: 221 | platform_security_groups: "{{ item.security_groups or [item.security_group_name] }}" 222 | platform_generated_image_id: "{{ (ami_info.results[index].images | sort(attribute='creation_date', reverse=True))[0].image_id }}" 223 | platform_image_id: "{{ item.image or platform_generated_image_id }}" 224 | 225 | platform_generated_cloud_config: 226 | users: 227 | - name: "{{ item.ssh_user }}" 228 | ssh_authorized_keys: 229 | - "{{ local_keypairs.results[index].public_key }}" 230 | sudo: "ALL=(ALL) NOPASSWD:ALL" 231 | platform_cloud_config: >- 232 | {{ (item.key_inject_method == 'cloud-init') 233 | | ternary((item.cloud_config | combine(platform_generated_cloud_config)), item.cloud_config) }} 234 | platform_user_data: |- 235 | #cloud-config 236 | {{ platform_cloud_config | to_yaml }} 237 | 238 | platform_generated_tags: 239 | instance: "{{ item.name }}" 240 | "molecule-run-id": "{{ run_config.run_id }}" 241 | platform_tags: "{{ (item.tags or {}) | combine(platform_generated_tags) }}" 242 | platform_filter_keys: "{{ platform_generated_tags.keys() | map('regex_replace', '^(.+)$', 'tag:\\1') }}" 243 | platform_filters: "{{ dict(platform_filter_keys | zip(platform_generated_tags.values())) }}" 244 | loop: "{{ platforms }}" 245 | loop_control: 246 | index_var: index 247 | label: "{{ item.name }}" 248 | register: ec2_instances_async 249 | async: 7200 250 | poll: 0 251 | 252 | - block: 253 | - name: Wait for instance creation to complete 254 | async_status: 255 | jid: "{{ item.ansible_job_id }}" 256 | loop: "{{ ec2_instances_async.results }}" 257 | loop_control: 258 | index_var: index 259 | label: "{{ platforms[index].name }}" 260 | register: ec2_instances 261 | until: ec2_instances is finished 262 | retries: 300 263 | 264 | - name: Collect instance configs 265 | set_fact: 266 | instance_config: 267 | instance: "{{ item.name }}" 268 | address: "{{ item.assign_public_ip | ternary(instance.public_ip_address, instance.private_ip_address) }}" 269 | user: "{{ item.ssh_user }}" 270 | port: "{{ item.ssh_port }}" 271 | identity_file: "{{ item.private_key_path }}" 272 | instance_ids: 273 | - "{{ instance.instance_id }}" 274 | vars: 275 | instance: "{{ ec2_instances.results[index].instances[0] }}" 276 | loop: "{{ platforms }}" 277 | loop_control: 278 | index_var: index 279 | label: "{{ item.name }}" 280 | register: instance_configs 281 | 282 | - name: Write Molecule instance configs 283 | copy: 284 | dest: "{{ molecule_instance_config }}" 285 | content: >- 286 | {{ instance_configs.results 287 | | map(attribute='ansible_facts.instance_config') 288 | | list 289 | | to_json 290 | | from_json 291 | | to_yaml }} 292 | 293 | - name: Start SSH pollers 294 | wait_for: 295 | host: "{{ item.address }}" 296 | port: "{{ item.port }}" 297 | search_regex: SSH 298 | delay: 10 299 | timeout: 320 300 | loop: "{{ instance_configs.results | map(attribute='ansible_facts.instance_config') | list }}" 301 | loop_control: 302 | label: "{{ item.instance }}" 303 | register: ssh_wait_async 304 | async: 300 305 | poll: 0 306 | 307 | - name: Wait for SSH 308 | async_status: 309 | jid: "{{ item.ansible_job_id }}" 310 | loop: "{{ ssh_wait_async.results }}" 311 | loop_control: 312 | index_var: index 313 | label: "{{ platforms[index].name }}" 314 | register: ssh_wait 315 | until: ssh_wait_async is finished 316 | retries: 300 317 | delay: 1 318 | 319 | - name: Wait for boot process to finish 320 | pause: 321 | seconds: "{{ platforms | map(attribute='boot_wait_seconds') | max }}" 322 | when: ec2_instances_async is changed 323 | {%- endraw %} 324 | -------------------------------------------------------------------------------- /molecule_ec2/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml: -------------------------------------------------------------------------------- 1 | {% raw -%} 2 | --- 3 | - name: Destroy 4 | hosts: localhost 5 | connection: local 6 | gather_facts: false 7 | no_log: "{{ molecule_no_log }}" 8 | collections: 9 | - community.aws 10 | vars: 11 | # Run config handling 12 | default_run_id: "{{ lookup('password', '/dev/null chars=ascii_lowercase length=5') }}" 13 | default_run_config: 14 | run_id: "{{ default_run_id }}" 15 | 16 | run_config_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/run-config.yml" 17 | run_config_from_file: "{{ (lookup('file', run_config_path, errors='ignore') or '{}') | from_yaml }}" 18 | run_config: '{{ default_run_config | combine(run_config_from_file) }}' 19 | 20 | # Platform settings handling 21 | default_aws_profile: "{{ lookup('env', 'AWS_PROFILE') }}" 22 | default_key_inject_method: cloud-init # valid values: [cloud-init, ec2] 23 | default_key_name: "molecule-{{ run_config.run_id }}" 24 | default_security_group_name: "molecule-{{ run_config.run_id }}" 25 | 26 | platform_defaults: 27 | aws_profile: "{{ default_aws_profile }}" 28 | key_inject_method: "{{ default_key_inject_method }}" 29 | key_name: "{{ default_key_name }}" 30 | region: "" 31 | security_group_name: "{{ default_security_group_name }}" 32 | security_groups: [] 33 | vpc_id: "" 34 | vpc_subnet_id: "" 35 | 36 | # Merging defaults into a list of dicts is, it turns out, not straightforward 37 | platforms: >- 38 | {{ [platform_defaults | dict2items] 39 | | product(molecule_yml.platforms | map('dict2items') | list) 40 | | map('flatten', levels=1) 41 | | list 42 | | map('items2dict') 43 | | list }} 44 | 45 | # Stored instance config 46 | instance_config: "{{ (lookup('file', molecule_instance_config, errors='ignore') or '{}') | from_yaml }}" 47 | pre_tasks: 48 | - name: Validate platform configurations 49 | assert: 50 | that: 51 | - platforms | length > 0 52 | - platform.name is string and platform.name | length > 0 53 | - platform.aws_profile is string 54 | - platform.key_inject_method is in ["cloud-init", "ec2"] 55 | - platform.key_name is string and platform.key_name | length > 0 56 | - platform.region is string 57 | - platform.security_group_name is string and platform.security_group_name | length > 0 58 | - platform.security_groups is sequence 59 | - platform.vpc_id is string 60 | - platform.vpc_subnet_id is string and platform.vpc_subnet_id | length > 0 61 | quiet: true 62 | loop: '{{ platforms }}' 63 | loop_control: 64 | loop_var: platform 65 | label: "{{ platform.name }}" 66 | tasks: 67 | - name: Look up subnets to determine VPCs (if needed) 68 | ec2_vpc_subnet_info: 69 | subnet_ids: "{{ item.vpc_subnet_id }}" 70 | loop: "{{ platforms }}" 71 | loop_control: 72 | label: "{{ item.name }}" 73 | when: not item.vpc_id 74 | register: subnet_info 75 | 76 | - name: Validate discovered information 77 | assert: 78 | that: platform.vpc_id or (subnet_info.results[index].subnets | length > 0) 79 | quiet: true 80 | loop: "{{ platforms }}" 81 | loop_control: 82 | loop_var: platform 83 | index_var: index 84 | label: "{{ platform.name }}" 85 | 86 | - name: Destroy ephemeral EC2 instances 87 | ec2_instance: 88 | profile: "{{ item.aws_profile | default(omit) }}" 89 | region: "{{ item.region | default(omit) }}" 90 | instance_ids: "{{ instance_config | map(attribute='instance_ids') | flatten }}" 91 | state: absent 92 | loop: "{{ platforms }}" 93 | loop_control: 94 | label: "{{ item.name }}" 95 | register: ec2_instances_async 96 | async: 7200 97 | poll: 0 98 | 99 | - name: Wait for instance destruction to complete 100 | async_status: 101 | jid: "{{ item.ansible_job_id }}" 102 | loop: "{{ ec2_instances_async.results }}" 103 | loop_control: 104 | index_var: index 105 | label: "{{ platforms[index].name }}" 106 | register: ec2_instances 107 | until: ec2_instances is finished 108 | retries: 300 109 | 110 | - name: Write Molecule instance configs 111 | copy: 112 | dest: "{{ molecule_instance_config }}" 113 | content: "{{ {} | to_yaml }}" 114 | 115 | - name: Destroy ephemeral security groups (if needed) 116 | ec2_group: 117 | profile: "{{ item.aws_profile | default(omit) }}" 118 | region: "{{ item.region | default(omit) }}" 119 | vpc_id: "{{ item.vpc_id or vpc_subnet.vpc_id }}" 120 | name: "{{ item.security_group_name }}" 121 | state: absent 122 | vars: 123 | vpc_subnet: "{{ subnet_info.results[index].subnets[0] }}" 124 | loop: "{{ platforms }}" 125 | loop_control: 126 | index_var: index 127 | label: "{{ item.name }}" 128 | when: item.security_groups | length == 0 129 | 130 | - name: Destroy ephemeral keys (if needed) 131 | ec2_key: 132 | profile: "{{ item.aws_profile | default(omit) }}" 133 | region: "{{ item.region | default(omit) }}" 134 | name: "{{ item.key_name }}" 135 | state: absent 136 | loop: "{{ platforms }}" 137 | loop_control: 138 | index_var: index 139 | label: "{{ item.name }}" 140 | when: item.key_inject_method == "ec2" 141 | {%- endraw %} 142 | -------------------------------------------------------------------------------- /molecule_ec2/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | {% raw -%} 3 | - name: Prepare 4 | hosts: all 5 | gather_facts: false 6 | tasks: 7 | - name: Make sure python3 is installed 8 | package: 9 | name: python3 10 | state: present 11 | become: true 12 | {%- endraw %} 13 | -------------------------------------------------------------------------------- /molecule_ec2/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 | from base64 import b64decode 22 | import os 23 | import sys 24 | 25 | try: 26 | from cryptography.hazmat.backends import default_backend 27 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 28 | from cryptography.hazmat.primitives.serialization import load_pem_private_key 29 | 30 | HAS_CRYPTOGRAPHY = True 31 | except ImportError: 32 | HAS_CRYPTOGRAPHY = False 33 | 34 | try: 35 | import boto3 36 | 37 | HAS_BOTO3 = True 38 | except ImportError: 39 | HAS_BOTO3 = False 40 | 41 | from molecule import logger 42 | from molecule.api import Driver 43 | 44 | from molecule import util 45 | 46 | LOG = logger.get_logger(__name__) 47 | 48 | 49 | class EC2(Driver): 50 | """ 51 | The class responsible for managing `EC2`_ instances. `EC2`_ 52 | is ``not`` the default driver used in Molecule. 53 | 54 | Molecule leverages Ansible's `ec2_module`_, by mapping variables from 55 | ``molecule.yml`` into ``create.yml`` and ``destroy.yml``. 56 | 57 | .. _`ec2_module`: https://docs.ansible.com/ansible/latest/ec2_module.html 58 | 59 | .. code-block:: yaml 60 | 61 | driver: 62 | name: ec2 63 | platforms: 64 | - name: instance 65 | 66 | Some configuration examples: 67 | 68 | .. code-block:: yaml 69 | 70 | driver: 71 | name: ec2 72 | platforms: 73 | - name: instance 74 | image: ami-0311dc90a352b25f4 75 | instance_type: t2.micro 76 | vpc_subnet_id: subnet-1cb17175 77 | 78 | If you don't know the AMI code or want to avoid hardcoding it: 79 | 80 | .. code-block:: yaml 81 | 82 | driver: 83 | name: ec2 84 | platforms: 85 | - name: instance 86 | image_owner: 099720109477 87 | image_name: ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-20190320 88 | instance_type: t2.micro 89 | vpc_subnet_id: subnet-1cb17175 90 | 91 | Use wildcards for getting the latest image. For example, the latest Ubuntu bionic image: 92 | 93 | .. code-block:: yaml 94 | 95 | driver: 96 | name: ec2 97 | platforms: 98 | - name: instance 99 | image_owner: 099720109477 100 | image_name: ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-* 101 | instance_type: t2.micro 102 | vpc_subnet_id: subnet-1cb17175 103 | 104 | Windows EC2 instances can be used as well: 105 | 106 | .. code-block:: yaml 107 | 108 | driver: 109 | name: ec2 110 | platforms: 111 | - name: instance 112 | image: ami-06a0d33fc8d328de0 113 | instance_type: t3a.medium 114 | vpc_subnet_id: subnet-1cb17175 115 | connection_options: 116 | sudo: False 117 | ansible_user: Administrator 118 | # Specify a password to override the automatic lookup 119 | # or omit to retrieve automatically 120 | # (requires boto3 & cryptography packages) 121 | # ansible_password: hunter2 122 | ansible_port: 5986 123 | ansible_connection: winrm 124 | ansible_winrm_scheme: https 125 | ansible_winrm_server_cert_validation: ignore 126 | 127 | .. code-block:: bash 128 | 129 | $ pip install 'molecule[ec2]' 130 | 131 | Change the options passed to the ssh client. 132 | 133 | .. code-block:: yaml 134 | 135 | driver: 136 | name: ec2 137 | ssh_connection_options: 138 | - '-o ControlPath=~/.ansible/cp/%r@%h-%p' 139 | 140 | .. important:: 141 | 142 | Molecule does not merge lists, when overriding the developer must 143 | provide all options. 144 | 145 | Provide a list of files Molecule will preserve, relative to the scenario 146 | ephemeral directory, after any ``destroy`` subcommand execution. 147 | 148 | .. code-block:: yaml 149 | 150 | driver: 151 | name: ec2 152 | safe_files: 153 | - foo 154 | 155 | .. _`EC2`: https://aws.amazon.com/ec2/ 156 | """ # noqa 157 | 158 | def __init__(self, config=None): 159 | super(EC2, self).__init__(config) 160 | self._name = "ec2" 161 | 162 | @property 163 | def name(self): 164 | return self._name 165 | 166 | @name.setter 167 | def name(self, value): 168 | self._name = value 169 | 170 | @property 171 | def login_cmd_template(self): 172 | if self._config.command_args.get("host"): 173 | hostname = self._config.command_args["host"] 174 | elif len(self._config.platforms.instances) == 1: 175 | hostname = self._config.platforms.instances[0]["name"] 176 | else: 177 | LOG.error("Please specify instance via '--host'") 178 | sys.exit(1) 179 | 180 | ansible_connection_options = self.ansible_connection_options(hostname) 181 | if ansible_connection_options.get("ansible_connection") == "winrm": 182 | return ( 183 | "xfreerdp " 184 | '"/u:%s" ' 185 | '"/p:%s" ' 186 | "/v:%s " 187 | "/cert-tofu " 188 | "+clipboard " 189 | "/grab-keyboard" 190 | % ( 191 | ansible_connection_options["ansible_user"], 192 | ansible_connection_options["ansible_password"], 193 | ansible_connection_options["ansible_host"], 194 | ) 195 | ) 196 | 197 | else: # normal ssh connection 198 | connection_options = " ".join(self.ssh_connection_options) 199 | 200 | return ( 201 | "ssh {{address}} " 202 | "-l {{user}} " 203 | "-p {{port}} " 204 | "-i {{identity_file}} " 205 | "{}" 206 | ).format(connection_options) 207 | 208 | @property 209 | def default_safe_files(self): 210 | return [self.instance_config] 211 | 212 | @property 213 | def default_ssh_connection_options(self): 214 | return self._get_ssh_connection_options() 215 | 216 | def login_options(self, instance_name): 217 | d = {"instance": instance_name} 218 | 219 | return util.merge_dicts(d, self._get_instance_config(instance_name)) 220 | 221 | def ansible_connection_options(self, instance_name): 222 | try: 223 | d = self._get_instance_config(instance_name) 224 | plat_conn_opts = next( 225 | ( 226 | item 227 | for item in self._config.config.get("platforms", []) 228 | if item["name"] == instance_name 229 | ), 230 | {}, 231 | ).get("connection_options", {}) 232 | conn_opts = util.merge_dicts( 233 | { 234 | "ansible_user": d["user"], 235 | "ansible_host": d["address"], 236 | "ansible_port": d["port"], 237 | "ansible_private_key_file": d["identity_file"], 238 | "connection": "ssh", 239 | "ansible_ssh_common_args": " ".join(self.ssh_connection_options), 240 | }, 241 | plat_conn_opts, 242 | ) 243 | if conn_opts.get("ansible_connection") == "winrm" and ( 244 | not conn_opts.get("ansible_password") 245 | ): 246 | conn_opts["ansible_password"] = self._get_windows_instance_pass( 247 | d["instance_ids"][0], d["identity_file"] 248 | ) 249 | return conn_opts 250 | except StopIteration: 251 | return {} 252 | except IOError: 253 | # Instance has yet to be provisioned , therefore the 254 | # instance_config is not on disk. 255 | return {} 256 | 257 | def _get_instance_config(self, instance_name): 258 | instance_config_dict = util.safe_load_file(self._config.driver.instance_config) 259 | 260 | return next( 261 | item for item in instance_config_dict if item["instance"] == instance_name 262 | ) 263 | 264 | def _get_windows_instance_pass(self, instance_id, key_file): 265 | if not HAS_BOTO3: 266 | LOG.error("boto3 required when using Windows instances") 267 | sys.exit(1) 268 | if not HAS_CRYPTOGRAPHY: 269 | LOG.error("cryptography package required when using Windows instances") 270 | sys.exit(1) 271 | ec2_client = boto3.client("ec2") 272 | data_response = ec2_client.get_password_data(InstanceId=instance_id) 273 | decoded = b64decode(data_response["PasswordData"]) 274 | with open(key_file, "rb") as f: 275 | key = load_pem_private_key(f.read(), None, default_backend()) 276 | return key.decrypt(decoded, PKCS1v15()).decode("utf-8") 277 | 278 | def sanity_checks(self): 279 | # FIXME(decentral1se): Implement sanity checks 280 | pass 281 | 282 | def template_dir(self): 283 | """Return path to its own cookiecutterm templates. It is used by init 284 | command in order to figure out where to load the templates from. 285 | """ 286 | return os.path.join(os.path.dirname(__file__), "cookiecutter") 287 | -------------------------------------------------------------------------------- /molecule_ec2/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-ec2/08e1e57bff0d77e8100570e6d9b782171e831dc2/molecule_ec2/test/__init__.py -------------------------------------------------------------------------------- /molecule_ec2/test/functional/.ansible-lint: -------------------------------------------------------------------------------- 1 | # ansible-lint config for functional testing, used to bypass expected metadata 2 | # errors in molecule-generated roles. Loaded via the metadata_lint_update 3 | # pytest helper. For reference, see "E7xx - metadata" in: 4 | # https://docs.ansible.com/ansible-lint/rules/default_rules.html 5 | skip_list: 6 | # metadata/701 - Role info should contain platforms 7 | - '701' 8 | # metadata/703 - Should change default metadata: " 9 | - '703' 10 | -------------------------------------------------------------------------------- /molecule_ec2/test/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-ec2/08e1e57bff0d77e8100570e6d9b782171e831dc2/molecule_ec2/test/functional/__init__.py -------------------------------------------------------------------------------- /molecule_ec2/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_ec2/test/functional/test_ec2.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 sh 25 | 26 | from molecule import logger 27 | from molecule.util import run_command 28 | from molecule.test.conftest import change_dir_to 29 | from molecule.test.functional.conftest import metadata_lint_update 30 | 31 | # import change_dir_to, temp_dir 32 | 33 | LOG = logger.get_logger(__name__) 34 | 35 | 36 | @pytest.mark.xfail(reason="need to fix template path") 37 | def test_command_init_scenario(temp_dir): 38 | role_directory = os.path.join(temp_dir.strpath, "test-init") 39 | options = {} 40 | cmd = sh.molecule.bake("init", "role", "test-init", **options) 41 | assert run_command(cmd).returncode == 0 42 | metadata_lint_update(role_directory) 43 | 44 | with change_dir_to(role_directory): 45 | molecule_directory = pytest.helpers.molecule_directory() 46 | scenario_directory = os.path.join(molecule_directory, "test-scenario") 47 | options = { 48 | "role_name": "test-init", 49 | "driver-name": "ec2", 50 | } 51 | cmd = sh.molecule.bake("init", "scenario", "test-scenario", **options) 52 | assert run_command(cmd).returncode == 0 53 | 54 | assert os.path.isdir(scenario_directory) 55 | 56 | cmd = sh.molecule.bake("test", "-s", "test-scenario") 57 | assert run_command(cmd).returncode == 0 58 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: [] 7 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: ec2 6 | platforms: 7 | - name: instance 8 | image: ami-a5b196c0 9 | instance_type: t2.micro 10 | vpc_subnet_id: subnet-6456fd1f 11 | provisioner: 12 | name: ansible 13 | playbooks: 14 | create: ../../../../../resources/playbooks/ec2/create.yml 15 | destroy: ../../../../../resources/playbooks/ec2/destroy.yml 16 | env: 17 | ANSIBLE_ROLES_PATH: ../../../../../resources/roles/ 18 | lint: 19 | name: ansible-lint 20 | scenario: 21 | name: default 22 | verifier: 23 | name: testinfra 24 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Make sure python3 is installed 7 | ansible.builtin.package: 8 | name: python3 9 | state: present 10 | become: true 11 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import testinfra.utils.ansible_runner 4 | 5 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 6 | os.environ["MOLECULE_INVENTORY_FILE"] 7 | ).get_hosts("all") 8 | 9 | 10 | # EC2 provides unique random hostnames. 11 | def test_hostname(host): 12 | pass 13 | 14 | 15 | def test_etc_molecule_directory(host): 16 | f = host.file("/etc/molecule") 17 | 18 | assert f.is_directory 19 | assert f.user == "root" 20 | assert f.group == "root" 21 | assert f.mode == 0o755 22 | 23 | 24 | def test_etc_molecule_ansible_hostname_file(host): 25 | filename = "/etc/molecule/{}".format(host.check_output("hostname -s")) 26 | f = host.file(filename) 27 | 28 | assert f.is_file 29 | assert f.user == "root" 30 | assert f.group == "root" 31 | assert f.mode == 0o644 32 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/multi-node/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: [] 7 | 8 | - name: Converge 9 | hosts: bar 10 | gather_facts: false 11 | become: true 12 | tasks: [] 13 | 14 | - name: Converge 15 | hosts: foo 16 | gather_facts: false 17 | become: true 18 | tasks: [] 19 | 20 | - name: Converge 21 | hosts: baz 22 | gather_facts: false 23 | become: true 24 | tasks: [] 25 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/multi-node/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: ec2 6 | lint: 7 | name: yamllint 8 | options: 9 | config-file: ../../../resources/.yamllint 10 | platforms: 11 | - name: instance-1 12 | image: ami-a5b196c0 13 | instance_type: t2.micro 14 | vpc_subnet_id: subnet-6456fd1f 15 | groups: 16 | - foo 17 | - bar 18 | - name: instance-2 19 | image: ami-a5b196c0 20 | instance_type: t2.micro 21 | vpc_subnet_id: subnet-6456fd1f 22 | groups: 23 | - foo 24 | - baz 25 | provisioner: 26 | name: ansible 27 | config_options: 28 | defaults: 29 | callback_whitelist: profile_roles,profile_tasks,timer 30 | playbooks: 31 | create: ../../../../../resources/playbooks/ec2/create.yml 32 | destroy: ../../../../../resources/playbooks/ec2/destroy.yml 33 | env: 34 | ANSIBLE_ROLES_PATH: ../../../../../resources/roles/ 35 | lint: 36 | name: ansible-lint 37 | scenario: 38 | name: multi-node 39 | verifier: 40 | name: testinfra 41 | lint: 42 | name: flake8 43 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/multi-node/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Make sure python3 is installed 7 | ansible.builtin.package: 8 | name: python3 9 | state: present 10 | become: true 11 | -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/multi-node/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible-community/molecule-ec2/08e1e57bff0d77e8100570e6d9b782171e831dc2/molecule_ec2/test/scenarios/driver/ec2/molecule/multi-node/tests/__init__.py -------------------------------------------------------------------------------- /molecule_ec2/test/scenarios/driver/ec2/molecule/multi-node/tests/test_default.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import testinfra.utils.ansible_runner 4 | 5 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 6 | os.environ["MOLECULE_INVENTORY_FILE"] 7 | ).get_hosts("all") 8 | 9 | 10 | # EC2 provides unique random hostnames. 11 | def test_hostname(host): 12 | pass 13 | 14 | 15 | def test_etc_molecule_directory(host): 16 | f = host.file("/etc/molecule") 17 | 18 | assert f.is_directory 19 | assert f.user == "root" 20 | assert f.group == "root" 21 | assert f.mode == 0o755 22 | 23 | 24 | def test_etc_molecule_ansible_hostname_file(host): 25 | filename = "/etc/molecule/{}".format(host.check_output("hostname -s")) 26 | f = host.file(filename) 27 | 28 | assert f.is_file 29 | assert f.user == "root" 30 | assert f.group == "root" 31 | assert f.mode == 0o644 32 | -------------------------------------------------------------------------------- /molecule_ec2/test/test_driver.py: -------------------------------------------------------------------------------- 1 | from molecule import api 2 | 3 | 4 | def test_driver_is_detected(): 5 | assert "ec2" in [str(d) for d in api.drivers()] 6 | -------------------------------------------------------------------------------- /platforms.rst: -------------------------------------------------------------------------------- 1 | ********************* 2 | Options documentation 3 | ********************* 4 | 5 | Molecule EC2 allows a wide degree of customisation via platform arguments. 6 | 7 | Environment Variables 8 | ===================== 9 | 10 | In addition to the standard molecule environment variables, the following 11 | system environment variables are used. 12 | 13 | =========================== =================================================== 14 | Variable Description 15 | =========================== =================================================== 16 | AWS_PROFILE Sets the aws_profile parameter if it is supplied 17 | =========================== =================================================== 18 | 19 | Platform Arguments 20 | ================== 21 | 22 | =========================== =================================================== 23 | Variable Description 24 | =========================== =================================================== 25 | assign_public_ip Assign a public ip, default = True 26 | aws_profile Boto profile, default = AWS_PROFILE or omits 27 | boot_wait_seconds Amount of time to wait after ssh starts 28 | cloud_config Dictionary suitable for instance user_data 29 | connection_options See Connection Options section 30 | image AMI to use, see Image Selection section 31 | image_filters Filters to select AMI, see Image Selection section 32 | image_name Name of AMI, see Image Selection section 33 | image_owner Owner of AMI, see Image Selection section 34 | instance_type AWS EC2 instance type, defaults to t3a.medium 35 | key_inject_method "cloud-init" or "ec2", see SSH Key section 36 | key_name SSH key name, see SSH Key section 37 | name Name for platform entry, used for Ansible inventory 38 | private_key_path SSH key private path, see SSH Key section 39 | public_key_path Not used 40 | region AWS region to use, defaults to AWS boto defaults 41 | security_group_name Name group to create, see Security Group section 42 | security_group_description Group description, see Security Group section 43 | security_group_rules Group ingress rules, see Security Group section 44 | security_group_rules_egress Group egress rules, see Security Group section 45 | security_groups List of security groups, see Security Group section 46 | ssh_user SSH user to use, defaults to ubuntu, \ 47 | see SSH Key section 48 | ssh_port SSH port to use 49 | tags Map of tags to apply to the instance 50 | volumes List of volumes as per aws.ec2_instance_module 51 | vpc_filters Filters to select VPC, see VPC Selection section 52 | vpc_id VPC ID, see VPC Selection section 53 | vpc_subnet_filters Filters to select Subnet,\ 54 | see Subnet Selection section 55 | vpc_subnet_id Subnet ID, see Subnet Selection section 56 | =========================== =================================================== 57 | 58 | Image Selection 59 | =============== 60 | 61 | The platform must specify an AMI for the image to use. 62 | 63 | This can be done directly, by setting the `image` parameter. 64 | 65 | Or it can be done indirectly, causing an AMI to be discovered using the 66 | `awazon.aws.ec2_ami_info` command. 67 | 68 | When using an indirect search, the `aws_profile` and `region` option will be 69 | used. 70 | The `image_owner` option will be used to select by owner if provided. 71 | The `image_name` option will be used to create a filter selecting by name. 72 | The `image_filters` option should be a dict, this will filter by key and value 73 | to select an image. 74 | 75 | If both `image_name` and `image_filters` is set, the name filter will be added 76 | to the supplied filters. 77 | 78 | When a search returns multiple images the newest creation_date will be used. 79 | 80 | Examples 81 | -------- 82 | 83 | .. code-block:: yaml 84 | 85 | platforms: 86 | - name: debian10 87 | image: ami-0f31df35880686b3f 88 | region: us-east-1 89 | - name: ubuntu1804 90 | image_owner: 099720109477 # Ubuntu 91 | image_name: ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-* 92 | - name: kali 93 | image_owner: aws-marketplace 94 | image_filters: 95 | product-code: 89bab4k3h9x4rkojcm2tj8j4l 96 | - name: RHEL8 97 | image_owner: 309956199498 # Redhat 98 | image_name: RHEL-8* 99 | image_filters: 100 | architecture: arm64 101 | virtualization-type: hvm 102 | instance_type: t4g.small # ARM64 103 | 104 | 105 | SSH Key 106 | ======= 107 | 108 | There are a variety of options which control the ssh key used for a host. 109 | 110 | The first, and most important option is `key_inject_method`. 111 | This can either be `cloud-init` or `ec2`, it defaults to `cloud-init`. 112 | 113 | The cloud-init method injects the public key in via the cloud-init user data. 114 | This creates a user, `ssh_user`, adds the public_key and grants passwordless 115 | sudo access. 116 | 117 | The ec2 method creates an AWS managed key called `key_name`. 118 | The instance is created and associated with that key, the default cloud-init 119 | setup typically adds this to the default user. 120 | For this configuration the `ssh_user` option is ignored, the user is the 121 | default for the image being used. 122 | 123 | For either option the `private_key_path` can be supplied to specify an 124 | existing key. 125 | Otherwise one is generated specifically for the creation. 126 | 127 | `public_key_path` is not actually used. 128 | 129 | Examples 130 | -------- 131 | 132 | .. code-block:: yaml 133 | 134 | platforms: 135 | - name: debian_as_ubuntu 136 | image: ami-0f31df35880686b3f 137 | region: us-east-1 138 | # Login user will be overridden from admin to ubuntu 139 | # Generated private key will be used 140 | - name: debian_as_admin 141 | image: ami-0f31df35880686b3f 142 | region: us-east-1 143 | key_inject_method: ec2 144 | connection_options: 145 | ansible_user: admin # default debian cloud user 146 | - name: debian_my_key 147 | image: ami-0f31df35880686b3f 148 | region: us-east-1 149 | key_inject_method: ec2 150 | key_name: my-key 151 | private_key_path: ~/.ssh/id_rsa 152 | # Existing private key will be used 153 | 154 | 155 | Security Group 156 | ============== 157 | 158 | Molecule EC2 can either use an existing security group or create one for the 159 | test. 160 | 161 | To use an existing security group the option `security_groups` should be a list 162 | of of existing security groups. 163 | 164 | If one or more existing groups are not specified one will be created, this is 165 | performed using the `amazon.aws.ec2_group` plugin. 166 | 167 | The `security_group_name`, `security_group_description`, 168 | `security_group_rules`, and `security_group_rules_egress` options are passed 169 | directly to `ec2_group` and are as documented there. 170 | 171 | The created security group will allow incoming ssh traffic and ICMP, all 172 | outgoing traffic will be permitted. 173 | 174 | Note that specifying `security_group_name` will cause a new security group to 175 | be created in that name, replacing any existing security group. 176 | 177 | Examples 178 | -------- 179 | 180 | .. code-block:: yaml 181 | 182 | platforms: 183 | - name: debian10 184 | image: ami-0f31df35880686b3f 185 | region: us-east-1 186 | # Default security group will be created in the first VPC 187 | - name: debian_my_sg 188 | image: ami-0f31df35880686b3f 189 | region: us-east-1 190 | security_groups: [ "public" ] 191 | # Existing public security group will be used 192 | - name: debian_specify_sg 193 | image: ami-0f31df35880686b3f 194 | region: us-east-1 195 | security_group_name: mole-whacked 196 | security_group_description: Wacking harder than we've wacked before 197 | security_group_rules: 198 | - proto: all 199 | group_name: vpn 200 | rule_desc: only allow access from the VPN 201 | security_group_rules_egress: 202 | - proto: tcp 203 | from_port: 80 204 | to_port: 80 205 | cidr_ip: "0.0.0.0/0" 206 | rule_desc: only allow old school web browsing 207 | - proto: udp 208 | from_port: 27015 209 | to_port: 27030 210 | cidr_ip: 211 | - 45.121.184.0/23 212 | - 45.121.186.0/23 213 | - 103.10.124.0/24 214 | cidr_ipv6: 215 | - 2404:3fc0::/48 216 | - 2404:3fc0:1:/48 217 | - 2404:3fc0:4:/47 218 | rule_desc: allow valve steam subset 219 | 220 | 221 | VPC Selection 222 | ============= 223 | 224 | The VPC that the EC2 image will be created in can be specified or chosen 225 | automatically. 226 | 227 | It can be specified directly by setting the `vpc_id` option. 228 | 229 | It can be specified indirectly by setting the `vpc_subnet_id` option. 230 | 231 | It can be selected by using the `vpc_filters` option, this is a dictionary 232 | which will be used by amazon.aws.ec2_vpc_net_info to select a VPC. 233 | 234 | If no VPC selection parameters are provided all vpcs will be selected. 235 | 236 | When multiple VPCs are selected the first is chosen, the order is not 237 | guaranteed. 238 | 239 | 240 | Examples 241 | -------- 242 | 243 | .. code-block:: yaml 244 | 245 | platforms: 246 | - name: first_vpc 247 | image: ami-0f31df35880686b3f 248 | region: us-east-1 249 | - name: specify_vpc 250 | image: ami-0f31df35880686b3f 251 | region: us-east-1 252 | vpc_id: vpc-3f64b58 253 | - name: specify_subnet 254 | image: ami-0f31df35880686b3f 255 | region: us-east-1 256 | vpc_subnet_id: subnet-a18bfcc6 257 | - name: filter_vpc 258 | image: ami-0f31df35880686b3f 259 | region: us-east-1 260 | vpc_filters: 261 | "tag:Name": Testground 262 | 263 | 264 | Subnet Selection 265 | ================ 266 | 267 | The subnet that the EC2 image will be created in can be specified or chosen 268 | automatically. 269 | 270 | It can be specified directly by setting the `vpc_subnet_id` option. 271 | 272 | It can be selected by using the `subnet_filters` option, this is a dictionary 273 | which will be used by amazon.aws.ec2_vpc_subnet_info to select a subnet. 274 | 275 | If no selection parameters are provided all subnets will be selected. 276 | 277 | If the `vpc_id` option is specified, it will be used to filter to that VPC, 278 | combining with `subnet_filters` if necessary. 279 | 280 | When multiple subnets are selected the first is chosen, the order is not 281 | guaranteed. 282 | 283 | Examples 284 | -------- 285 | 286 | .. code-block:: yaml 287 | 288 | platforms: 289 | - name: first_subnet 290 | image: ami-0f31df35880686b3f 291 | region: us-east-1 292 | - name: first_subnet_in_specified_vpc 293 | image: ami-0f31df35880686b3f 294 | region: us-east-1 295 | vpc_id: vpc-3f64b58 296 | - name: specify_subnet 297 | image: ami-0f31df35880686b3f 298 | region: us-east-1 299 | vpc_subnet_id: subnet-a18bfcc6 300 | - name: filter_subnet 301 | image: ami-0f31df35880686b3f 302 | region: us-east-1 303 | subnet_filters: 304 | availability-zone: us-east-1b 305 | 306 | Connection Options 307 | ================== 308 | 309 | Connection options to pass to the Ansible inventory such as `ansible_user`. 310 | 311 | Special handling is performed if the `ansible_connection` option is `winrm`. 312 | If the password is not set via the `ansible_password` option, it will be 313 | retrieved using the AWS boto3 client and set. 314 | 315 | The `ansible_connection` option being `winrm` is also used to choose between 316 | `ssh` and `xfreerdp` when using the `molecule login` command. 317 | 318 | Examples 319 | -------- 320 | 321 | .. code-block:: yaml 322 | 323 | platforms: 324 | - name: debian10 325 | image: ami-0f31df35880686b3f 326 | region: us-east-1 327 | connection_options: 328 | ansible_user: admin # default debian cloud user 329 | ansible_become: true 330 | ansible_python_interpereter: /usr/bin/python3 331 | - name: win2016 332 | image_name: Windows_Server-2016-English-Full-Base-* 333 | image_owner: amazon 334 | security_groups: [ "win" ] 335 | key_inject_method: ec2 336 | connection_options: 337 | sudo: false 338 | ansible_user: Administrator 339 | ansible_port: 5986 340 | ansible_connection: winrm 341 | ansible_winrm_scheme: https 342 | ansible_winrm_server_cert_validation: ignore 343 | connection: winrm 344 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "pip >= 19.3.1", 4 | "setuptools >= 41.4.0", 5 | "setuptools_scm >= 3.3.3", 6 | "setuptools_scm_git_archive >= 1.1", 7 | "wheel >= 0.33.6", 8 | ] 9 | build-backend = "setuptools.build_meta" 10 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -rxXs --doctest-modules --durations 10 --no-cov-on-fail --cov=molecule_* --cov-report term-missing:skip-covered 3 | doctest_optionflags = ALLOW_UNICODE ELLIPSIS 4 | junit_suite_name = molecule_test_suite 5 | norecursedirs = dist doc build .tox .eggs test/scenarios test/resources 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sh==1.14.1 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | dists = clean --all sdist bdist_wheel 3 | 4 | [metadata] 5 | name = molecule-ec2 6 | url = https://github.com/ansible-community/molecule-ec2 7 | project_urls = 8 | Bug Tracker = https://github.com/ansible-community/molecule-ec2/issues 9 | Release Management = https://github.com/ansible-community/molecule-ec2/releases 10 | Source Code = https://github.com/ansible-community/molecule-ec2 11 | description = EC2 Molecule Plugin :: run molecule tests using AWS EC2 12 | long_description = file: README.rst 13 | long_description_content_type = text/x-rst 14 | author = Sorin Sbarnea 15 | author_email = sorin.sbarnea@gmail.com 16 | maintainer = Sorin Sbarnea 17 | maintainer_email = sorin.sbarnea@gmail.com 18 | license = MIT 19 | license_file = LICENSE 20 | classifiers = 21 | Development Status :: 5 - Production/Stable 22 | Environment :: Console 23 | Framework :: Pytest 24 | Intended Audience :: Developers 25 | Intended Audience :: Information Technology 26 | Intended Audience :: System Administrators 27 | License :: OSI Approved :: MIT License 28 | Natural Language :: English 29 | Operating System :: OS Independent 30 | Programming Language :: Python :: 3 31 | Programming Language :: Python :: 3.8 32 | Programming Language :: Python :: 3.9 33 | Programming Language :: Python :: 3.10 34 | Topic :: System :: Systems Administration 35 | Topic :: Utilities 36 | 37 | keywords = 38 | ansible 39 | roles 40 | testing 41 | molecule 42 | plugin 43 | ec2 44 | aws 45 | boto 46 | 47 | [options] 48 | use_scm_version = True 49 | python_requires = >=3.6 50 | packages = find: 51 | include_package_data = True 52 | zip_safe = False 53 | 54 | # These are required during `setup.py` run: 55 | setup_requires = 56 | setuptools_scm >= 1.15.0 57 | setuptools_scm_git_archive >= 1.0 58 | 59 | # These are required in actual runtime: 60 | install_requires = 61 | # do not use ceiling unless you already know that newer version breaks 62 | # do not use pre-release versions 63 | molecule >= 3.2 64 | pyyaml >= 5.1 65 | boto3; platform_system == "Windows" 66 | 67 | [options.extras_require] 68 | test = 69 | molecule[test] 70 | 71 | [options.entry_points] 72 | molecule.driver = 73 | ec2 = molecule_ec2.driver:EC2 74 | 75 | [options.packages.find] 76 | where = . 77 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | import setuptools 3 | 4 | 5 | if __name__ == "__main__": 6 | setuptools.setup(use_scm_version=True) 7 | -------------------------------------------------------------------------------- /test_requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: [] 3 | collections: 4 | - community.aws 5 | -------------------------------------------------------------------------------- /tools/test-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | # Used by Zuul CI to perform extra bootstrapping 4 | 5 | # Platforms coverage: 6 | # Fedora 30 : has vagrant-libvirt no compilation needed 7 | # CentOS 7 : install upstream vagrant rpm and compiles plugin (broken runtime) 8 | # CentOS 8 : install upstream vagrant rpm and compiles plugin (broken runtime) 9 | 10 | 11 | # Bumping system tox because version from CentOS 7 is too old 12 | # We are not using pip --user due to few bugs in tox role which does not allow 13 | # us to override how is called. Once these are addressed we will switch back 14 | # non-sudo 15 | command -v python3 python 16 | 17 | PYTHON=$(command -v python3 python|head -n1) 18 | 19 | sudo $PYTHON -m pip install -U tox "zipp<0.6.0;python_version=='2.7'" 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # For more information about tox, see https://tox.readthedocs.io/en/latest/ 2 | [tox] 3 | minversion = 3.9.0 4 | envlist = 5 | lint 6 | 7 | packaging 8 | py{36,37,38,39} 9 | py{38,39}-{devel} 10 | 11 | # do not enable skip missing to avoid CI false positives 12 | skip_missing_interpreters = False 13 | isolated_build = True 14 | 15 | [testenv] 16 | description = 17 | Unit testing 18 | usedevelop = True 19 | extras = test 20 | commands = 21 | pytest --collect-only 22 | pytest --color=yes {tty:-s} 23 | deps = 24 | py{36,37,38,39}: molecule[test] 25 | py{36,37,38,39}-{devel}: git+https://github.com/ansible-community/molecule.git@main#egg=molecule[test] 26 | -rrequirements.txt 27 | setenv = 28 | ANSIBLE_FORCE_COLOR={env:ANSIBLE_FORCE_COLOR:1} 29 | ANSIBLE_INVENTORY={toxinidir}/tests/hosts.ini 30 | ANSIBLE_CONFIG={toxinidir}/ansible.cfg 31 | ANSIBLE_NOCOWS=1 32 | ANSIBLE_RETRY_FILES_ENABLED=0 33 | ANSIBLE_STDOUT_CALLBACK={env:ANSIBLE_STDOUT_CALLBACK:debug} 34 | ANSIBLE_VERBOSITY={env:ANSIBLE_VERBOSITY:0} 35 | PIP_DISABLE_PIP_VERSION_CHECK=1 36 | PY_COLORS={env:PY_COLORS:1} 37 | # pip: Avoid 2020-01-01 warnings: https://github.com/pypa/pip/issues/6207 38 | PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command 39 | PYTHONDONTWRITEBYTECODE=1 40 | # This should pass these args to molecule, no effect here as this is the default 41 | # but it validates that it accepts extra params. 42 | MOLECULE_OPTS=--destroy always 43 | passenv = 44 | CI 45 | CURL_CA_BUNDLE 46 | DOCKER_* 47 | PYTEST_OPTIONS 48 | REQUESTS_CA_BUNDLE 49 | SSH_AUTH_SOCK 50 | SSL_CERT_FILE 51 | TOXENV 52 | TWINE_* 53 | whitelist_externals = 54 | bash 55 | twine 56 | pytest 57 | pre-commit 58 | rm 59 | 60 | [testenv:lint] 61 | description = Performs linting, style checks, metadata-validation, packaging 62 | skip_install = true 63 | deps = 64 | pre-commit 65 | commands = 66 | pre-commit run -a 67 | 68 | [testenv:packaging] 69 | description = 70 | Validate that we can install the wheel w/ or w/o extras on Ubuntu, Debian, 71 | Fedora, RHEL 8 and CentOS 7 by using containers. 72 | deps = 73 | collective.checkdocs >= 0.2 74 | pep517 >= 0.5.0 75 | twine >= 2.0.0 76 | commands = 77 | bash -c "rm -rf {toxinidir}/dist/ && mkdir -p {toxinidir}/dist/" 78 | python -m pep517.build \ 79 | --source \ 80 | --binary \ 81 | --out-dir {toxinidir}/dist/ {toxinidir} 82 | twine check dist/* 83 | 84 | [testenv:devel] 85 | description= Unit testing using main branches of molecule and ansible 86 | extras = test 87 | commands = 88 | {[testenv]commands} 89 | 90 | [testenv:upload] 91 | description = Builds the packages and uploads them to https://pypi.org 92 | envdir={toxworkdir}/packaging 93 | deps= 94 | {[testenv:packaging]deps} 95 | commands = 96 | {[testenv:packaging]commands} 97 | twine upload --disable-progress-bar --skip-existing --verbose dist/* 98 | --------------------------------------------------------------------------------