Skip to content

Commit

Permalink
Merge pull request #8 from andreygubarev/feature-vmnet-support
Browse files Browse the repository at this point in the history
feature vmnet support
  • Loading branch information
andreygubarev authored May 31, 2023
2 parents 59f3e7d + a00bd2d commit 56ffedf
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 27 deletions.
32 changes: 32 additions & 0 deletions .yamllint
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
extends: default

rules:
braces:
max-spaces-inside: 1
level: error
brackets:
max-spaces-inside: 1
level: error
colons:
max-spaces-after: -1
level: error
commas:
max-spaces-after: -1
level: error
comments: disable
comments-indentation: disable
document-start: disable
empty-lines:
max: 3
level: error
hyphens:
level: error
indentation: disable
key-duplicates: enable
line-length: disable
new-line-at-end-of-file: disable
new-lines:
type: unix
trailing-spaces: disable
truthy: disable
67 changes: 62 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,23 @@ Support guest OS:
* Ubuntu 20.04 LTS (x86_64)
* Debian 11 (x86_64)

Support of other platforms and guest OS is possible, but not tested.
Support of other platforms and guest OS is possible, but not tested. Please, open an issue if you want to add support for other platforms.

## Network modes

Network mode is selected by setting `vm_network` in `molecule.yml`. Supported modes are: `user` and `vmnet-shared`. Default mode is `user`. All modes are mutually exclusive.

### `user` network mode

This is the default network mode. It uses QEMU's user networking mode.

Mode is selected by setting `vm_network: user` in `molecule.yml`.

### `vmnet-shared` network mode

This mode uses QEMU's `vmnet-shared` networking mode. It requires `vmnet.framework` to be installed on the host. This mode is only supported on MacOS. It requires *passwordless* `sudo` access for current user.

Mode is selected by setting `vm_network: vmnet-shared` in `molecule.yml`.

# Examples

