├── .ansible-lint ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml-disabled ├── LICENSE ├── README.md ├── defaults └── main.yml ├── docs └── molecule │ ├── default │ ├── converge.yml │ ├── create.yml │ ├── destroy.yml │ ├── molecule.yml │ ├── requirements.yml │ └── vars.yml │ └── libvirt │ ├── converge.yml │ ├── create.yml │ ├── destroy.yml │ ├── molecule.yml │ ├── requirements.yml │ └── vars.yml ├── files └── docker │ ├── Dockerfile_CentOS │ ├── Dockerfile_Debian │ ├── Dockerfile_Fedora │ ├── Dockerfile_Ubuntu │ └── entrypoint.sh ├── meta └── main.yml ├── molecule └── default │ ├── cleanup.yml │ ├── converge.yml │ ├── create.yml │ ├── destroy.yml │ ├── molecule.yml │ ├── prepare.yml │ ├── requirements.yml │ └── vars.yml ├── renovate.json ├── requirements-ansible_14.txt ├── requirements-ansible_15.txt ├── requirements-ansible_16.txt ├── requirements-ansible_latest.txt ├── requirements.yml ├── tasks ├── create.yml ├── create_container.yml ├── create_libvirt.yml ├── create_virtual_machine.yml ├── dependency.yml ├── destroy.yml ├── destroy_container.yml ├── destroy_libvirt.yml ├── destroy_virtual_machine.yml └── main.yml ├── templates ├── AutoUnattend.xml ├── autorun.bat ├── instance.xml └── setup-winrm.ps1 └── tests ├── create.yml ├── create_vm.yml ├── destroy.yml ├── destroy_vm.yml ├── provision.yml └── virtualmachine.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - docs 3 | - molecule 4 | - tests 5 | - .github 6 | skip_list: 7 | - no-handler 8 | - yaml[line-length] 9 | - yaml[empty-lines] 10 | - jinja[spacing] 11 | - command-instead-of-shell 12 | - template-instead-of-copy # copy with 'content' that contains variables 13 | - name[template] # variables in task names that are not at the end 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Molecule 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | # monthly on 30th, 03:15 12 | - cron: "15 3 30 * *" 13 | 14 | jobs: 15 | molecule: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | path: "${{ github.repository }}" 22 | - name: Check docker config 23 | run: cat /etc/docker/daemon.json || echo; docker system info; docker version 24 | - name: Check ansible 25 | run: ansible --version 26 | - name: Test 27 | uses: gofrolist/molecule-action@v2 28 | with: 29 | # molecule_options: --debug -v 30 | molecule_working_dir: "${{ github.repository }}" 31 | molecule_command: test 32 | env: 33 | ANSIBLE_FORCE_COLOR: "1" 34 | ANSIBLE_STDOUT_CALLBACK: "debug" 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv 2 | /ansible.cfg 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # run: pre-commit 2 | repos: 3 | - repo: https://github.com/ansible/ansible-lint.git 4 | rev: v6.3.0 5 | hooks: 6 | - id: ansible-lint 7 | files: \.(yaml|yml)$ 8 | exclude: .travis.yml 9 | -------------------------------------------------------------------------------- /.travis.yml-disabled: -------------------------------------------------------------------------------- 1 | --- 2 | os: linux 3 | dist: focal 4 | language: python 5 | python: 6 | - "3.9" 7 | cache: pip 8 | services: 9 | - docker 10 | 11 | install: 12 | - pip3 install -U ansible ansible-lint jq docker molecule molecule-docker 13 | 14 | before_script: 15 | - molecule lint 16 | 17 | script: 18 | - molecule test 19 | 20 | notifications: 21 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 DrPsychick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible testing 2 | [![Build Status](https://img.shields.io/github/actions/workflow/status/DrPsychick/ansible-testing/ci.yml 3 | )](https://github.com/DrPsychick/ansible-testing/actions/workflows/ci.yml) 4 | [![license](https://img.shields.io/github/license/drpsychick/ansible-testing.svg)](https://github.com/drpsychick/ansible-testing/blob/main/LICENSE) 5 | [![Paypal](https://img.shields.io/badge/donate-paypal-00457c.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FTXDN7LCDWUEA&source=url) 6 | [![GitHub Sponsor](https://img.shields.io/badge/github-sponsor-blue?logo=github)](https://github.com/sponsors/DrPsychick) 7 | 8 | Creates fully functional SystemD docker containers which you can use to test your Ansible roles with. 9 | With `libvirt` you can also automatically provision Windows virtual machines to test your roles with `WinRM`. 10 | Intended to be run locally on a Linux box with Docker (and optionally libvirt) installed. 11 | 12 | ## Configuration 13 | Check [defaults/main.yml](defaults/main.yml) see how to define containers and adjust it to your needs. 14 | * Define your own `work_dir`. The contents are temporary and will be removed with `destroy`! 15 | * Define the containers you want to spin up. 16 | 17 | ## What it does (in a nutshell) 18 | This role has `create` and `destroy` tasks for containers and virtual machines. It loops over the configured lists 19 | (`containers` or `virtual_machines` in `vars.yml`) and creates the instances accordingly. 20 | Each Molecule scenario must either use containers **or** virtual machines, because Molecule only supports a single driver per scenario. 21 | 22 | Linux: It creates privileged docker containers that are started with `/sbin/init` to have a fully SystemD capable instance. 23 | 24 | Windows: It creates the VM (unattended install of Windows) and configures WinRM for Ansible (plain HTTP) upon first start. 25 | 26 | **Hint:** Creating Windows VMs is expensive and takes over 10 Minutes. 27 | 28 | # Contributing 29 | If you have other systems you want to test, feel free to provide a PR with additional Dockerfiles or libvirt configuration. 30 | 31 | Contributing is really easy: 32 | 1. Fork the project on GitHub : https://github.com/DrPsychick/ansible-testing 33 | 1. Checkout the fork on your Linux box 34 | 1. Symlink the fork in your roles Molecule scenario (i.e. `./molecule/default/`) 35 | 1. Make changes and test your role with them until you're happy - commit and create a pull-request 36 | 1. You can run GitHub Actions locally for fast feedback with `act`: https://nektosact.com/installation/index.html 37 | 38 | ```shell 39 | GitHubName=YourName 40 | YourRoleDir=/This/Is/Your/Role/Directory/MyRole 41 | YourRoleName=MyRole 42 | WhereYourForkIs=/This/Is/Where/You/Clone/Your/Fork 43 | 44 | # clone your fork 45 | cd $WhereYourForkIs 46 | git clone https://github.com/$GitHubName/ansible-testing.git 47 | 48 | # symlink your local version in your molecule scenario 49 | cd $YourRoleDir/molecule/default 50 | ln -s $WhereYourForkIs/ansible-testing drpsychick.ansible_testing 51 | 52 | # comment out role in requirements and delete downloaded version 53 | sed -i -e 's/^ /# /' requirements.yml 54 | rm -rf ~/.cache/molecule/$YourRoleName/default/roles/drpsychick.ansible_testing 55 | ``` 56 | 57 | Now, when you run `molecule` it will use the symlink to include the `drpsychick.ansible_testing` role. 58 | Make your changes, commit regularly and when you're done, don't forget to create a pull-request so others can benefit 59 | from your improvements as well. What's more is that you can test the role itself with Molecule: 60 | just execute `molecule test` in your local fork directory. 61 | 62 | # Usage 63 | Requirements: 64 | * Linux (as it spawns containers in `privileged` mode and binds `/sys/fs/cgroup`) 65 | * Docker 66 | * libvirt (for Windows virtual machines) 67 | 68 | # Test with Docker containers (Linux) 69 | Requirements 70 | * `pip3 install -U molecule molecule-docker` 71 | 72 | ## With Ansible `molecule` 73 | Create a new role with `molecule init role ` or initialize the Molecule scenario in an existing role directory with 74 | `molecule init scenario default`. 75 | 76 | Download the example files from this repo which make use of this role (in `create` and `destroy`): 77 | ```shell 78 | for f in create destroy molecule requirements vars; do 79 | curl -o molecule/default/$f.yml https://raw.githubusercontent.com/DrPsychick/ansible-testing/main/docs/molecule/default/$f.yml 80 | done 81 | ``` 82 | 83 | Adjust the `molecule/default/vars.yml` to define which containers to provision. 84 | Then adjust the `platforms` in `molecule/default/molecule.yml` accordingly. 85 | 86 | `vars.yml` 87 | ```yaml 88 | work_dir: "/tmp/ansible-testrole-default" 89 | containers: 90 | - { name: fedora40, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 40 } } 91 | - { name: ubuntu2404, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 24.04 } } 92 | - { name: centos7, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 7 } } 93 | ``` 94 | 95 | `molecule.yml` 96 | ```yaml 97 | [...] 98 | platforms: 99 | - name: fedora40 100 | - name: ubuntu2404 101 | - name: centos7 102 | [...] 103 | ``` 104 | 105 | Run molecule 106 | ```shell 107 | # steps separately 108 | molecule dependency 109 | molecule create 110 | molecule prepare 111 | molecule converge 112 | molecule idempotence 113 | molecule verify 114 | molecule cleanup 115 | molecule destroy 116 | 117 | # or everything in one go 118 | molecule test 119 | ``` 120 | 121 | ## Standalone 122 | Write your own playbook or use the playbooks in `tests` 123 | ```shell 124 | ansible-galaxy install -r requirements.yml 125 | 126 | # edit tests/provision.yml to your needs 127 | echo "[defaults] 128 | roles_path = .." > ansible.cfg 129 | 130 | # create containers 131 | ansible-playbook tests/create.yml 132 | 133 | # destroy containers 134 | ansible-playbook tests/destroy.yml 135 | ``` 136 | 137 | 138 | # Test with Windows VMs 139 | Requirements 140 | * install `libvirt`, `libvirt-clients`, `virtinst` 141 | * ansible-galaxy `community.libvirt` 142 | * for Ansible to connect with WinRM: `python3-winrm` 143 | * see [defaults/main.yml](defaults/main.yml) 144 | * Download the Windows Image of your choice (Test is setup for Windows 2016) 145 | * Download the VirtIO ISO 146 | * Put both ISOs into the `libvirt_iso_dir` 147 | 148 | ```shell 149 | # download the ISOs 150 | sudo curl -Lo /var/lib/libvirt/isos/WindowsServer2016.iso http://care.dlservice.microsoft.com/dl/download/1/6/F/16FA20E6-4662-482A-920B-1A45CF5AAE3C/14393.0.160715-1616.RS1_RELEASE_SERVER_EVAL_X64FRE_EN-US.ISO 151 | sudo curl -Lo /var/lib/libvirt/isos/virtio-win.iso https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso 152 | ``` 153 | 154 | ## Use libvirt as user (no become/sudo) 155 | Create `image` and `iso` pool that is writeable by the user and set the permissions of the pool accordingly. 156 | Make sure the user is part of 157 | ```shell 158 | sudo virsh pool-create-as myisos dir --target /mydir/libvirt/isos 159 | sudo virsh pool-edit isos # set permissions 160 | 161 | sudo virsh pool-create-as myimages dir --target /mydir/libvirt/images 162 | sudo virsh pool-edit images2 # set permissions 163 | ``` 164 | 165 | Reference the directories and pool in your `molecule/libvirt/vars.yml` 166 | ```shell 167 | libvirt_image_dir: "/mydir/libvirt/images" 168 | libvirt_iso_dir: "/mydir/libvirt/isos" 169 | libvirt_disk_pool: "myimages" 170 | ``` 171 | 172 | 173 | ## With Ansible `molecule` 174 | Create a new role with `molecule init role ` or initialize the Molecule scenario in an existing role directory with 175 | `molecule init scenario default`. 176 | 177 | Download the example files from this repo which make use of this role (in `create` and `destroy`): 178 | ```shell 179 | for f in create destroy molecule requirements vars; do 180 | curl -o molecule/libvirt/$f.yml https://raw.githubusercontent.com/DrPsychick/ansible-testing/main/docs/molecule/libvirt/$f.yml 181 | done 182 | ``` 183 | 184 | Adjust the `molecule/libvirt/vars.yml` to define which containers to provision. 185 | Then adjust the `platforms` in `molecule/libvirt/molecule.yml` accordingly. 186 | 187 | ```shell 188 | # run the scenario "libvirt" 189 | molecule test -s libvirt 190 | ``` 191 | 192 | ## Standalone 193 | ```shell 194 | ansible-galaxy install -r requirements.yml 195 | 196 | # edit tests/provision.yml to your needs 197 | echo "[defaults] 198 | roles_path = .." > ansible.cfg 199 | 200 | # create virtual machine 201 | ansible-playbook tests/create_vm.yml 202 | 203 | # destroy virtual machine 204 | ansible-playbook tests/destroy_vm.yml 205 | ``` 206 | 207 | ## Using predefined Windows images to speed up provisioning 208 | A full spin-up (create) run for 2 Windows instances with predefined images took **less than 4 minutes** on my i7. 209 | 1. Create a `qcow2` image or simply provision a VM once with unattended install 210 | 2. Create a zip from the ready to use VM: `zip windows2016-clean.qcow2.zip windows2016.qcow2` 211 | (the filename must match and be in the root of the zip file - with no path) 212 | 3. Move the zip file to `libvirt_iso_dir` or provide it via URL (`disk_image_url`) 213 | 214 | # Test locally with different Ansible versions 215 | 216 | Requires `python3-venv` 217 | 218 | ```shell 219 | # version 16 and latest fail with 220 | # ERROR! Unexpected Exception, this is probably a bug: cannot import name 'should_retry_error' from 'ansible.galaxy.api' 221 | ANSIBLE_VERSION=15 222 | python3 -m venv .venv 223 | . .venv/bin/activate 224 | pip3 install --upgrade pip setuptools wheel 225 | pip3 install --requirement requirements-ansible_${ANSIBLE_VERSION}.txt 226 | 227 | molecule test 228 | ``` 229 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | work_dir: /tmp/ansible-testing 3 | ssh_keyfile: "ansible-id_rsa" 4 | ssh_keyname: "ansible@local" 5 | # defining the containers you want to create 6 | containers: [] 7 | # - { name: fedora33, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 33 } } 8 | # - { name: fedora40, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 40 } } 9 | # - { name: ubuntu2004, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 20.04 } } 10 | # - { name: ubuntu2404, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 24.04 } } 11 | # - { name: centos7, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 7 } } 12 | # - { name: centos8, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 8 } } 13 | 14 | libvirt_upgrade_powershell_url: "https://raw.githubusercontent.com/jborean93/ansible-windows/master/scripts/Upgrade-PowerShell.ps1" 15 | libvirt_winrm_hotfix_url: "https://raw.githubusercontent.com/jborean93/ansible-windows/master/scripts/Install-WMF3Hotfix.ps1" 16 | # if you change this, make sure the image directory is defined as storage pool in libvirt 17 | # see `virsh pool-dumpxml images` 18 | libvirt_image_dir: "/var/lib/libvirt/images" 19 | libvirt_iso_dir: "/var/lib/libvirt/isos" 20 | libvirt_disk_pool: "default" 21 | 22 | libvirt_defaults: 23 | windows: 24 | cpu: 2 25 | memory: 4194304 26 | 27 | virtual_machines: [] 28 | # Unattended install (requires ISOs to be downloaded, drivers to be loaded etc.) 29 | # - name: "windows2016" 30 | # #uuid: "c3cedf53-bf5b-4271-881c-ab6380ea1847" # default is generated 31 | # #hostname: "windows2016" # default to name 32 | # password: "Ecoo6pev" 33 | # os: "windows" 34 | # isos: 35 | # # 2016 : http://care.dlservice.microsoft.com/dl/download/1/6/F/16FA20E6-4662-482A-920B-1A45CF5AAE3C/14393.0.160715-1616.RS1_RELEASE_SERVER_EVAL_X64FRE_EN-US.ISO # noqa 204 36 | # # 2019 : https://software-download.microsoft.com/download/pr/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso 37 | # # 2022 : https://software-download.microsoft.com/download/sg/20348.169.210806-2348.fe_release_svc_refresh_SERVER_EVAL_x64FRE_en-us.iso 38 | # - name: "WindowsServer2016.iso" 39 | # dev: sda 40 | # # https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso 41 | # - name: "virtio-win.iso" 42 | # dev: sdb 43 | # - name: "scripts-windows2016.iso" 44 | # dev: sdc 45 | # # disk: "windows2016.qcow2" default 46 | # disk_size: 20G 47 | # # optionally download or use a prepared image to boot from 48 | # # a zip file that only contains the image in the root ($name.qcow2) 49 | # disk_image_url: "https://yourdownload.server/windows2016-clean.qcow2.zip" 50 | # disk_image: "windows2016-clean.qcow2.zip" 51 | # image_index: 2 52 | # # image_name: "Windows Server 2016 Standard Evaluation (Desktop Experience)" # did not work? 53 | # drivers: 54 | # - 'E:\amd64\2k16' 55 | # - 'E:\NetKVM\2k16\amd64' 56 | # - 'E:\Balloon\2k16\amd64' 57 | # mac: "02:00:00:00:13:37" 58 | # vnc_port: 56682 59 | # Create a VM from a predefined image which has WinRM already setup, speeds up instance creation significantly 60 | # You can simply use the result of an unattended install (zip windows2019-clean.qcow2.zip windows2019.qcow2) 61 | # - name: "windows2019" 62 | # password: "Ecoo6pev" 63 | # os: "windows" 64 | # # disk_image_url: provide a URL to download 65 | # disk_image: "windows2019-clean.qcow2.zip" 66 | # mac: "02:00:00:00:13:38" 67 | # vnc_port: 56683 68 | -------------------------------------------------------------------------------- /docs/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | tasks: 5 | - name: "Include myrole" 6 | ansible.builtin.include_role: 7 | name: "myrole" 8 | -------------------------------------------------------------------------------- /docs/molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | no_log: "{{ molecule_no_log }}" 6 | vars_files: 7 | - vars.yml 8 | tasks: 9 | 10 | - name: Provision 11 | import_role: 12 | name: drpsychick.ansible_testing 13 | vars: 14 | provision_action: 'create' 15 | 16 | - block: 17 | - name: Populate instance config dict 18 | ansible.builtin.set_fact: 19 | instance_conf_dict: { 20 | 'instance': "{{ item.name }}", 21 | } 22 | with_items: "{{ container_inventory }}" 23 | register: instance_config_dict 24 | 25 | - name: Convert instance config dict to a list 26 | ansible.builtin.set_fact: 27 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 28 | 29 | - name: Dump instance config 30 | ansible.builtin.copy: 31 | content: | 32 | # Molecule managed 33 | 34 | {{ instance_conf | to_json | from_json | to_yaml }} 35 | dest: "{{ molecule_instance_config }}" 36 | mode: "0600" 37 | -------------------------------------------------------------------------------- /docs/molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | no_log: "{{ molecule_no_log }}" 6 | vars_files: 7 | - vars.yml 8 | tasks: 9 | 10 | - name: Deprovision 11 | import_role: 12 | name: drpsychick.ansible_testing 13 | vars: 14 | provision_action: 'destroy' 15 | 16 | - name: Populate instance config 17 | ansible.builtin.set_fact: 18 | instance_conf: {} 19 | 20 | - name: Dump instance config 21 | ansible.builtin.copy: 22 | content: | 23 | # Molecule managed 24 | 25 | {{ instance_conf | to_json | from_json | to_yaml }} 26 | dest: "{{ molecule_instance_config }}" 27 | mode: "0600" 28 | when: server.changed | default(false) | bool -------------------------------------------------------------------------------- /docs/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: fedora40 10 | - name: ubuntu2404 11 | - name: centos8 12 | provisioner: 13 | name: ansible 14 | verifier: 15 | name: ansible -------------------------------------------------------------------------------- /docs/molecule/default/requirements.yml: -------------------------------------------------------------------------------- 1 | roles: 2 | - drpsychick.ansible_testing 3 | -------------------------------------------------------------------------------- /docs/molecule/default/vars.yml: -------------------------------------------------------------------------------- 1 | work_dir: "/tmp/ansible-testhost" 2 | containers: 3 | # - { name: fedora33, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 33 } } 4 | - { name: fedora40, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 40 } } 5 | # - { name: ubuntu2004, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 20.04 } } 6 | - { name: ubuntu2404, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 24.04 } } 7 | # - { name: centos7, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 7 } } 8 | - { name: centos8, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 8 } } 9 | -------------------------------------------------------------------------------- /docs/molecule/libvirt/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | tasks: 5 | - name: "Include myrole" 6 | ansible.builtin.include_role: 7 | name: "myrole" 8 | -------------------------------------------------------------------------------- /docs/molecule/libvirt/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | no_log: "{{ molecule_no_log }}" 6 | vars_files: 7 | - vars.yml 8 | tasks: 9 | 10 | - name: Provision 11 | import_role: 12 | name: drpsychick.ansible_testing 13 | vars: 14 | provision_action: 'create' 15 | 16 | - block: 17 | - name: Populate instance config dict 18 | ansible.builtin.set_fact: 19 | instance_conf_dict: { 20 | 'instance': "{{ virtual_machine_inventory[item].name }}", 21 | 'address': "{{ virtual_machine_inventory[item].ip }}", 22 | 'connection': 'winrm', 23 | 'winrm_transport': 'basic', 24 | 'user': 'Administrator', 25 | 'password': '{{ virtual_machine_inventory[item].password }}', 26 | 'port': 5985, 27 | } 28 | with_items: "{{ virtual_machine_inventory }}" 29 | register: instance_config_dict 30 | 31 | - name: Convert instance config dict to a list 32 | ansible.builtin.set_fact: 33 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 34 | 35 | - name: Dump instance config 36 | ansible.builtin.copy: 37 | content: | 38 | # Molecule managed 39 | 40 | {{ instance_conf | to_json | from_json | to_yaml }} 41 | dest: "{{ molecule_instance_config }}" 42 | mode: "0600" 43 | -------------------------------------------------------------------------------- /docs/molecule/libvirt/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | no_log: "{{ molecule_no_log }}" 6 | vars_files: 7 | - vars.yml 8 | tasks: 9 | 10 | - name: Deprovision 11 | import_role: 12 | name: drpsychick.ansible_testing 13 | vars: 14 | provision_action: 'destroy' 15 | 16 | - name: Populate instance config 17 | ansible.builtin.set_fact: 18 | instance_conf: {} 19 | 20 | - name: Dump instance config 21 | ansible.builtin.copy: 22 | content: | 23 | # Molecule managed 24 | 25 | {{ instance_conf | to_json | from_json | to_yaml }} 26 | dest: "{{ molecule_instance_config }}" 27 | mode: "0600" 28 | when: server.changed | default(false) | bool -------------------------------------------------------------------------------- /docs/molecule/libvirt/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: default 8 | platforms: 9 | - name: windows2016 10 | provisioner: 11 | name: ansible 12 | verifier: 13 | name: ansible -------------------------------------------------------------------------------- /docs/molecule/libvirt/requirements.yml: -------------------------------------------------------------------------------- 1 | roles: 2 | - drpsychick.ansible_testing 3 | -------------------------------------------------------------------------------- /docs/molecule/libvirt/vars.yml: -------------------------------------------------------------------------------- 1 | work_dir: "/tmp/ansible-testhost" 2 | containers: [] 3 | virtual_machines: 4 | - name: "windows2016" 5 | password: "Ecoo6pev" 6 | os: "windows" 7 | isos: 8 | - name: "WindowsServer2016.iso" 9 | dev: sda 10 | - name: "virtio-win.iso" 11 | dev: sdb 12 | - name: "scripts-windows2016.iso" 13 | dev: sdc 14 | disk_size: 20G 15 | image_index: 2 16 | drivers: 17 | - 'E:\amd64\2k16' 18 | - 'E:\NetKVM\2k16\amd64' 19 | - 'E:\Balloon\2k16\amd64' 20 | mac: "02:00:00:00:13:37" 21 | vnc_port: 56682 -------------------------------------------------------------------------------- /files/docker/Dockerfile_CentOS: -------------------------------------------------------------------------------- 1 | ARG VERSION=8 2 | FROM centos:$VERSION 3 | 4 | ENV container=docker 5 | 6 | STOPSIGNAL SIGRTMIN+3 7 | 8 | # fix centos 8 mirrors 9 | ARG VERSION 10 | RUN if [ $VERSION -eq 8 ]; then \ 11 | cat /etc/yum.repos.d/CentOS-Linux-AppStream.repo; \ 12 | sed -i'' -e 's#mirrorlist=http://mirrorlist.centos.org/\(.*\)#\#mirrorslist=http://vault.centos.org/\1#' /etc/yum.repos.d/CentOS-Linux-{BaseOS,AppStream,Extras}.repo; \ 13 | sed -i'' -e 's#\#baseurl=http://mirror.centos.org/\(.*\)#baseurl=http://vault.centos.org/\1#' /etc/yum.repos.d/CentOS-Linux-{BaseOS,AppStream,Extras}.repo; \ 14 | fi 15 | 16 | RUN yum -y update \ 17 | && yum -y install systemd systemd-libs openssh-server sudo \ 18 | && yum clean all 19 | #RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \ 20 | # rm -f /lib/systemd/system/multi-user.target.wants/*;\ 21 | # rm -f /etc/systemd/system/*.wants/*;\ 22 | # rm -f /lib/systemd/system/local-fs.target.wants/*; \ 23 | # rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ 24 | # rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ 25 | # rm -f /lib/systemd/system/basic.target.wants/*;\ 26 | # rm -f /lib/systemd/system/anaconda.target.wants/*; 27 | 28 | # was not needed 29 | #RUN systemctl set-default multi-user.target && \ 30 | # systemctl mask dev-hugepages.mount sys-fs-fuse-connections.mount 31 | 32 | COPY entrypoint.sh / 33 | RUN chmod +x /entrypoint.sh 34 | 35 | VOLUME /sys/fs/cgroup 36 | ENTRYPOINT ["/entrypoint.sh"] 37 | CMD ["/usr/sbin/init"] 38 | -------------------------------------------------------------------------------- /files/docker/Dockerfile_Debian: -------------------------------------------------------------------------------- 1 | ARG VERSION=12 2 | FROM debian:$VERSION 3 | 4 | ENV container=docker 5 | 6 | STOPSIGNAL SIGRTMIN+3 7 | 8 | RUN apt update \ 9 | && apt install -y systemd dbus openssh-server rsyslog ca-certificates sudo python3 \ 10 | && apt clean 11 | 12 | #RUN sed -i 's/^\(module(load="imklog")\)/#\1/' /etc/rsyslog.conf 13 | 14 | COPY entrypoint.sh / 15 | RUN chmod +x /entrypoint.sh 16 | 17 | VOLUME /sys/fs/cgroup 18 | ENTRYPOINT ["/entrypoint.sh"] 19 | CMD ["/sbin/init"] 20 | -------------------------------------------------------------------------------- /files/docker/Dockerfile_Fedora: -------------------------------------------------------------------------------- 1 | ARG VERSION=42 2 | FROM fedora:$VERSION 3 | 4 | ENV container=docker 5 | 6 | STOPSIGNAL SIGRTMIN+3 7 | 8 | RUN dnf -y update \ 9 | && dnf -y install python3 python3-libdnf5 systemd openssh-server sudo gnupg2 \ 10 | && dnf clean all 11 | # && (cd /lib/systemd/system/sysinit.target.wants/ ; for i in * ; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i ; done) ; \ 12 | # rm -f /lib/systemd/system/multi-user.target.wants/* ;\ 13 | # rm -f /etc/systemd/system/*.wants/* ;\ 14 | # rm -f /lib/systemd/system/local-fs.target.wants/* ; \ 15 | # rm -f /lib/systemd/system/sockets.target.wants/*udev* ; \ 16 | # rm -f /lib/systemd/system/sockets.target.wants/*initctl* ; \ 17 | # rm -f /lib/systemd/system/basic.target.wants/* ;\ 18 | # rm -f /lib/systemd/system/anaconda.target.wants/* 19 | 20 | #RUN chcon -t var_run_t /run 21 | 22 | COPY entrypoint.sh / 23 | RUN chmod +x /entrypoint.sh 24 | 25 | VOLUME /sys/fs/cgroup 26 | ENTRYPOINT ["/entrypoint.sh"] 27 | # "Failed to attach X to compat systemd cgroup" 28 | # did not help: RUN sed -i -e 's#PrivateTmp=yes#PrivateTmp=no#' /lib/systemd/system/systemd-resolved.service 29 | # did not help? check again! docker daemon.json: { "exec-opts": ["native.cgroupdriver=systemd"] } 30 | # did not help: /sbin/init systemd.legacy_systemd_cgroup_controller 31 | CMD ["/sbin/init"] 32 | -------------------------------------------------------------------------------- /files/docker/Dockerfile_Ubuntu: -------------------------------------------------------------------------------- 1 | ARG VERSION=24.04 2 | FROM ubuntu:$VERSION 3 | 4 | ENV container=docker 5 | 6 | STOPSIGNAL SIGRTMIN+3 7 | 8 | RUN apt update \ 9 | && apt install -y systemd dbus openssh-server rsyslog ca-certificates sudo \ 10 | && apt clean 11 | 12 | #RUN sed -i 's/^\(module(load="imklog")\)/#\1/' /etc/rsyslog.conf 13 | 14 | COPY entrypoint.sh / 15 | RUN chmod +x /entrypoint.sh 16 | 17 | VOLUME /sys/fs/cgroup 18 | ENTRYPOINT ["/entrypoint.sh"] 19 | CMD ["/sbin/init"] 20 | -------------------------------------------------------------------------------- /files/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ -n "$ROOT_KEY" ]; then 6 | mkdir -p /root/.ssh 7 | echo "$ROOT_KEY" >> /root/.ssh/authorized_keys 8 | fi 9 | 10 | cat /dev/null 2>&1 & 11 | 12 | exec "$@" 13 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: DrPsychick 3 | role_name: ansible_testing 4 | namespace: drpsychick 5 | description: role for molecule to spin up systemd docker containers or libvirt VMs to test your ansible roles with 6 | license: MIT 7 | min_ansible_version: "2.10" 8 | 9 | platforms: 10 | - name: Ubuntu 11 | - name: Fedora 12 | 13 | galaxy_tags: 14 | - ansible 15 | - docker 16 | - testing 17 | - libvirt 18 | - molecule 19 | 20 | # dependencies: 21 | # "community.docker": ">=3.10.0" 22 | # "community.general": ">=9.0.0" 23 | # "community.libvirt": ">=1.3.0" 24 | -------------------------------------------------------------------------------- /molecule/default/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: all 4 | vars_files: 5 | - vars.yml 6 | ignore_unreachable: true 7 | tasks: 8 | - name: "Include ansible_testing destroy" 9 | ansible.builtin.include_role: 10 | name: drpsychick.ansible_testing 11 | vars: 12 | provision_action: 'destroy' 13 | ignore_errors: true 14 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | vars_files: 5 | - vars.yml 6 | tasks: 7 | - name: "Include ansible_testing create" 8 | ansible.builtin.include_role: 9 | name: drpsychick.ansible_testing 10 | vars: 11 | provision_action: 'create' 12 | -------------------------------------------------------------------------------- /molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | no_log: "{{ molecule_no_log }}" 6 | vars_files: 7 | - vars.yml 8 | tasks: 9 | 10 | - name: Provision 11 | import_role: 12 | name: drpsychick.ansible_testing 13 | vars: 14 | provision_action: 'create' 15 | 16 | - block: 17 | - name: Populate instance config dict 18 | ansible.builtin.set_fact: 19 | instance_conf_dict: { 20 | 'instance': "{{ item.name }}", 21 | } 22 | with_items: "{{ container_inventory }}" 23 | register: instance_config_dict 24 | 25 | - name: Convert instance config dict to a list 26 | ansible.builtin.set_fact: 27 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 28 | 29 | - name: Dump instance config 30 | ansible.builtin.copy: 31 | content: | 32 | # Molecule managed 33 | 34 | {{ instance_conf | to_json | from_json | to_yaml }} 35 | dest: "{{ molecule_instance_config }}" 36 | mode: "0600" 37 | -------------------------------------------------------------------------------- /molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | no_log: "{{ molecule_no_log }}" 6 | vars_files: 7 | - vars.yml 8 | tasks: 9 | 10 | - name: Deprovision 11 | import_role: 12 | name: drpsychick.ansible_testing 13 | vars: 14 | provision_action: 'destroy' 15 | 16 | - name: Populate instance config 17 | ansible.builtin.set_fact: 18 | instance_conf: {} 19 | 20 | - name: Dump instance config 21 | ansible.builtin.copy: 22 | content: | 23 | # Molecule managed 24 | 25 | {{ instance_conf | to_json | from_json | to_yaml }} 26 | dest: "{{ molecule_instance_config }}" 27 | mode: "0600" 28 | when: server.changed | default(false) | bool -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | lint: | 9 | ansible-lint 10 | platforms: 11 | - name: ubuntu2404 12 | - name: fedora41 13 | # - name: debian11 14 | provisioner: 15 | name: ansible 16 | verifier: 17 | name: ansible 18 | -------------------------------------------------------------------------------- /molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install docker driver 4 | hosts: all 5 | tasks: 6 | - name: Update ansible (fix for Fedora) 7 | ansible.builtin.shell: | 8 | pip3 install -U ansible 9 | delegate_to: localhost 10 | run_once: true 11 | when: ansible_distribution == "Fedora" 12 | 13 | - name: Install fuse-overlayfs 14 | package: 15 | name: fuse-overlayfs 16 | 17 | - name: Install prerequisite docker 18 | hosts: all 19 | vars: 20 | docker_install_compose: false 21 | docker_daemon_options: 22 | storage-driver: "fuse-overlayfs" 23 | log-driver: "local" 24 | log-opts: 25 | max-size: "100m" 26 | roles: 27 | - geerlingguy.docker 28 | 29 | - name: Install required packages 30 | hosts: all 31 | tasks: 32 | - name: Required packages 33 | ansible.builtin.package: 34 | name: "{{ ['python3-setuptools', 'python3-pip', 'python3-docker'] + (['openssh-client'] if ansible_distribution in ['Ubuntu','Debian'] else ['openssh-clients']) }}" 35 | -------------------------------------------------------------------------------- /molecule/default/requirements.yml: -------------------------------------------------------------------------------- 1 | roles: 2 | - name: geerlingguy.docker 3 | collections: 4 | - community.docker 5 | - community.general 6 | - community.libvirt 7 | -------------------------------------------------------------------------------- /molecule/default/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | work_dir: "/tmp/ansible-drpsychick.ansible_testing-default" 3 | containers: 4 | - { name: ubuntu2404, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 24.04 } } 5 | - { name: fedora41, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 41 } } 6 | # - { name: debian11, os: ubuntu, dockerfile: Dockerfile_Debian, files: ["entrypoint.sh"], args: { VERSION: 11 } } 7 | virtual_machines: [] 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "enabledManagers": ["dockerfile", "github-actions"] 7 | } 8 | -------------------------------------------------------------------------------- /requirements-ansible_14.txt: -------------------------------------------------------------------------------- 1 | # includes ansible-core 2.14.x 2 | ansible<8.0.0 3 | ansible-base 4 | ansible-lint 5 | ansible-lint-junit 6 | ansible-compat 7 | pywinrm 8 | molecule 9 | molecule-docker 10 | -------------------------------------------------------------------------------- /requirements-ansible_15.txt: -------------------------------------------------------------------------------- 1 | # includes ansible-core 2.15.x 2 | ansible<9.0.0 3 | ansible-base 4 | ansible-lint 5 | ansible-lint-junit 6 | ansible-compat 7 | pywinrm 8 | molecule 9 | molecule-docker 10 | -------------------------------------------------------------------------------- /requirements-ansible_16.txt: -------------------------------------------------------------------------------- 1 | # includes ansible-core 2.16.x 2 | ansible<10.0.0 3 | ansible-base 4 | ansible-lint 5 | ansible-lint-junit 6 | ansible-compat 7 | pywinrm 8 | molecule 9 | molecule-docker 10 | -------------------------------------------------------------------------------- /requirements-ansible_latest.txt: -------------------------------------------------------------------------------- 1 | ansible 2 | ansible-base 3 | ansible-lint 4 | ansible-lint-junit 5 | ansible-compat 6 | pywinrm 7 | molecule 8 | molecule-docker 9 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | # ansible-galaxy install -r requirements.yml 2 | collections: 3 | - community.docker 4 | - community.general 5 | - community.libvirt 6 | -------------------------------------------------------------------------------- /tasks/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure work directory exists 3 | ansible.builtin.file: 4 | path: "{{ work_dir }}" 5 | state: directory 6 | mode: "0755" 7 | 8 | - name: Generate SSH key 9 | ansible.builtin.shell: > 10 | if [ ! -r "{{ work_dir + '/' + ssh_keyfile }}" ]; then 11 | ssh-keygen -q -N "" -f {{ work_dir + '/' + ssh_keyfile }} -C {{ ssh_keyname }} || exit 1 12 | echo "done" 13 | fi 14 | register: ssh_key 15 | changed_when: ssh_key.stdout == "done" 16 | 17 | - name: Slurp public key 18 | ansible.builtin.slurp: 19 | src: "{{ work_dir + '/' + ssh_keyfile }}.pub" 20 | register: ssh_key_pub 21 | 22 | - name: Build and create containers 23 | ansible.builtin.include_tasks: 24 | file: create_container.yml 25 | vars: 26 | container: "{{ outer_container }}" 27 | with_items: "{{ containers }}" 28 | loop_control: 29 | loop_var: "outer_container" 30 | 31 | - name: Generate docker inventory 32 | ansible.builtin.copy: 33 | dest: "{{ work_dir }}/docker_inventory" 34 | mode: "0644" 35 | content: | 36 | all: 37 | hosts: 38 | {% for container in container_inventory %} 39 | {{ container.name }}: 40 | ansible_connection: docker 41 | {% endfor %} 42 | -------------------------------------------------------------------------------- /tasks/create_container.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy Dockerfile 3 | ansible.builtin.copy: 4 | src: "docker/{{ item }}" 5 | dest: "{{ work_dir }}/{{ item }}" 6 | mode: "0644" 7 | with_items: "{{ container.files + [container.dockerfile] }}" 8 | 9 | - name: Build container image {{ container.name }} 10 | community.docker.docker_image: 11 | name: "{{ container.name }}" 12 | source: build 13 | # not --check safe 14 | # force_source: true 15 | build: 16 | dockerfile: "{{ work_dir }}/{{ container.dockerfile }}" 17 | path: "{{ work_dir }}" 18 | args: "{{ container.args }}" 19 | 20 | # run named container in background with root-SSH 21 | - name: Run container {{ container.name }} 22 | community.docker.docker_container: 23 | name: "{{ container.name }}" 24 | hostname: "{{ container.name }}" 25 | image: "{{ container.name }}" 26 | state: started 27 | # required for now to have DBus working 28 | privileged: true 29 | tty: true 30 | security_opts: 31 | - seccomp=unconfined 32 | env: 33 | ROOT_KEY: "{{ ssh_key_pub['content'] | b64decode }}" 34 | cgroupns_mode: host 35 | volumes: 36 | # read-write required to work in GitHub Actions (+cgroupns=host) 37 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 38 | tmpfs: 39 | - /run 40 | - /run/lock 41 | - /tmp 42 | # did not work: 43 | # {{ ['/run', '/run/lock', '/tmp'] 44 | # + (['/sys/fs/cgroup/systemd'] if ansible_distribution != 'CentOS' else []) 45 | # + (['/var/lib/journal'] if ansible_distribution == 'Ubuntu' else []) 46 | # }} 47 | # - /sys/fs/cgroup/systemd # NOT for centos7 48 | # - /var/lib/journal 49 | # only got DBus to work with 'privileged' :( 50 | # capabilities: 51 | # - SYS_ADMIN 52 | # - SETPCAP 53 | # - NET_ADMIN 54 | # - NET_RAW 55 | # - NET_BIND_SERVICE 56 | # - DAC_OVERRIDE 57 | # - SYS_PTRACE 58 | # - SYSLOG 59 | # - AUDIT_CONTROL 60 | # - AUDIT_READ 61 | # - CHOWN 62 | # - DAC_READ_SEARCH 63 | # - FOWNER 64 | # - SETUID 65 | # - SETGID 66 | # - SETFCAP 67 | # - MAC_OVERRIDE 68 | # - SYS_CHROOT 69 | register: docker_start 70 | 71 | - name: Get container info 72 | community.docker.docker_container_info: 73 | name: "{{ container.name }}" 74 | register: container_info 75 | retries: 3 76 | delay: 3 77 | until: container_info.container.NetworkSettings.IPAddress != "" 78 | ignore_errors: true 79 | 80 | - name: Get error logs 81 | ansible.builtin.shell: |- 82 | docker logs {{ container.name }} 83 | register: docker_log 84 | when: container_info.container.State.Running == "false" or container_info.container.State.ExitCode != 0 85 | changed_when: false 86 | 87 | - name: Print logs 88 | ansible.builtin.debug: 89 | msg: "start {{ docker_start }}\nlogs {{ docker_log }}" 90 | when: docker_log is not skipped 91 | 92 | - name: SSH into the container {{ container.name }} 93 | ansible.builtin.shell: > 94 | ssh -i {{ work_dir + '/' + ssh_keyfile }} \ 95 | -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=off -o ConnectTimeout=10 \ 96 | root@{{ container_info.container.NetworkSettings.IPAddress }} \ 97 | "echo 'container is up'" 98 | register: ssh_cmd 99 | changed_when: false 100 | failed_when: ssh_cmd.stdout != "container is up" 101 | retries: 3 102 | delay: 3 103 | until: ssh_cmd.rc == 0 104 | 105 | - name: Add container {{ container.name }} to inventory as {{ container_info.container.NetworkSettings.IPAddress }} 106 | ansible.builtin.set_fact: 107 | container_inventory: > 108 | {{ container_inventory | default([]) + [ 109 | { 'name': container.name, 'os': container.os, 'ip': container_info.container.NetworkSettings.IPAddress } 110 | ] }} 111 | -------------------------------------------------------------------------------- /tasks/create_libvirt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure target directory exists 3 | ansible.builtin.file: 4 | path: "{{ work_dir }}/scripts" 5 | state: directory 6 | mode: "0755" 7 | 8 | - name: Download PowerShell upgrade script 9 | ansible.builtin.get_url: 10 | url: "{{ libvirt_upgrade_powershell_url }}" 11 | dest: "{{ work_dir }}/scripts/upgrade-powershell.ps1" 12 | mode: "0644" 13 | register: upgrade_powershell 14 | 15 | - name: Download WinRM hotfix script 16 | ansible.builtin.get_url: 17 | url: "{{ libvirt_winrm_hotfix_url }}" 18 | dest: "{{ work_dir }}/scripts/winrm-hotfix.ps1" 19 | mode: "0644" 20 | register: winrm_hotfix 21 | 22 | - name: Create virtual machines 23 | ansible.builtin.include_tasks: create_virtual_machine.yml 24 | vars: 25 | instance: "{{ outer_instance }}" 26 | with_items: "{{ virtual_machines }}" 27 | loop_control: 28 | loop_var: "outer_instance" 29 | 30 | - name: Wait to get virtual machine IPs 31 | ansible.builtin.shell: | 32 | set -o pipefail 33 | virsh domifaddr {{ item }} | grep 'ipv4' | awk '{print $4}' | awk -F/ '{print $1}' 34 | args: 35 | executable: /bin/bash 36 | register: vm_ips 37 | with_items: "{{ virtual_machine_inventory }}" 38 | changed_when: false 39 | failed_when: not vm_ips.stdout | regex_search('[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+') 40 | # wait up to 5 minutes 41 | retries: 30 42 | delay: 10 43 | until: vm_ips is not failed 44 | tags: 45 | - skip_ansible_lint 46 | 47 | - name: Add IPs to inventory 48 | ansible.builtin.set_fact: 49 | virtual_machine_inventory: "{{ virtual_machine_inventory | combine( 50 | { item.item: { 'ip': item.stdout } }, recursive=True 51 | ) }}" 52 | with_items: "{{ vm_ips.results }}" 53 | 54 | - name: Wait (up to 10 min) for virtual machines WinRM to be reachable 55 | ansible.builtin.wait_for: 56 | host: "{{ virtual_machine_inventory[item].ip }}" 57 | port: 5985 58 | state: started 59 | timeout: 600 60 | register: winrm_check 61 | with_items: "{{ virtual_machine_inventory }}" 62 | 63 | - name: Wait for successful WinRM command 64 | ansible.builtin.uri: 65 | url: "http://{{ virtual_machine_inventory[item].ip }}:5985/wsman" 66 | method: POST 67 | headers: 68 | Content-Type: application/soap+xml;charset=UTF-8 69 | force_basic_auth: true 70 | url_username: Administrator 71 | url_password: "{{ virtual_machine_inventory[item].password }}" 72 | body: | 73 | 77 | 78 | http://{{ virtual_machine_inventory[item].name }}:5985/wsman 79 | 80 | http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_LogicalDisk 81 | 82 | 83 | 84 | http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 85 | 86 | 87 | 88 | http://schemas.xmlsoap.org/ws/2004/09/transfer/Get 89 | 90 | 153600 91 | uuid:4ED2993C-4339-4E99-81FC-C2FD3812781A 92 | 93 | 94 | c: 95 | 96 | PT60.000S 97 | 98 | 99 | 100 | status_code: 200 101 | return_content: true 102 | register: winrm_login 103 | retries: 20 104 | delay: 10 105 | until: winrm_login is not failed 106 | with_items: "{{ virtual_machine_inventory }}" 107 | 108 | - name: Generate virsh inventory 109 | ansible.builtin.copy: 110 | dest: "{{ work_dir }}/virsh_inventory" 111 | mode: "0644" 112 | content: | 113 | all: 114 | hosts: 115 | {% for instance in virtual_machine_inventory %} 116 | {{ instance }}: 117 | ansible_host: {{ virtual_machine_inventory[instance].ip }} 118 | ansible_connection: 'winrm' 119 | ansible_wirm_transport: 'basic' 120 | ansible_user: 'Administrator' 121 | ansible_password: '{{ virtual_machine_inventory[instance].password }}' 122 | ansible_port: 5985 123 | {% endfor %} 124 | -------------------------------------------------------------------------------- /tasks/create_virtual_machine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # to setup a new VM 3 | - name: Ensure target directory exists 4 | ansible.builtin.file: 5 | path: "{{ work_dir }}/scripts-{{ instance.name }}" 6 | state: directory 7 | mode: "0755" 8 | when: instance.disk_image is not defined 9 | 10 | - name: Copy PowerShell upgrade script 11 | ansible.builtin.copy: 12 | remote_src: true 13 | src: "{{ work_dir }}/scripts/upgrade-powershell.ps1" 14 | dest: "{{ work_dir }}/scripts-{{ instance.name }}/upgrade-powershell.ps1" 15 | mode: "0644" 16 | register: upgrade_powershell 17 | when: instance.disk_image is not defined 18 | 19 | - name: Copy WinRM hotfix script 20 | ansible.builtin.copy: 21 | remote_src: true 22 | src: "{{ work_dir }}/scripts/winrm-hotfix.ps1" 23 | dest: "{{ work_dir }}/scripts-{{ instance.name }}/winrm-hotfix.ps1" 24 | mode: "0644" 25 | register: winrm_hotfix 26 | when: instance.disk_image is not defined 27 | 28 | - name: Template WinRM setup script 29 | ansible.builtin.template: 30 | src: setup-winrm.ps1 31 | dest: "{{ work_dir }}/scripts-{{ instance.name }}/setup-winrm.ps1" 32 | mode: "0644" 33 | register: setup_winrm 34 | when: instance.disk_image is not defined 35 | 36 | - name: Template AutoUnattend.xml 37 | ansible.builtin.template: 38 | src: AutoUnattend.xml 39 | dest: "{{ work_dir }}/scripts-{{ instance.name }}/AutoUnattend.xml" 40 | mode: "0644" 41 | register: autounattend 42 | when: instance.disk_image is not defined 43 | 44 | - name: Template autorun.bat 45 | ansible.builtin.template: 46 | src: autorun.bat 47 | dest: "{{ work_dir }}/scripts-{{ instance.name }}/autorun.bat" 48 | mode: "0644" 49 | register: autorun 50 | when: instance.disk_image is not defined 51 | 52 | - name: Generate scripts.iso 53 | ansible.builtin.shell: | 54 | mkisofs -o {{ libvirt_iso_dir }}/scripts-{{ instance.name }}.iso -J -r {{ work_dir }}/scripts-{{ instance.name }} 55 | when: instance.disk_image is not defined and ( 56 | upgrade_powershell is changed or winrm_hotfix is changed or setup_winrm is changed 57 | or autounattend is changed or autorun is changed 58 | ) 59 | changed_when: true 60 | 61 | - name: Set instance_uuid variable 62 | ansible.builtin.set_fact: 63 | instance_uuid: "{{ instance.uuid | default(99999999 | random | to_uuid) }}" 64 | 65 | - name: Template instance 66 | ansible.builtin.template: 67 | src: instance.xml 68 | dest: "{{ work_dir }}/instance-{{ instance.name }}.xml" 69 | mode: "0644" 70 | register: instance_template 71 | changed_when: instance_template is changed and instance.uuid is defined 72 | 73 | - name: Create disk 74 | ansible.builtin.shell: > 75 | if [ ! -r "{{ libvirt_image_dir }}/{{ instance.name }}.qcow2" ]; then 76 | virsh vol-create-as {{ libvirt_disk_pool }} {{ instance.name }}.qcow2 {{ instance.disk_size }} --format qcow2 > /dev/null; 77 | echo "created" 78 | fi 79 | register: create_disk 80 | changed_when: create_disk.stdout == "created" 81 | when: instance.disk_size is defined and instance.disk_image is not defined 82 | 83 | - name: Download disk image 84 | ansible.builtin.get_url: 85 | url: "{{ instance.disk_image_url }}" 86 | dest: "{{ libvirt_iso_dir }}/{{ instance.disk_image }}" 87 | mode: "0444" 88 | when: instance.disk_image_url is defined and instance.disk_image is defined 89 | 90 | - name: Unarchive disk image 91 | ansible.builtin.unarchive: 92 | src: "{{ libvirt_iso_dir }}/{{ instance.disk_image }}" 93 | dest: "{{ libvirt_image_dir }}/" 94 | mode: "0644" 95 | remote_src: true 96 | when: instance.disk_image is defined 97 | 98 | - name: Define virtual machine 99 | community.libvirt.virt: 100 | command: define 101 | xml: "{{ lookup('template', 'instance.xml') }}" 102 | 103 | - name: Create virtual machine 104 | community.libvirt.virt: 105 | command: create 106 | name: "{{ instance.name }}" 107 | xml: "{{ lookup('template', 'instance.xml') }}" 108 | state: running 109 | autostart: true 110 | register: create_vm 111 | 112 | - name: Get virtual machine info 113 | community.libvirt.virt: 114 | command: info 115 | name: "{{ instance.name }}" 116 | register: virt_info_cmd 117 | retries: 10 118 | delay: 10 119 | until: not virt_info_cmd.failed and virt_info_cmd[instance.name].state == "running" 120 | 121 | - name: Add instance {{ instance.name }} to inventory 122 | # as {{ .IPAddress }} 123 | ansible.builtin.set_fact: 124 | virtual_machine_inventory: > 125 | {{ virtual_machine_inventory | default({}) | combine( { instance.name: 126 | { 'name': instance.name, 'os': instance.os, 'password': instance.password } 127 | } ) }} 128 | -------------------------------------------------------------------------------- /tasks/dependency.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check for required packages 3 | ansible.builtin.shell: > 4 | pip3 || echo "pip3 missing"; 5 | ssh -G localhost || echo "openssh-client missing"; 6 | python3 -c 'import setuptools' || echo "setuptools python library missing"; 7 | python3 -c 'import docker' || echo "docker python library missing"; 8 | register: required_packages 9 | changed_when: false 10 | failed_when: "'missing' in required_packages.stdout" 11 | -------------------------------------------------------------------------------- /tasks/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy containers 3 | ansible.builtin.include_tasks: 4 | file: destroy_container.yml 5 | vars: 6 | container: "{{ outer_container }}" 7 | with_items: "{{ containers }}" 8 | loop_control: 9 | loop_var: "outer_container" 10 | 11 | - name: Delete SSH key 12 | ansible.builtin.shell: | 13 | if [ -r "{{ work_dir + '/' + ssh_keyfile }}" ]; then 14 | rm -f {{ work_dir + '/' + ssh_keyfile }} {{ ssh_keyfile }}.pub 15 | echo "done" 16 | fi 17 | register: ssh_key 18 | changed_when: ssh_key.stdout == "done" 19 | 20 | - name: Delete work directory 21 | ansible.builtin.file: 22 | path: "{{ work_dir }}" 23 | state: absent 24 | -------------------------------------------------------------------------------- /tasks/destroy_container.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Stop and remove container {{ container.name }} 3 | community.docker.docker_container: 4 | name: "{{ container.name }}" 5 | state: absent 6 | failed_when: false 7 | -------------------------------------------------------------------------------- /tasks/destroy_libvirt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy virtual machines 3 | ansible.builtin.include_tasks: destroy_virtual_machine.yml 4 | vars: 5 | instance: "{{ outer_instance }}" 6 | with_items: "{{ virtual_machines }}" 7 | loop_control: 8 | loop_var: "outer_instance" 9 | -------------------------------------------------------------------------------- /tasks/destroy_virtual_machine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy virtual machine 3 | community.libvirt.virt: 4 | name: "{{ instance.name }}" 5 | state: destroyed 6 | failed_when: false 7 | 8 | - name: Delete virtual machine 9 | community.libvirt.virt: 10 | name: "{{ instance.name }}" 11 | command: undefine 12 | failed_when: false 13 | 14 | - name: Check for image 15 | ansible.builtin.file: 16 | path: "{{ libvirt_image_dir }}/{{ instance.name }}.qcow2" 17 | state: file 18 | register: image_file 19 | failed_when: false 20 | 21 | - name: Delete image (volume) 22 | ansible.builtin.shell: virsh vol-delete {{ instance.name }}.qcow2 --pool {{ libvirt_disk_pool }} 23 | when: image_file.state != "absent" and instance.disk_image is not defined 24 | failed_when: false 25 | tags: 26 | - skip_ansible_lint 27 | 28 | - name: Delete image (file) 29 | ansible.builtin.file: 30 | path: "{{ libvirt_image_dir }}/{{ instance.name }}.qcow2" 31 | state: absent 32 | when: image_file.state != "absent" and instance.disk_image is defined 33 | failed_when: false 34 | 35 | - name: Delete directory 36 | ansible.builtin.file: 37 | path: "{{ work_dir }}" 38 | state: absent 39 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include dependency 3 | ansible.builtin.include_tasks: dependency.yml 4 | when: provision_action == 'create' 5 | 6 | - name: Create containers 7 | ansible.builtin.include_tasks: create.yml 8 | when: provision_action == 'create' and containers|length > 0 9 | 10 | - name: Create virtual machines 11 | ansible.builtin.include_tasks: create_libvirt.yml 12 | when: provision_action == 'create' and virtual_machines|length > 0 13 | 14 | - name: Destroy containers 15 | ansible.builtin.include_tasks: destroy.yml 16 | when: provision_action == 'destroy' and containers|length > 0 17 | 18 | - name: Destroy virtual machines 19 | ansible.builtin.include_tasks: destroy_libvirt.yml 20 | when: provision_action == 'destroy' and virtual_machines|length > 0 21 | -------------------------------------------------------------------------------- /templates/AutoUnattend.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | {% for d in instance.drivers %} 10 | 11 | {{ d }} 12 | 13 | {% endfor %} 14 | 15 | 16 | 19 | 20 | en-US 21 | 22 | en-US 23 | en-US 24 | en-US 25 | en-US 26 | en-US 27 | 28 | 31 | 32 | true 33 | 34 | 35 | OnError 36 | 37 | 0 38 | true 39 | 40 | 41 | 1 42 | Primary 43 | true 44 | 45 | 46 | 47 | 48 | NTFS 49 | 50 | C 51 | 1 52 | 1 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | OnError 61 | 62 | 0 63 | 1 64 | 65 | 66 | 67 | {% if instance.image_wimpath is defined %} 68 | {{ instance.image_wimpath }} 69 | {% endif %} 70 | 71 | /IMAGE/{{ 'INDEX' if instance.image_index is defined else 'NAME' }} 72 | {% if instance.image_index is defined %} 73 | {{ instance.image_index }} 74 | {% else %} 75 | {{ instance.image_name }} 76 | {% endif %} 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 87 | {{ instance.hostname|default(instance.name) }} 88 | 89 | 90 | 91 | 94 | 95 | OOBE 96 | 97 | 98 | 101 | UTC 102 | 103 | 104 | {{ instance.password }} 105 | true</PlainText> 106 | </AdministratorPassword> 107 | </UserAccounts> 108 | <AutoLogon> 109 | <Password> 110 | <Value>{{ instance.password }}</Value> 111 | <PlainText>true</PlainText> 112 | </Password> 113 | <Enabled>true</Enabled> 114 | <LogonCount>3</LogonCount> 115 | <Username>Administrator</Username> 116 | </AutoLogon> 117 | <FirstLogonCommands> 118 | <SynchronousCommand wcm:action="add"> 119 | <Order>1</Order> 120 | <CommandLine>F:\autorun.bat</CommandLine> 121 | <Description>Setup PowerShell and WinRM</Description> 122 | <RequiresUserInput>false</RequiresUserInput> 123 | </SynchronousCommand> 124 | </FirstLogonCommands> 125 | </component> 126 | </settings> 127 | </unattend> 128 | -------------------------------------------------------------------------------- /templates/autorun.bat: -------------------------------------------------------------------------------- 1 | PowerShell.exe -File F:\upgrade-powershell.ps1 -Version 5.1 -Verbose 2 | PowerShell.exe -File F:\winrm-hotfix.ps1 3 | PowerShell.exe -File F:\setup-winrm.ps1 4 | -------------------------------------------------------------------------------- /templates/instance.xml: -------------------------------------------------------------------------------- 1 | <domain type='kvm' id='50'> 2 | <name>{{ instance.name }}</name> 3 | <uuid>{{ instance_uuid }}</uuid> 4 | <metadata> 5 | <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0"> 6 | <libosinfo:os id="http://microsoft.com/win/2k19"/> 7 | </libosinfo:libosinfo> 8 | </metadata> 9 | <memory unit='KiB'>{{ libvirt_defaults.windows.memory }}</memory> 10 | <currentMemory unit='KiB'>{{ libvirt_defaults.windows.memory }}</currentMemory> 11 | <vcpu placement='static'>{{ libvirt_defaults.windows.cpu }}</vcpu> 12 | <resource> 13 | <partition>/machine</partition> 14 | </resource> 15 | <os> 16 | <type arch='x86_64' machine='pc-q35-4.2'>hvm</type> 17 | {% if instance.disk_image is not defined %} 18 | <boot dev="cdrom"/> 19 | {% endif %} 20 | <boot dev="hd"/> 21 | </os> 22 | <features> 23 | <acpi/> 24 | <hyperv> 25 | <relaxed state='on'/> 26 | <vapic state='on'/> 27 | <spinlocks state='on' retries='8191'/> 28 | </hyperv> 29 | </features> 30 | <cpu mode='host-passthrough' check='none' /> 31 | <clock offset='localtime'> 32 | <timer name='rtc' tickpolicy='catchup'/> 33 | <timer name='pit' tickpolicy='delay'/> 34 | <timer name='hpet' present='no'/> 35 | <timer name='hypervclock' present='yes'/> 36 | </clock> 37 | <on_poweroff>destroy</on_poweroff> 38 | <on_reboot>restart</on_reboot> 39 | <on_crash>destroy</on_crash> 40 | <pm> 41 | <suspend-to-mem enabled='no'/> 42 | <suspend-to-disk enabled='no'/> 43 | </pm> 44 | <devices> 45 | <emulator>/usr/bin/qemu-system-x86_64</emulator> 46 | <disk type='file' device='disk'> 47 | <driver name='qemu' type='qcow2'/> 48 | <source file='{{ libvirt_image_dir }}/{{ instance.name }}.qcow2' index='{{ instance.isos|default([])|length + 1 }}'/> 49 | <backingStore/> 50 | <target dev='vda' bus='virtio'/> 51 | <alias name='virtio-disk0'/> 52 | <address type='pci' domain='0x0000' bus='0x03' slot='0x00' function='0x0'/> 53 | </disk> 54 | {% for iso in instance.isos|default([]) %} 55 | <disk type='file' device='cdrom'> 56 | <driver name='qemu' type='raw'/> 57 | <source file='{{ libvirt_iso_dir }}/{{ iso.name }}' index='{{ instance.isos|length - loop.index + 1 }}'/> 58 | <backingStore/> 59 | <target dev='{{ iso.dev }}' bus='sata'/> 60 | <readonly/> 61 | <alias name='sata0-0-0'/> 62 | <address type='drive' controller='0' bus='0' target='0' unit='{{ loop.index-1 }}'/> 63 | </disk> 64 | {% endfor %} 65 | <controller type='usb' index='0' model='qemu-xhci' ports='15'> 66 | <alias name='usb'/> 67 | <address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/> 68 | </controller> 69 | <controller type='sata' index='0'> 70 | <alias name='ide'/> 71 | <address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/> 72 | </controller> 73 | <controller type='pci' index='0' model='pcie-root'> 74 | <alias name='pcie.0'/> 75 | </controller> 76 | <controller type='pci' index='1' model='pcie-root-port'> 77 | <model name='pcie-root-port'/> 78 | <target chassis='1' port='0x10'/> 79 | <alias name='pci.1'/> 80 | <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0' multifunction='on'/> 81 | </controller> 82 | <controller type='pci' index='2' model='pcie-root-port'> 83 | <model name='pcie-root-port'/> 84 | <target chassis='2' port='0x11'/> 85 | <alias name='pci.2'/> 86 | <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x1'/> 87 | </controller> 88 | <controller type='pci' index='3' model='pcie-root-port'> 89 | <model name='pcie-root-port'/> 90 | <target chassis='3' port='0x12'/> 91 | <alias name='pci.3'/> 92 | <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x2'/> 93 | </controller> 94 | <controller type='pci' index='4' model='pcie-root-port'> 95 | <model name='pcie-root-port'/> 96 | <target chassis='4' port='0x13'/> 97 | <alias name='pci.4'/> 98 | <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x3'/> 99 | </controller> 100 | <controller type='pci' index='5' model='pcie-root-port'> 101 | <model name='pcie-root-port'/> 102 | <target chassis='5' port='0x14'/> 103 | <alias name='pci.5'/> 104 | <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x4'/> 105 | </controller> 106 | <interface type='network'> 107 | <mac address='{{ instance.mac }}'/> 108 | <source network='default' portid='0b0631cd-fe8c-4335-a058-967c8175e7b9' bridge='virbr0'/> 109 | <target dev='vnet0'/> 110 | <model type='virtio'/> 111 | <alias name='net0'/> 112 | <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> 113 | </interface> 114 | <serial type='pty'> 115 | <source path='/dev/pts/3'/> 116 | <target type='isa-serial' port='0'> 117 | <model name='isa-serial'/> 118 | </target> 119 | <alias name='serial0'/> 120 | </serial> 121 | <console type='pty' tty='/dev/pts/3'> 122 | <source path='/dev/pts/3'/> 123 | <target type='serial' port='0'/> 124 | <alias name='serial0'/> 125 | </console> 126 | <input type='tablet' bus='usb'> 127 | <alias name='input0'/> 128 | <address type='usb' bus='0' port='1'/> 129 | </input> 130 | <input type='mouse' bus='ps2'> 131 | <alias name='input1'/> 132 | </input> 133 | <input type='keyboard' bus='ps2'> 134 | <alias name='input2'/> 135 | </input> 136 | {% if instance.vnc_port is defined %} 137 | <graphics type='vnc' port='{{ instance.vnc_port }}' autoport='no' listen='127.0.0.1'> 138 | <listen type='address' address='127.0.0.1'/> 139 | </graphics> 140 | {% endif %} 141 | <video> 142 | <model type='qxl' ram='65536' vram='65536' vgamem='16384' heads='1' primary='yes'/> 143 | <alias name='video0'/> 144 | <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x0'/> 145 | </video> 146 | <memballoon model='virtio'> 147 | <alias name='balloon0'/> 148 | <address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/> 149 | </memballoon> 150 | </devices> 151 | <seclabel type='dynamic' model='apparmor' relabel='yes'> 152 | <label>libvirt-{{ instance_uuid }}</label> 153 | <imagelabel>libvirt-{{ instance_uuid }}</imagelabel> 154 | </seclabel> 155 | <seclabel type='dynamic' model='dac' relabel='yes'> 156 | <label>+64055:+108</label> 157 | <imagelabel>+64055:+108</imagelabel> 158 | </seclabel> 159 | </domain> 160 | -------------------------------------------------------------------------------- /templates/setup-winrm.ps1: -------------------------------------------------------------------------------- 1 | # Setup WinRM - HTTP, basic 2 | Set-Item -Path WSMan:\localhost\Client\Auth\Basic -Value $true 3 | Set-Item -Path WSMan:\localhost\Client\AllowUnencrypted -Value $true 4 | Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true 5 | Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true 6 | 7 | # remove all listeners 8 | Remove-Item -Path WSMan:\localhost\Listener\* -Recurse -Force 9 | 10 | # create HTTP listener 11 | $selector_set = @{ 12 | Address = "*" 13 | Transport = "HTTP" 14 | } 15 | $value_set = @{ 16 | Hostname = "{{ instance.hostname|default(instance.name) }}" 17 | Enabled = "true" 18 | } 19 | New-WSManInstance -ResourceURI "winrm/config/Listener" -SelectorSet $selector_set -ValueSet $value_set 20 | 21 | Get-Service -Name WinRM | Restart-Service -------------------------------------------------------------------------------- /tests/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test create 3 | hosts: localhost 4 | vars: 5 | provision_action: 'create' 6 | vars_files: 7 | - provision.yml 8 | roles: 9 | - ansible-testing 10 | -------------------------------------------------------------------------------- /tests/create_vm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test create virtual machine 3 | hosts: localhost 4 | vars: 5 | provision_action: 'create' 6 | vars_files: 7 | - virtualmachine.yml 8 | roles: 9 | - ansible-testing 10 | -------------------------------------------------------------------------------- /tests/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test destroy 3 | hosts: localhost 4 | vars: 5 | provision_action: 'destroy' 6 | vars_files: 7 | - provision.yml 8 | roles: 9 | - ansible-testing 10 | -------------------------------------------------------------------------------- /tests/destroy_vm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test destroy virtual machine 3 | hosts: localhost 4 | vars: 5 | provision_action: 'destroy' 6 | vars_files: 7 | - virtualmachine.yml 8 | roles: 9 | - ansible-testing 10 | -------------------------------------------------------------------------------- /tests/provision.yml: -------------------------------------------------------------------------------- 1 | containers: 2 | # - { name: fedora33, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 33 } } 3 | # - { name: fedora40, os: fedora, dockerfile: Dockerfile_Fedora, files: ["entrypoint.sh"], args: { VERSION: 40 } } 4 | - { name: ubuntu2004, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 20.04 } } 5 | # - { name: ubuntu2404, os: ubuntu, dockerfile: Dockerfile_Ubuntu, files: ["entrypoint.sh"], args: { VERSION: 24.04 } } 6 | # - { name: centos7, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 7 } } 7 | # - { name: centos8, os: centos, dockerfile: Dockerfile_CentOS, files: ["entrypoint.sh"], args: { VERSION: 8 } } 8 | -------------------------------------------------------------------------------- /tests/virtualmachine.yml: -------------------------------------------------------------------------------- 1 | containers: [] 2 | virtual_machines: 3 | - name: "windows2016" 4 | password: "Ecoo6pev" 5 | os: "windows" 6 | isos: 7 | - name: "WindowsServer2016.iso" 8 | dev: sda 9 | - name: "virtio-win.iso" 10 | dev: sdb 11 | - name: "scripts-windows2016.iso" 12 | dev: sdc 13 | disk_size: 20G 14 | image_index: 2 15 | drivers: 16 | - 'E:\amd64\2k16' 17 | - 'E:\NetKVM\2k16\amd64' 18 | - 'E:\Balloon\2k16\amd64' 19 | mac: "02:00:00:00:13:37" 20 | vnc_port: 56682 21 | --------------------------------------------------------------------------------