diff --git a/.github/workflows/molecule.yml b/.github/workflows/molecule.yml index 71f8164..32f8fbd 100644 --- a/.github/workflows/molecule.yml +++ b/.github/workflows/molecule.yml @@ -11,27 +11,29 @@ on: jobs: molecule: name: molecule - runs-on: ubuntu-latest + runs-on: libvirt strategy: matrix: scenario: - - name: install - - name: offline - - name: online - - name: stop - - name: start - - name: restart - - name: update - - name: upgrade + - install + - offline + - online + - stop + - start + - restart + - update + - upgrade image: - - name: ubuntu2204 - command: /lib/systemd/systemd - - name: ubuntu2004 - command: /lib/systemd/systemd - - name: debian11 - command: /lib/systemd/systemd - - name: rockylinux9 - command: /usr/lib/systemd/systemd + - https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img + include: + - scenario: install + image: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img + - scenario: install + image: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2 + - scenario: install + image: https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2 + - scenario: update + image: https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2 steps: - name: Check out the codebase uses: actions/checkout@v4 @@ -61,6 +63,5 @@ jobs: env: PY_COLORS: '1' ANSIBLE_FORCE_COLOR: '1' - MOLECULE_DOCKER_IMAGE: ${{ matrix.image.name }} - MOLECULE_DOCKER_COMMAND: ${{ matrix.image.command }} - MOLECULE_SCENARIO: ${{ matrix.scenario.name }} + MOLECULE_KVM_IMAGE: ${{ matrix.image }} + MOLECULE_SCENARIO: ${{ matrix.scenario }} diff --git a/Makefile b/Makefile index c3873df..ea5975c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all ${MAKECMDGOALS} MOLECULE_SCENARIO ?= install -MOLECULE_DOCKER_IMAGE ?= ubuntu2004 +MOLECULE_KVM_IMAGE ?= https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img GALAXY_API_KEY ?= GITHUB_REPOSITORY ?= $$(git config --get remote.origin.url | cut -d: -f 2 | cut -d. -f 1) GITHUB_ORG = $$(echo ${GITHUB_REPOSITORY} | cut -d/ -f 1) @@ -21,6 +21,7 @@ test: lint install: @type poetry >/dev/null || pip3 install poetry @type yq || sudo apt-get install -y yq + @sudo apt-get install -y libvirt-dev @poetry install --no-root lint: install @@ -41,7 +42,7 @@ build: requirements @poetry run ansible-galaxy collection build --force dependency create prepare converge idempotence side-effect verify destroy login reset list: - MOLECULE_DOCKER_IMAGE=${MOLECULE_DOCKER_IMAGE} poetry run molecule $@ -s ${MOLECULE_SCENARIO} + MOLECULE_KVM_IMAGE=${MOLECULE_KVM_IMAGE} poetry run molecule $@ -s ${MOLECULE_SCENARIO} ignore: @poetry run ansible-lint --generate-ignore diff --git a/galaxy.yml b/galaxy.yml index 2e07464..0ff84c3 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: nephelaiio name: mongodb -version: 0.1.4 +version: 0.2.0 readme: README.md authors: - Ted Cook diff --git a/molecule/common/cleanup.yml b/molecule/common/cleanup.yml new file mode 100644 index 0000000..7d70d50 --- /dev/null +++ b/molecule/common/cleanup.yml @@ -0,0 +1,22 @@ +--- +- name: Destroy libvirt storage + hosts: localhost + become: true + vars_files: + - vars.yml + tasks: + - name: Delete cached images + ansible.builtin.file: + dest: "{{ _path }}" + state: absent + vars: + _basename: "{{ item | urlsplit('path') | basename }}" + _path: "{{ _libvirt_path }}/{{ _basename }}" + loop_control: + label: "{{ _path }}" + loop: "{{ molecule_yml.platforms | map(attribute='image') | unique }}" + + - name: Destroy libvirt pool + community.libvirt.virt_pool: + state: deleted + name: "{{ _libvirt_pool }}" diff --git a/molecule/common/create.yml b/molecule/common/create.yml new file mode 100644 index 0000000..f4511a1 --- /dev/null +++ b/molecule/common/create.yml @@ -0,0 +1,272 @@ +--- +- name: Configure KVM host + hosts: localhost + become: true + vars_files: + - vars.yml + tasks: + - name: Install package requirements + ansible.builtin.package: + name: + - qemu-kvm + - qemu-efi + - libvirt-clients + - libvirt-daemon-system + - libvirt-daemon-system-systemd + - python3-libvirt + become: true + + - name: Query libvirt pools + community.libvirt.virt_pool: + command: list_pools + register: _pools + + - name: Create libvirt path + ansible.builtin.file: + path: "{{ _libvirt_path }}" + state: directory + owner: root + group: root + mode: 0755 + become: true + + - name: Define libvirt pool + community.libvirt.virt_pool: + command: define + name: "{{ _libvirt_pool }}" + xml: "{{ lookup('ansible.builtin.template', 'pool.j2.xml') }}" + vars: + _pool: "{{ _libvirt_pool }}" + _path: "{{ _libvirt_path }}" + + - name: Activate libvirt pool + community.libvirt.virt_pool: + state: active + name: "{{ _libvirt_pool }}" + + - name: Add libvirt network + community.libvirt.virt_net: + command: define + name: "{{ _libvirt_network }}" + xml: "{{ lookup('ansible.builtin.template', 'network.j2.xml') }}" + vars: + _network: "{{ _libvirt_network }}" + _address: "{{ _libvirt_address }}" + _dhcp: "{{ _libvirt_dhcp }}" + _net_address: "{{ _address | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }}" + _net_mask: "{{ _address | ansible.utils.ipaddr('netmask') }}" + _net_dhcp_start: "{{ _dhcp | ansible.utils.ipaddr('2') | ansible.utils.ipaddr('address') }}" + _net_dhcp_end: "{{ _dhcp | ansible.utils.ipaddr('-2') | ansible.utils.ipaddr('address') }}" + when: _libvirt_network != "default" + + - name: Start libvirt network + community.libvirt.virt_net: + command: create + name: "{{ _libvirt_network }}" + when: _libvirt_network != "default" + + - name: List cached images + ansible.builtin.find: + paths: "{{ _libvirt_pool }}" + recurse: false + register: _cache_query + + - name: Inspect cached images + ansible.builtin.set_fact: + _uncached_images: "{{ (_uncached_images | default([])) + [item] }}" + vars: + _cached_images: "{{ _cache_query.files | map(attribute='path') }}" + _basename: "{{ item | urlsplit('path') | basename }}" + _path: "{{ _libvirt_pool }}/{{ _basename }}" + loop: "{{ molecule_yml.platforms | map(attribute='image') | unique }}" + when: _path not in _cached_images + + - name: Cache platform images + ansible.builtin.get_url: + url: "{{ item }}" + dest: "{{ _path }}" + owner: root + group: root + mode: 0600 + vars: + _basename: "{{ item | urlsplit('path') | basename }}" + _path: "{{ _libvirt_path }}/{{ _basename }}" + loop: "{{ _uncached_images | default([]) }}" + + +- name: Create KVM guests + hosts: all + gather_facts: false + become: true + vars_files: + - vars.yml + tasks: + - name: Create KVM guest + delegate_to: localhost + block: + - name: Create SSH key + community.crypto.openssh_keypair: + path: "{{ lookup('ansible.builtin.env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/id_rsa" + run_once: true + register: _pubkey + become: false + + - name: Query active KVM guests + ansible.builtin.command: virsh list --name + run_once: true + register: guest_query + + - name: Manage KVM guests + when: inventory_hostname not in guest_query.stdout_lines + block: + + - name: Create KVM guest disks + ansible.builtin.copy: + remote_src: true + src: "{{ _libvirt_path }}/{{ _platform.image | urlsplit('path') | basename }}" + dest: "{{ _disk }}" + owner: libvirt-qemu + group: kvm + mode: 0666 + force: true + vars: + _platforms: "{{ molecule_yml.platforms }}" + _platform: "{{ _platforms | selectattr('name', 'equalto', inventory_hostname) | first }}" + _disk: "{{ _libvirt_path }}/{{ _platform.name }}.img" + + - name: Resize KVM guest disks + ansible.builtin.command: + cmd: qemu-img resize {{ _disk }} {{ _platform.size | default('20G') }} + vars: + _platforms: "{{ molecule_yml.platforms }}" + _platform: "{{ _platforms | selectattr('name', 'equalto', inventory_hostname) | first }}" + _disk: "{{ _libvirt_path }}/{{ _platform.name }}.img" + + - name: Create cloud-init tempdir + ansible.builtin.tempfile: + state: directory + prefix: libvirt + register: libvirt_tmpdir + run_once: true + + - name: Create KVM guest + block: + - name: Create instance tempdir + ansible.builtin.file: + dest: "{{ libvirt_tmpdir.path }}/{{ inventory_hostname }}" + state: directory + + - name: Create cloud-init meta data + ansible.builtin.template: + src: "meta-data.yml.j2" + dest: "{{ libvirt_tmpdir.path }}/{{ inventory_hostname }}/meta-data" + + - name: Create cloud-init user data + ansible.builtin.template: + src: "user-data.yml.j2" + dest: "{{ libvirt_tmpdir.path }}/{{ inventory_hostname }}/user-data" + vars: + guest_user: "{{ _libvirt_user }}" + guest_key: "{{ _pubkey.public_key }}" + + - name: Create cloud-init iso + ansible.builtin.command: + cmd: | + xorriso -as mkisofs + -volid cidata -joliet -rock + -o {{ _libvirt_path }}/{{ inventory_hostname }}.iso . + chdir: "{{ libvirt_tmpdir.path }}/{{ inventory_hostname }}" + + - name: Create KVM guest + community.libvirt.virt: + xml: "{{ lookup('ansible.builtin.template', 'vm.j2.xml') }}" + command: define + vars: + guest_hostname: "{{ inventory_hostname }}" + guest_disk_format: qcow2 + guest_disk_path: "{{ _libvirt_path }}/{{ inventory_hostname }}.img" + guest_iso_path: "{{ _libvirt_path }}/{{ inventory_hostname }}.iso" + guest_network: "{{ _libvirt_network }}" + guest_mac: "{{ '52:54:00' | random_mac(seed=guest_hostname) }}" + loop_control: + label: "{{ inventory_hostname }} - {{ _libvirt_path }}/{{ inventory_hostname }}.img" + + always: + - name: Destroy cloud-init tempdir + ansible.builtin.file: + dest: "{{ libvirt_tmpdir.path }}" + state: absent + run_once: true + + - name: Start KVM guest + community.libvirt.virt: + name: "{{ inventory_hostname }}" + state: running + + +- name: Build instance config file + hosts: localhost + vars_files: + - vars.yml + tasks: + - name: Create SSH key + community.crypto.openssh_keypair: + path: "{{ lookup('ansible.builtin.env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/id_rsa" + run_once: true + register: _pubkey + + - name: Retrieve platform ip information + become: true + block: + - name: Wait for DHCP lease assignment + ansible.builtin.shell: + cmd: "virsh net-dhcp-leases --network {{ _libvirt_network }} | grep {{ item }}" + executable: /bin/bash + loop: "{{ molecule_yml.platforms | map(attribute='name') }}" + register: dhcp_query + retries: 6 + delay: 30 + until: dhcp_query is succeeded + + - name: Query DHCP leases + ansible.builtin.shell: > + virsh net-dhcp-leases --network {{ _libvirt_network }} | grep {{ item }} | awk '{print $5}' + loop: "{{ molecule_yml.platforms | map(attribute='name') }}" + register: dhcp_query + + - name: Populate instance config dict + ansible.builtin.set_fact: + instance_conf_dict: { + 'instance': "{{ item }}", + 'address': "{{ _libvirt_address | ansible.utils.ipaddr('address') }}", + 'user': "{{ _libvirt_user }}", + 'port': "22", + 'identity_file': "{{ _pubkey.filename }}" + } + vars: + _libvirt_addresses: "{{ dhcp_query.results | nephelaiio.plugins.list_to_dict('item') }}" + _libvirt_address: "{{ _libvirt_addresses[item].stdout }}" + loop: "{{ molecule_yml.platforms | map(attribute='name') }}" + register: instance_config_dict + + - name: Convert instance config dict to a list + ansible.builtin.set_fact: + _instance_conf: "{{ _instance_results | map(attribute='ansible_facts.instance_conf_dict') | list }}" + vars: + _instance_results: "{{ instance_config_dict.results }}" + + - name: Dump instance config + ansible.builtin.copy: + content: "{{ _instance_conf | ansible.builtin.to_nice_yaml(indent=2) }}" + dest: "{{ molecule_instance_config }}" + mode: 0640 + + - name: Configure host file entries + ansible.builtin.lineinfile: + path: /etc/hosts + regexp: ".*{{ item.instance }}.*" + line: "{{ item.address }} {{ item.instance }}" + loop_control: + label: "{{ item.instance }}" + loop: "{{ _instance_conf }}" + become: true diff --git a/molecule/common/destroy.yml b/molecule/common/destroy.yml new file mode 100644 index 0000000..9ec8f4d --- /dev/null +++ b/molecule/common/destroy.yml @@ -0,0 +1,57 @@ +--- +- name: Destroy KVM guests + hosts: localhost + become: true + vars_files: + - vars.yml + tasks: + - name: Query active KVM guests + ansible.builtin.command: virsh list --name + register: guest_query + + - name: Set KVM guest facts + ansible.builtin.set_fact: + _guests: "{{ molecule_yml.platforms | selectattr('name', 'in', guest_query.stdout_lines) }}" + + - name: Stop KVM guests + ansible.builtin.command: virsh destroy {{ item }} + loop: "{{ _guests | map(attribute='name') }}" + + - name: Query defined KVM guests + ansible.builtin.command: virsh list --name --all + register: guest_query + + - name: Undefine KVM guests + ansible.builtin.command: virsh undefine {{ item }} + loop: "{{ _guests | map(attribute='name') }}" + + - name: Delete guest disks + ansible.builtin.file: + dest: "{{ _path }}" + state: absent + vars: + _extensions: ['img', 'iso'] + _path: "{{ _libvirt_path }}/{{ item.0 }}.{{ item.1 }}" + loop_control: + label: "{{ _path }}" + loop: "{{ molecule_yml.platforms | map(attribute='name') | product(_extensions) }}" + + - name: Stop libvirt network + community.libvirt.virt_net: + command: destroy + name: "{{ _libvirt_network }}" + when: _libvirt_network != "default" + failed_when: false + + - name: Delete libvirt network + community.libvirt.virt_net: + command: undefine + name: "{{ _libvirt_network }}" + when: _libvirt_network != "default" + + - name: Delete host file entries + ansible.builtin.lineinfile: + path: /etc/hosts + regexp: ".*{{ item }}.*" + state: absent + loop: "{{ molecule_yml.platforms | map(attribute='name') }}" diff --git a/molecule/common/prepare.yml b/molecule/common/prepare.yml index 7aba4a6..52c3150 100644 --- a/molecule/common/prepare.yml +++ b/molecule/common/prepare.yml @@ -1,6 +1,7 @@ --- - name: Update apt repos hosts: all + become: true roles: - nephelaiio.mongodb.repo tasks: diff --git a/molecule/common/templates/meta-data.yml.j2 b/molecule/common/templates/meta-data.yml.j2 new file mode 100644 index 0000000..d2a729f --- /dev/null +++ b/molecule/common/templates/meta-data.yml.j2 @@ -0,0 +1,2 @@ +instance-id: {{ inventory_hostname }} +local-hostname: {{ inventory_hostname }} diff --git a/molecule/common/templates/network.j2.xml b/molecule/common/templates/network.j2.xml new file mode 100644 index 0000000..be95b60 --- /dev/null +++ b/molecule/common/templates/network.j2.xml @@ -0,0 +1,14 @@ + + {{ _network }} + + + + + + + + + + + + diff --git a/molecule/common/templates/pool.j2.xml b/molecule/common/templates/pool.j2.xml new file mode 100644 index 0000000..03109ed --- /dev/null +++ b/molecule/common/templates/pool.j2.xml @@ -0,0 +1,11 @@ + + {{ _pool }} + + {{ _path }} + + 0711 + 0 + 0 + + + diff --git a/molecule/common/templates/user-data.yml.j2 b/molecule/common/templates/user-data.yml.j2 new file mode 100644 index 0000000..954bde0 --- /dev/null +++ b/molecule/common/templates/user-data.yml.j2 @@ -0,0 +1,8 @@ +#cloud-config +users: + - name: {{ guest_user }} + ssh_authorized_keys: + - {{ guest_key }} + sudo: "ALL=(ALL) NOPASSWD:ALL" + groups: sudo + shell: /bin/bash diff --git a/molecule/common/templates/vm.j2.xml b/molecule/common/templates/vm.j2.xml new file mode 100644 index 0000000..a314e04 --- /dev/null +++ b/molecule/common/templates/vm.j2.xml @@ -0,0 +1,213 @@ + + {{ guest_hostname }} + 4194304 + 4194304 + 2 + + /machine + + + hvm + + + + + + + + + + + + + + destroy + restart + + + + + + /usr/bin/qemu-system-x86_64 + + + + + + +
+ + + + + + + + +
+ + + +
+ + + + + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + +
+ + + +
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + +
+ +