Expand All @@ -31,7 +47,8 @@ Support of other platforms and guest OS is possible, but not tested.
molecule init scenario default --driver-name molecule-qemu --verifier-name testinfra
```

## Example `molecule.yml`
## Example `molecule.yml` for `user` network mode

```yaml
---
dependency:
Expand All @@ -40,21 +57,56 @@ driver:
name: molecule-qemu
platforms:
- name: ubuntu-1
image: file:///Users/andrey/Downloads/focal-server-cloudimg-arm64.img
image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img
image_checksum: sha256:https://cloud-images.ubuntu.com/focal/current/SHA256SUMS
image_arch: aarch64
ssh_port: 10022
ssh_user: ubuntu
- name: ubuntu-2
image: file:///Users/andrey/Downloads/focal-server-cloudimg-amd64.img
image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img
image_checksum: sha256:https://cloud-images.ubuntu.com/focal/current/SHA256SUMS
image_arch: x86_64 # default
ssh_port: 10023
ssh_user: ubuntu
- name: debian-1
image: file:///Users/andrey/Downloads/debian-11-generic-amd64.qcow2
image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img
image_checksum: sha256:https://cloud-images.ubuntu.com/focal/current/SHA256SUMS
image_arch: x86_64 # default
ssh_port: 10024
ssh_user: debian
provisioner:
name: ansible
verifier:
name: ansible
```
## Example `molecule.yml` for `vmnet-shared` network mode

```yaml
---
dependency:
name: galaxy
driver:
name: molecule-qemu
platforms:
- name: ubuntu-1
image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img
image_checksum: sha256:https://cloud-images.ubuntu.com/focal/current/SHA256SUMS
image_arch: aarch64
ssh_user: ubuntu
vm_network: vmnet-shared
- name: ubuntu-2
image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img
image_checksum: sha256:https://cloud-images.ubuntu.com/focal/current/SHA256SUMS
image_arch: x86_64 # default
ssh_user: ubuntu
vm_network: vmnet-shared
- name: debian-1
image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img
image_checksum: sha256:https://cloud-images.ubuntu.com/focal/current/SHA256SUMS
image_arch: x86_64 # default
ssh_user: debian
vm_network: vmnet-shared
provisioner:
name: ansible
verifier:
Expand All @@ -71,3 +123,8 @@ verifier:
* [Ansible](https://www.ansible.com/)
* [Molecule](https://molecule.readthedocs.io/en/latest/)
* [QEMU](https://www.qemu.org/)

## QEMU vmnet-shared networking

* [vmnet.framework modes](https://lore.kernel.org/all/[email protected]/T/)

117 changes: 95 additions & 22 deletions molecule_qemu/playbooks/create.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
qemu_vm_memory: "1024"
qemu_vm_cpus: "2"
qemu_vm_disk: "4G"
qemu_vm_network: "user"

tasks:
### configuration #########################################################

- name: Register VMs data
ansible.builtin.set_fact:
instance: {
Expand All @@ -29,13 +31,14 @@
"image_arch": "{{ item.image_arch | default(qemu_vm_image_arch) }}",
"image_format": "{{ item.image_format | default(qemu_vm_image_format) }}",

"ssh_host": "{{ item.ssh_host | default('127.0.0.1') }}",
"ssh_port": "{{ item.ssh_port | default(10022) }}",
"ssh_port": "{{ item.ssh_port | default(22) }}",
"ssh_user": "{{ item.ssh_user }}",

"vm_cpus": "{{ item.vm_cpus | default(qemu_vm_cpus) }}",
"vm_memory": "{{ item.vm_memory | default(qemu_vm_memory) }}",
"vm_disk": "{{ item.vm_disk | default(qemu_vm_disk) }}",
"vm_network": "{{ item.vm_network | default(qemu_vm_network) }}",
"vm_network_mac": "{{ '52:54:00' | community.general.random_mac(seed=item.name | to_json | hash('md5')) | regex_replace('(^|:)0([0-9A-Fa-f])', '\\1\\2') }}",

"path_disk": "{{ molecule_ephemeral_directory }}/run/{{ item.name }}.qcow2",
"path_pid": "{{ molecule_ephemeral_directory }}/run/{{ item.name }}.pid",
Expand All @@ -50,27 +53,48 @@
molecule_instances: "{{ molecule_instances.results | map(attribute='ansible_facts.instance') | list }}"

### assertions ############################################################

- name: Assert VMs configuration
ansible.builtin.assert:
that:
- molecule_instances is defined
- molecule_instances | length > 0
- molecule_instances | length == molecule_yml.platforms | length
- molecule_instances | map(attribute='ssh_port') | list | unique | length == molecule_instances | length
fail_msg: "Molecule instances are not properly defined"
success_msg: "Molecule instances are properly defined"

- name: Assert supported VMs configuration
ansible.builtin.assert:
that:
- item.image_arch in ['x86_64', 'aarch64']
- item.vm_network in ['user', 'vmnet-shared']
fail_msg: "Molecule instance {{ item.name }} configuration is not supported"
success_msg: "Molecule instance {{ item.name }} configuration is supported"
loop: "{{ molecule_instances }}"
loop_control:
label: "{{ item.name }}"

- name: Assert VMs network configuration
ansible.builtin.assert:
that:
- molecule_instances | map(attribute='vm_network') | list | unique | length == 1
fail_msg: "Network modes are mutually exclusive"
success_msg: "Network modes are configured properly"

- name: Set QEMU network
ansible.builtin.set_fact:
qemu_vm_network: "{{ molecule_instances | map(attribute='vm_network') | list | unique | first }}"

- name: Assert VMs ssh configuration
ansible.builtin.assert:
that:
- molecule_instances | map(attribute='ssh_port') | list | unique | length == molecule_instances | length
fail_msg: "Molecule instances SSH are not properly configured for 'user' network"
success_msg: "Molecule instances SSH are properly configured for 'user' network"
when: qemu_vm_network == 'user'

### capabilities ##########################################################

- name: Read kern.hv_support
ansible.builtin.command: sysctl kern.hv_support
register: hv_support
Expand Down Expand Up @@ -111,6 +135,7 @@
when: "'aarch64' in molecule_instances | map(attribute='image_arch') | list | unique"

### images ###############################################################

- name: Create images directory
ansible.builtin.file:
path: "{{ molecule_driver_directory }}/images/"
Expand Down Expand Up @@ -141,6 +166,7 @@
label: "{{ item[0] | basename }}"

### cloud-init ############################################################

- name: Create cloud-init folders
ansible.builtin.file:
path: "{{ molecule_ephemeral_directory }}/run/cloud-init/{{ item.name }}/"
Expand Down Expand Up @@ -183,6 +209,7 @@
label: "{{ item.name }}"

### qemu ##################################################################

- name: Create VMs disks
ansible.builtin.command: >
qemu-img create
Expand All @@ -196,17 +223,26 @@
loop_control:
label: "{{ item.name }}"

- name: Launch VMs as priviliged user
ansible.builtin.set_fact:
qemu_privileged: "{{ (qemu_vm_network == 'vmnet-shared') | bool }}"

- name: Launch VMs
ansible.builtin.command: >
become: "{{ qemu_privileged }}"
ansible.builtin.command: >-
qemu-system-{{ item.image_arch }}
-name {{ item.name }}
-boot d
-cdrom {{ molecule_ephemeral_directory }}/run/cloud-init/{{ item.name }}.iso
-hda {{ item.path_disk }}
-m {{ item.vm_memory }}
-smp {{ item.vm_cpus }}
-net nic
-net user,hostfwd=tcp::{{ item.ssh_port }}-:22
{% if item.vm_network == 'vmnet-shared' %}
-nic vmnet-shared,model=virtio-net-pci,mac={{ item.vm_network_mac }}
{% endif %}
{% if item.vm_network == 'user' %}
-nic user,hostfwd=tcp::{{ item.ssh_port }}-:22
{% endif %}
-display none
-daemonize
-pidfile {{ item.path_pid }}
Expand All @@ -224,44 +260,81 @@
loop_control:
label: "{{ item.name }}"

- name: Allow read access to VMs pid files
become: "{{ qemu_privileged }}"
ansible.builtin.file:
path: "{{ item.path_pid }}"
mode: "0644"
loop: "{{ molecule_instances }}"
loop_control:
label: "{{ item.name }}"

### qemu network #########################################################

- name: Create instances IPv4 empty dict
ansible.builtin.set_fact:
molecule_instances_ipv4: {}

- name: Create instances IPv4 dict
ansible.builtin.set_fact:
molecule_instances_ipv4: "{{ molecule_instances_ipv4 | combine({item.name: '127.0.0.1'}) }}"
loop: "{{ molecule_instances }}"
loop_control:
label: "{{ item.name }}"

### qemu network: vmnet-shared ############################################

- name: Configure QEMU network (vmnet-shared)
when: qemu_vm_network == 'vmnet-shared'
block:
- name: Get IPv4 addresses from ARP table (vmnet-shared)
ansible.builtin.shell: |
set -o pipefail
arp -an | grep -i '{{ item.vm_network_mac }}' | awk '{print $2}' | sed 's/[()]//g'
until: molecule_instances_arp.stdout | length > 0
retries: 30
delay: 1
loop: "{{ molecule_instances }}"
loop_control:
label: "{{ item.name }}"
register: molecule_instances_arp
changed_when: false

- name: Create instances IPv4 dict (vmnet-shared)
ansible.builtin.set_fact:
molecule_instances_ipv4: "{{ molecule_instances_ipv4 | combine({item.item.name: item.stdout}) }}"
loop: "{{ molecule_instances_arp.results }}"
loop_control:
label: "{{ item.item.name }}"


### ssh ###################################################################

- name: Wait for SSH
ansible.builtin.wait_for:
host: "{{ item.ssh_host }}"
host: "{{ molecule_instances_ipv4[item.name] }}"
port: "{{ item.ssh_port }}"
delay: 5
timeout: 180
search_regex: "OpenSSH"
loop: "{{ molecule_instances }}"
loop_control:
label: "{{ item.name }}"
async: 300
poll: 0
register: qemu_launch

- name: Wait for SSH to be ready
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ qemu_launch.results }}"
loop_control:
label: "{{ item.item.name }}"
register: qemu_launch_status
until: qemu_launch_status.finished
retries: 30
delay: 10

### molecule ##############################################################

- name: Prepare VMs config dict
ansible.builtin.set_fact:
instance_conf_dict: {
"instance": "{{ item.instance }}",
"name": "{{ item.name }}",
"address": "{{ item.ssh_host }}",
"address": "{{ molecule_instances_ipv4[item.name] }}",
"user": "{{ item.ssh_user }}",
"port": "{{ item.ssh_port }}",
"identity_file": "{{ ssh_keypair.filename }}",
"pidfile": "{{ item.path_pid }}",
"diskfile": "{{ item.path_disk }}",
"privileged": "{{ qemu_privileged }}",
}
loop: "{{ molecule_instances }}"
loop_control:
Expand Down
2 changes: 2 additions & 0 deletions molecule_qemu/playbooks/destroy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
register: qemu_processes

- name: Destroy running VMs
become: "{{ item.item.privileged | default(false) }}"
ansible.builtin.command: >
kill {{ item.ansible_facts.pid }}
changed_when: false
Expand All @@ -32,6 +33,7 @@
label: "{{ item.item.name }}"

- name: Delete PID files
become: "{{ item.item.privileged | default(false) }}"
ansible.builtin.file:
path: "{{ item.pidfile }}"
state: absent
Expand Down

0 comments on commit 56ffedf

Please sign in to comment.