Infrastructure roles

These are examples of infrastructure roles for deployment of VM's and/or containers on proxmox.
As OS for these containers/VM's I'm using Rocky Linux.
The container template could be any linux, but I chose to standardize on Rocky.
For virtual machine deployments, I'm using coud-init enabled templates, so configuration is easy during deployment.

role_infrastructure_proxmox_lxc

This role consists of a number of files in the folowing directory structure:

.
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
└── tasks
    └── main.yml

As you can see, this is a standard role layout within a collection. In fact is is a role from a collection I use for proxmox.

We will go over the files one-by-one:

README.md

As the name says, read this file to know ho the role works and what is does.

# infrastructure_lxc role

This role will create a lxc container on a proxmox node/cluster from variables given.  
It registers the ip address in DNS, and then create the lxc container. The DNS is expected to be a linux based bind9 type dns-server, controlled by ansible. 

The container is first created, then started, as the collection will issue an error like "VM with ID <number> not found on cluster" when using state 'started' on creation.  

If rhaap has a dynamic inventory for the proxmox instances, the new node will automaticly 
pop-up in the inventory.  

The default action is to 'create' a new container, possiblew action are:

- create
- remove
- redeploy
- purge

'redeploy' will first remove the container and the 'create' a fresh instance of the same container.

## dependencies

This role needs the following collections/dependecies to run correctly:

### Collections

- community.general  
- community.proxmox  
- ansible.utils  

Community.general and ansible.utils are needed for the upodates to be made on the bind nameserver(s).  
Community.proxmox is obvious.  

### python

- dnspython  
- proxmoxer  
- requests  
- netaddr  

### Extra

Added symlink /usr/bin/python3 to /usr/bin/python3.11  else the inventory will work in controller, but not in the playbook,  
this way it will run the plugin correctly in the job.  

## inventory vars

Be sure to have the following structure in the inventory of the play that calls this role and that they are part of the proxmox group:

group_vars/all/proxmox.yml
```yml
proxmox_node: proxmox01
proxmox_user: <use a crecential for root user>
proxmox_password: <use a credential for the root password>
proxmox_api_port: '8006'
netmask: '/24'
gateway: <gateway ip>
dns_server: <dns server ip address>
dns_domain: <your dns domain>
dns_rev_zone: <the reverse zone name>
dns_key: <the keyname for ddns>
dns_key_secret: < ddns secret>
dns_key_algorithm: <ddns secret algorithm>
dns_zonefile: <name of the zonefile in dns>
network_bridge: <network bridge name>
template_location: <the full path to the template directory like:'/mnt/pve/NAS-01/import/'>
cloud_init_location: <shared storage name>
default_root_passwd: <the default root password for your containers>
ssh_pub_key: <the public key to be inserted in authorized_keys>
qemu_storage: local-lvm
qemu_template: <the name of the cloudinit enables template for qemu vm like; Rocky-9-GenericCloud.latest.x86_64.qcow2>
lxc_storage: local
lxc_template: <template for lxc containers, like; lxc-rocky-ansible.tar.gz>
drn_net:
  netmask: '/24'
  network_bridge: <network bridge name>
  network_vlan: <vlan>
````
Some of these variables are not used in this role, but will be used by the qemu role.

## survey vars

The extra vars (from survey or other source) for the lxc role to work, are as followes:  
These are the vars that can be used at the moment, additional vars will be added later.  

````yml
infrastructure_proxmox_action: <one of; 'create', 'remove', 'redeploy', 'purge'>
infrastructure_proxmox_lxc_proxmox_node: <the name of the proxmox node to work on (where the lxc lives)>
infrastructure_proxmox_lxc_host_name: <automaticly set to inventory_hostname>
infrastructure_proxmox_lxc_ip_adress: <ip address for the lxc container>
infrastructure_proxmox_lxc_host_id: <node_id on proxmox cluster, empty will generate one>
infrastructure_proxmox_lxc_cores: <number of cores to assign to container>
infrastructure_proxmox_lxc_disk_size: <integer with storage in GB to assign>
infrastructure_proxmox_lxc_disk_storage: <one of; 'local', 'local-lvm' or other known storage>
infrastructure_proxmox_lxc_template: <template file to use>
infrastructure_proxmox_lxc_memory_size: <memory in MB to allocate to the container>
infrastructure_proxmox_lxc_swap_size: <swap in MB to allocate to the container>
infrastructure_proxmox_lxc_auto_startup: <start the container as proxmox boots?>
````


## example

In rhaap we create a job-template that will fil in the following paramaters for the role:  

````yml
infrastructure_proxmox_action: create
infrastructure_proxmox_lxc_host_name: test1.homelab
infrastructure_proxmox_lxc_ip_adress: 10.1.1.5
infrastructure_proxmox_lxc_host_id: 101
infrastructure_proxmox_lxc_cores: 2
infrastructure_proxmox_lxc_disk_size: 3
infrastructure_proxmox_lxc_disk_storage: local
infrastructure_proxmox_lxc_template: lxc-rocky.tar.gz
infrastructure_proxmox_lxc_memory_size: 32
infrastructure_proxmox_lxc_swap_size: 128
infrastructure_proxmox_lxc_auto_startup: true
````

The above example will create a host on proxmox with the name test1.homelab.  
This role will do no further configuration on this lxc container, this should be done by other
plays.

## DNS issue
The serial we create for the bind nameserver is created using the years without the century, because named will find the serial out-of-range and thus will not load the new file.  

For other actions, just fill the infrastructure_lxc_action with the correct value.  

defaults/main.yml

The default values for variables are set here:

---
infrastructure_proxmox_lxc_template: "{{ template }}"                         # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_host_id: "{{ id }}"                                # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_cores: "{{ cpu_cores }}"                           # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_disk_storage: "{{ disk_storage }}"                 # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_disk_size: "{{ disk_size }}"                       # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_proxmox_api_node: "{{ proxmox_api_node }}"         # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_proxmox_node: "{{ proxmox_node }}"                 # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_action: create                                     # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_auto_startup: true                                 # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_swap_size: "{{ swap_size | default('0') }}"        # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_memory_size: "{{ memory_size | default('128') }}"  # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_ip_address: "{{ ip_address }}"                     # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_dns_zonefile: "{{ dns_zone_file }}"                # noqa: var-naming[no-role-prefix]
lxc_features: "{{ lxc_features | default('') }}"                              # noqa: var-naming[no-role-prefix]
lxc_unprivileged: "{{ lxc_unprivileged | default(true) }}"                    # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_description: "{{ vm_description | default('# VM name') }}" # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_lxc_wait_for_cloudinit: "{{ pause_cloud_init | default('20') }}" # noqa: var-naming[no-role-prefix]

meta/main.yml

---
# This file is required for the requirements.yaml mechanism to work
galaxy_info:
  role_name: infrastructure_proxmox_lxc
  namespace: wilcofolkers
  description: This role creates a lxc container on proxmox
  author: Wilco Folkers
  license: BSD
  min_ansible_version: '2.15'
  platforms:
    - name: EL
      versions: [all]

tasks/main.yml

This is where the magic happens:

---
- name: Set the host_name variable
  ansible.builtin.set_fact:                 # noqa: var-naming[no-role-prefix]
    infrastructure_proxmox_lxc_host_name: "{{ inventory_hostname }}"

- name: Excute block when infrastructure_proxmox_lxc_action is 'redeploy'
  when: infrastructure_proxmox_lxc_action == "redeploy"
  block:

    - name: Set the vars for container re-creation
      ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
        infrastructure_proxmox_lxc_ip_address: "{{ proxmox_net0['ip'] | split('/') | first }}"
        infrastructure_proxmox_lxc_host_id: "{{ id }}"
        infrastructure_proxmox_lxc_proxmox_api_node: "{{ proxmox_api_node }}"
        infrastructure_proxmox_lxc_proxmox_node: "{{ proxmox_node }}"
        infrastructure_proxmox_lxc_disk_storage: "{{ proxmox_rootfs['disk_image'] | split(':') | first }}"
        infrastructure_proxmox_lxc_disk_size: "{{ proxmox_rootfs['size'][:-1] }}"
        infrastructure_proxmox_lxc_auto_startup: true

- name: Excute block when infrastructure_proxmox_lxc_action is 'create'
  delegate_to: "{{ dns_server }}"
  become: true
  when: infrastructure_proxmox_lxc_action == "create"
  block:

    - name: Create new serial for DNS
      ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
        _dns_serial: "{{ '%y%m%d%H%M' | strftime }}"
        infrastructure_proxmox_lxc_ip_address: "{{ ip_address }}"

    - name: Create container record in DNS
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.forward"
        line: >-
          {{ "{:23}".format(inventory_hostname.split('.') | first) }}
          {{ "{:7}".format('A') }}
          {{ infrastructure_proxmox_lxc_ip_address }}
        regexp: "^{{ inventory_hostname.split('.') | first }} "
        insertafter: EOF
        owner: named
        mode: '0644'
        state: present

    - name: Update Serial in DNS forward zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.forward"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Create reverse DNS entry for container
      ansible.builtin.lineinfile:              # noqa: jinja[spacing]
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.rev"
        line: >-
          {{ "{:23}".format(infrastructure_proxmox_lxc_ip_address.split('.') | last) }}
          {{ "{:7}".format('PTR') }}
          {{ inventory_hostname }}{{'.'}}
        regexp: "^{{ infrastructure_proxmox_lxc_ip_address.split('.') | last }} "
        insertafter: EOF
        owner: named
        mode: '0644'
        state: present

    - name: Update Serial in DNS reverse zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.rev"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Restart named to activate new record
      ansible.builtin.service:
        name: named
        state: restarted
      run_once: true

- name: Excute block when infrastructure_proxmox_lxc_action is 'remove' or 'purge'
  delegate_to: "{{ dns_server }}"
  become: true
  when: >
    infrastructure_proxmox_lxc_action == "remove" or
    infrastructure_proxmox_lxc_action == "purge"
  block:
    - name: Create new serial for DNS
      ansible.builtin.set_fact:              # noqa: var-naming[no-role-prefix]
        _dns_serial: "{{ '%y%m%d%H%M' | strftime }}"

    - name: Remove container record from DNS
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.forward"
        line: ''
        regexp: "^{{ inventory_hostname.split('.') | first }} "
        owner: named
        mode: '0644'
        state: absent

    - name: Update Serial in DNS forward zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.forward"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Remove reverse DNS entry for container
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.rev"
        line: ''
        regexp: "^{{ infrastructure_proxmox_lxc_ip_address.split('.') | last }} "
        owner: named
        mode: '0644'
        state: absent

    - name: Update Serial in DNS reverse zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_lxc_dns_zonefile }}.rev"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Restart named to activate new record
      ansible.builtin.service:
        name: named
        state: restarted
      run_once: true

- name: Excute block when infrastructure_proxmox_lxc_action is 'remove, redeploy or purge'
  delegate_to: localhost
  when: >
    infrastructure_proxmox_lxc_action == "remove" or
    infrastructure_proxmox_lxc_action == "purge" or
    infrastructure_proxmox_lxc_action == "redeploy"
  block:

    - name: Check if the container exists
      community.proxmox.proxmox_vm_info:
        api_host: "{{ infrastructure_proxmox_lxc_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        type: lxc
        validate_certs: false
        name: "{{ infrastructure_proxmox_lxc_host_name }}"

    - name: Stop the container
      community.proxmox.proxmox:
        api_host: "{{ infrastructure_proxmox_lxc_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_lxc_proxmox_node }}"
        vmid: "{{ infrastructure_proxmox_lxc_host_id }}"
        hostname: "{{ infrastructure_proxmox_lxc_host_name }}"
        validate_certs: false
        timeout: 120
        force: true
        state: stopped

    - name: Delete the container
      community.proxmox.proxmox:
        api_host: "{{ infrastructure_proxmox_lxc_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_lxc_proxmox_node }}"
        vmid: "{{ infrastructure_proxmox_lxc_host_id }}"
        hostname: "{{ infrastructure_proxmox_lxc_host_name }}"
        validate_certs: false
        state: absent
        purge: true

- name: Excute block when infrastructure_proxmox_lxc_action is 'create or redeploy'
  delegate_to: localhost
  when: >
    infrastructure_proxmox_lxc_action == "create" or
    infrastructure_proxmox_lxc_action == "redeploy"
  block:

    - name: Create network interface dict with jinja
      ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
        _net_if: >
          {%- set net = dict() -%}
          {%- if network_vlan is defined -%}
          {%- set _ = net.update({'net0': 'name=eth0,gw=' + gateway + ',ip=' + infrastructure_proxmox_lxc_ip_address + netmask + ',bridge=' + network_bridge + ',tag=' + network_vlan}) -%}
          {%- else -%}
          {%- set _ = net.update({'net0': 'name=eth0,gw=' + gateway + ',ip=' + infrastructure_proxmox_lxc_ip_address + netmask + ',bridge=' + network_bridge}) -%}
          {%- endif -%}
          {%- if drn_interface is defined -%}
          {%- if drn_net['network_vlan'] is defined -%}
          {%- set _ = net.update({'net1': 'name=eth1,ip=' + drn_interface['ip_address'] + drn_net['netmask'] + ',bridge=' + drn_net['network_bridge'] + ',tag=' + drn_net['network_vlan']}) -%}
          {%- else -%}
          {%- set _ = net.update({'net1': 'name=eth1,ip=' + drn_interface['ip_address'] + drn_net['netmask'] + ',bridge=' + drn_net['network_bridge']}) -%}
          {%- endif -%}
          {%- endif -%}
          {{ net }}

    - name: Create the container from template
      community.proxmox.proxmox:
        api_host: "{{ infrastructure_proxmox_lxc_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_lxc_proxmox_node }}"
        vmid: "{{ infrastructure_proxmox_lxc_host_id }}"
        password: "{{ default_root_passwd }}"
        hostname: "{{ infrastructure_proxmox_lxc_host_name }}"
        description: |
          "{{ infrastructure_proxmox_lxc_description }}"
        cores: "{{ infrastructure_proxmox_lxc_cores }}"
        disk: "{{ infrastructure_proxmox_lxc_disk_storage }}:{{ infrastructure_proxmox_lxc_disk_size }}"
        nameserver: "{{ dns_servers }}"
        searchdomain: "{{ dns_domain }}"
        ostemplate: "{{ template_location }}:vztmpl/{{ infrastructure_proxmox_lxc_template }}"
        netif:
          "{{ _net_if }}"
        onboot: "{{ infrastructure_proxmox_lxc_auto_startup | bool }}"
        swap: "{{ infrastructure_proxmox_lxc_swap_size }}"
        memory: "{{ infrastructure_proxmox_lxc_memory_size }}"
        timezone: 'Europe/Amsterdam'
        tags: "{{ tags }}"
        unprivileged: "{{ lxc_unprivileged }}"
        features: "{{ lxc_features }}"
        update: true
        validate_certs: false
        state: present

    - name: Let the cluster settle for a moment
      ansible.builtin.pause:
        seconds: 5

    - name: Set swappiness for the container to 0
      ansible.builtin.lineinfile:
        path: "/etc/pve/lxc/{{ infrastructure_proxmox_lxc_host_id }}.conf"
        line: 'lxc.cgroup.memory.swappiness: 0'
        insertafter: EOF
        owner: root
        group: www-data
        mode: '0640'
        state: present
      delegate_to: "{{ infrastructure_proxmox_lxc_proxmox_node }}.homelab"
      become: true

    - name: Start the container
      community.proxmox.proxmox:
        api_host: "{{ infrastructure_proxmox_lxc_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_lxc_proxmox_node }}"
        vmid: "{{ infrastructure_proxmox_lxc_host_id }}"
        hostname: "{{ infrastructure_proxmox_lxc_host_name }}"
        validate_certs: false
        state: started

    - name: Wait for ssh to be ready
      ansible.builtin.wait_for:
        port: 22
        host: "{{ infrastructure_proxmox_lxc_host_name }}"
        delay: 10
        state: started
        timeout: 120

    - name: Configure ssh certificate login
      ansible.builtin.shell:                     # noqa: command-instead-of-shell
        cmd: "pct exec {{ infrastructure_proxmox_lxc_host_id }} -- bash -c '{{ _command }}'"
      changed_when: true
      loop: "{{ cloud_commands }}"
      loop_control:
        loop_var: _command
      become: true
      delegate_to: "{{ infrastructure_proxmox_lxc_proxmox_node }}.homelab"

role_infrastructure_proxmox_qemu

The role to deploy a VM on proxmox has the following directory layout:

.
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
└── templates
    └── cloud-init.yaml.j2

As you can see, this also conformes to the standard role directory layout, whitout the otherwise empty directories.

README.md

The README.md file has the following content, explaining the code and variables:

# role_infrastructure_proxmox_qemu

A role to install a cloudinit vm on a proxmox cluster.  

The machine is initially configured with a cloudinit script, after configuration the cloudinit device is removed.
Further configuration is done through ansible playbooks you will have to write.

This role can handle LVM and non-LVM based templates.   

## Execution environment

To use this role from this collection, it is best to build a custom execution environment with the following content:

### collection dependencies

The collections used for this role:
```yml
---
collections:
  - community.general
  - community.proxmox
  - ansible.utils
````

### python dependecies

These collections have a number of python dependecies:  

```yml
 - requests
 - dnspython
 - netaddr
 - proxmoxer
````

### symlink

Added symlink /usr/bin/python3 to /usr/bin/python3.11  else the inventory will work in controller, but not in the playbook,  
this way it will run the plugin correctly in the job.  

## inventory vars

Be sure to have the following structure in the inventory of the play that calls this role and that they are part of the proxmox group:

group_vars/all/proxmox.yml
```yml
proxmox:
  proxmox_node: proxmox01
  proxmox_user: <use a crecential for root user>
  proxmox_password: <use a credential for the root password>
  proxmox_api_port: '8006'
  netmask: '/24'
  gateway: <gateway ip>
  dns_servers: <dns server ip address> # must be only one here! we configure more later
  dns_domain: <your dns domain>
  dns_rev_zone: <the reverse zone name>
  dns_key: <the keyname for ddns>
  dns_key_secret: < ddns secret>
  dns_key_algorithm: <ddns secret algorithm>
  network_bridge: <network bridge name>
  template_location: <the full path to the template directory like:'/mnt/pve/NAS-01/import/'>
  cloud_init_location: <shared storage name>
  default_root_passwd: <the default root password for your containers>
  ssh_pub_key: <the public key to be inserted in authorized_keys>
  qemu_storage: local-lvm
  qemu_template: <the name of the cloudinit enables template for qemu vm like; Rocky-9-GenericCloud.latest.x86_64.qcow2>
  lxc_storage: local
  lxc_template: <template for lxc containers, like; lxc-rocky-ansible.tar.gz>
  drn_net:
    netmask: '/24'
    network_bridge: <network bridge name>
    network_vlan: <vlan>
````
Some of these variables are not used in this role, but will be used by the lxc role.

## host vars

In the host_vars file in the inventory, specify the following:

```yml
host_name: target15.homelab
description: |
  "
    platform: Rocky Linux
    contact: Wilco Folkers
  "
ip_address: 192.168.2.135
cpu_cores: 2
memory_size: 2048
id: 435
drn_interface:
  ip_address: 10.0.0.135
type: qemu
proxmox_node: proxmox02
root_disk_size: 30
os:
  name: LNX_rhel
  version: master
disks:
  - name: scsi1
    size: 10
    vg: test
  - name: scsi2
    size: 20
    vg: best
lvm:
  - lv_name: log
    mountpoint: /var
    size: '10'
    vg_name: test
  - lv_name: app
    mountpoint: /opt/app
    size: '20'
    vg_name: best
````
In the host_vars above, the following variables are optional:  
- root_disk_size (default is 10G)
- disks (default no extra disks)
- lvm (mandatory when extra disks are defined)


## example

```yml
---
- name: Example playbook 
  hosts: target15.homelab
  connection: local
  gather_facts: false
  vars:
   infrastructure_proxmox_qemu_action: create

  roles:
    wf_linux.infra.role_infrastructure_proxmox_qemu
````
This will create the new vm on the proxmox2 node of the cluster...

defaults/main.yml

The defaults.yml holds all variable defaults for this role:

---
infrastructure_proxmox_qemu_template: "{{ template }}"                                 # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_id: "{{ id }}"                                             # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_cores: "{{ cpu_cores | default('2') }}"                    # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_storage: "{{ disk_storage }}"                              # noqa: var-naming[no-role-prefix]
# infrastructure_proxmox_qemu_disk_size: 10                                            # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_action: create                                             # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_auto_startup: true                                         # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_memory_size: "{{ memory_size | default('2048') }}"         # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_ip_address: "{{ ip_address }}"                             # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_proxmox_node: "{{ proxmox_node }}"                         # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_proxmox_api_node: "{{ proxmox_api_node }}"                 # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_dns_zonefile: "{{ dns_zone_file }}"                        # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_description: "{{ vm_description | default('# VM name') }}" # noqa: var-naming[no-role-prefix]
infrastructure_proxmox_qemu_wait_for_cloudinit: "{{ pause_cloud_init | default('20') }}" # noqa: var-naming[no-role-prefix]

meta/main.yml

The meta information for this role:

---
# This file is required for the requirements.yaml mechanism to work
galaxy_info:
  role_name: infrastructure_proxmox_qemu
  namespace: wilcofolkers
  description: This role creates a cloudinit vm on proxmox
  author: Wilco Folkers
  license: BSD
  min_ansible_version: '2.15'
  platforms:
    - name: EL
      versions: [all]

tasks/main.yml

This is the code that will deploy a VM on proxmox, using variables and a cloud enabled template.
First the system is registered in DNS, so it will tranlate correctly.

---
- name: Set the host_name variable
  ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
    infrastructure_proxmox_qemu_host_name: "{{ inventory_hostname }}"

- name: Excute block when infrastructure_proxmox_qemu_action is 'redeploy'
  when: infrastructure_proxmox_qemu_action == "redeploy"
  block:

    - name: Get network setings
      ansible.builtin.setup:
        gather_subset:
          - 'default_ipv4'

    - name: Set the vars for host re-creation
      ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
        infrastructure_proxmox_qwmu_ip_address: "{{ ansible_facts['default_ipv4']['address'] }}"
        infrastructure_proxmox_qemu_id: "{{ proxmox_vmid }}"
        infrastructure_proxmox_qemu_proxmox_node: "{{ proxmox_node }}"
        infrastructure_proxmox_qemu_proxmox_api_node: "{{ proxmox_api_node }}"
        infrastructure_proxmox_qemu_disk_storage: "{{ proxmox_scsi0['disk_image'] | split(':') | first }}"
        infrastructure_proxmox_qemu_disk_size: "{{ proxmox_scsi0['size'][:-1] }}"
        infrastructure_proxmox_qemu_auto_startup: true

- name: Excute block when infrastructure_proxmox_qemu_action is 'create'
  delegate_to: "{{ dns_server }}"
  become: true
  when: infrastructure_proxmox_qemu_action == "create"
  block:

    - name: Create new serial for DNS
      ansible.builtin.set_fact:           # noqa: var-naming[no-role-prefix]
        _dns_serial: "{{ '%y%m%d%H%M' | strftime }}"
        infrastructure_proxmox_qemu_ip_address: "{{ ip_address }}"

    - name: Create host record in DNS
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.forward"
        line: >-
          {{ "{:23}".format(inventory_hostname.split('.') | first) }}
          {{ "{:7}".format('A') }}
          {{ infrastructure_proxmox_qemu_ip_address }}
        regexp: "^{{ inventory_hostname.split('.') | first }} "
        insertafter: EOF
        owner: named
        mode: '0644'
        state: present

    - name: Update Serial in DNS forward zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.forward"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Create reverse DNS entry for host
      ansible.builtin.lineinfile:            # noqa: jinja[spacing]
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.rev"
        line: >-
          {{ "{:23}".format(infrastructure_proxmox_qemu_ip_address.split('.') | last) }}
          {{ "{:7}".format('PTR') }}
          {{ inventory_hostname }}{{'.'}}
        regexp: "^{{ infrastructure_proxmox_qemu_ip_address.split('.') | last }} "
        insertafter: EOF
        owner: named
        mode: '0644'
        state: present

    - name: Update Serial in DNS reverse zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.rev"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Restart named to activate new record
      ansible.builtin.service:
        name: named
        state: restarted
      run_once: true

- name: Excute block when infrastructure_proxmox_qemu_action is 'remove' or 'purge'
  delegate_to: "{{ dns_server }}"
  become: true
  when: >
    infrastructure_proxmox_qemu_action =="remove" or
    infrastructure_proxmox_qemu_action == "purge"
  block:

    - name: Create new serial for DNS
      ansible.builtin.set_fact:               # noqa: var-naming[no-role-prefix]
        _dns_serial: "{{ '%y%m%d%H%M' | strftime }}"

    - name: Remove host record from DNS
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.forward"
        line: ''
        regexp: "^{{ inventory_hostname.split('.') | first }} "
        owner: named
        mode: '0644'
        state: absent

    - name: Update Serial in DNS forward zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.forward"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Remove reverse DNS entry for host
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.rev"
        line: ''
        regexp: "^{{ infrastructure_proxmox_qemu_ip_address.split('.') | last }} "
        owner: named
        mode: '0644'
        state: absent

    - name: Update Serial in DNS reverse zone
      ansible.builtin.lineinfile:
        path: "/var/named/{{ infrastructure_proxmox_qemu_dns_zonefile }}.rev"
        line: >-
          {{ "{:31}".format(' ') }}
          {{ _dns_serial }}
          {{ "{:>10}".format('; serial') }}
        regexp: "serial$"
        owner: named
        mode: '0644'
        state: present

    - name: Restart named to activate new record
      ansible.builtin.service:
        name: named
        state: restarted
      run_once: true

- name: Excute block when infrastructure_proxmox_qemu_action is 'remove, redeploy or purge'
  delegate_to: localhost
  when: >
    infrastructure_proxmox_qemu_action =="remove" or
    infrastructure_proxmox_qemu_action =="purge" or
    infrastructure_proxmox_qemu_action =="redeploy"
  block:

    - name: Check if the machine exists
      community.proxmox.proxmox_vm_info:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        type: qemu
        validate_certs: false
        name: "{{ infrastructure_proxmox_qemu_host_name }}"

    - name: Stop the host
      community.proxmox.proxmox_kvm:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_qemu_proxmox_node }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        name: "{{ infrastructure_proxmox_qemu_host_name }}"
        validate_certs: false
        force: true
        state: stopped

    - name: Delete the host
      community.proxmox.proxmox_kvm:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: '{{ proxmox_password }}'
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_qemu_proxmox_node }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        name: "{{ infrastructure_proxmox_qemu_host_name }}"
        validate_certs: false
        state: absent

- name: Excute block when infrastructure_proxmox_qemu_action is 'create or redeploy'
  delegate_to: localhost
  when: >
    infrastructure_proxmox_qemu_action =="create" or
    infrastructure_proxmox_qemu_action =="redeploy"
  block:

    - name: Check if the machine exists
      community.proxmox.proxmox_vm_info:       # noqa: var-naming[no-role-prefix]
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        name: "{{ infrastructure_proxmox_qemu_host_name }}"
      register: _vm_info_list

    - name: Fail the play if node exists
      ansible.builtin.fail:
        msg: "Node {{ infrastructure_proxmox_qemu_host_name }} allready exists"
      when: host_name in _vm_info_list

    - name: Upload Cloudinit user-data file
      ansible.builtin.template:
        src: cloud-init.yaml.j2
        dest: "/mnt/pve/NAS-01/snippets/{{ infrastructure_proxmox_qemu_id }}-cloudinit.yaml"
        mode: "0644"
      delegate_to: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
      become: true

    - name: Create network interface dict with jinja
      ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
        net_if: >
          {%- set net = dict() -%}
          {%- if network_vlan is defined -%}
          {%- set _ = net.update({'net0': 'virtio,bridge=' + network_bridge + ',tag=' + network_vlan + ',firewall=1'}) -%}
          {%- else -%}
          {%- set _ = net.update({'net0': 'virtio,bridge=' + network_bridge + ',firewall=1'}) -%}
          {%- endif -%}
          {%- if drn_interface is defined -%}
          {%- if drn_net['network_vlan'] is defined -%}
          {%- set _ = net.update({'net1': 'virtio,bridge=' + drn_net['network_bridge'] + ',tag=' + drn_net['network_vlan'] + ',firewall=1'}) -%}
          {%- else -%}
          {%- set _ = net.update({'net1': 'virtio,bridge=' + drn_net['network_bridge'] + ',firewall=1'}) -%}
          {%- endif -%}
          {%- endif -%}
          {{ net }}

    - name: Create network ipconfig dict with jinja
      ansible.builtin.set_fact:             # noqa: var-naming[no-role-prefix]
        net_ip: >
          {%- set netip = dict() -%}
          {%- set _ = netip.update({'ipconfig0': 'ip=' + infrastructure_proxmox_qemu_ip_address~netmask + ',gw=' + gateway }) -%}
          {%- if drn_interface is defined -%}
          {%- set _ = netip.update({'ipconfig1': 'ip=' + drn_interface['ip_address'] + drn_net['netmask']}) -%}
          {%- endif -%}
          {{ netip }}

    - name: Create VM
      community.proxmox.proxmox_kvm:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        node: "{{ infrastructure_proxmox_qemu_proxmox_node }}"
        # Basic VM info
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        name: "{{ infrastructure_proxmox_qemu_host_name }}"
        description: |
          "{{ infrastructure_proxmox_qemu_description }}"
        ostype: l26 # See https://docs.ansible.com/ansible/latest/collections/community/general/proxmox_kvm_module.html
        # Hardware info
        memory: "{{ infrastructure_proxmox_qemu_memory_size | default('2048') }}"
        cores: "{{ infrastructure_proxmox_qemu_cores | default('2') }}"
        cpu: host
        scsihw: virtio-scsi-pci
        ide:
          ide2: "{{ infrastructure_proxmox_qemu_storage }}:cloudinit,format=raw"
        vga: qxl
        boot: order=scsi0
        net:
          "{{ net_if }}"
        ipconfig:
          "{{ net_ip }}"
        citype: nocloud
        cicustom: "user={{ cloud_init_location }}:snippets/{{ infrastructure_proxmox_qemu_id }}-cloudinit.yaml"
        agent: "enabled=1"
        tags: "{{ tags }}"
        validate_certs: false
        # Desired state
        state: present

    # Import the cloud-init disk from the template VM
    - name: Import cloud-init disk
      community.proxmox.proxmox_disk:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        disk: scsi0
        import_from: "{{ template_location }}{{ template }}"
        storage: "{{ infrastructure_proxmox_qemu_storage }}"
        format: raw
        validate_certs: false
        state: present

    - name: Start VM and run auto config (cloudinit)
      community.proxmox.proxmox_kvm:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        validate_certs: false
        state: started

    - name: Wait for ssh to be ready
      ansible.builtin.wait_for:
        port: 22
        host: "{{ infrastructure_proxmox_qemu_host_name }}"
        delay: 10
        state: started
        timeout: 120

    - name: Sleep for a number of seconds
      ansible.builtin.pause:
        seconds: "{{ infrastructure_proxmox_qemu_wait_for_cloudinit }}"

    - name: Check the cloud-init status            # noqa: var-naming[no-role-prefix]
      ansible.builtin.raw: /usr/bin/cloud-init status
      register: _cloudinit
      until: |
        (_cloudinit.stdout is defined) and
        ("'done' in _cloudinit.stdout")
      retries: 10
      delay: 10
      delegate_to: "{{ inventory_hostname }}"
      become: true
      changed_when: false

    - name: Add the lvm packages          # noqa: command-instead-of-module
      ansible.builtin.command:
        argv:
          - /usr/bin/dnf
          - install
          - lvm2
          - --assumeyes
      changed_when: true
      become: true
      delegate_to: "{{ inventory_hostname }}"

    - name: Grow root disk
      community.proxmox.proxmox_disk:            # noqa: var-naming[no-role-prefix]
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        validate_certs: false
        disk: scsi0
        size: "{{ root_disk_size }}G"
        state: resized
      register: _resize_root
      when:
        - root_disk_size is defined

    - name: Re-read partition table on /dev/sda
      ansible.builtin.raw: /usr/sbin/sgdisk -e /dev/sda;/usr/sbin/partprobe /dev/sda           # noqa: var-naming[no-role-prefix]
      become: true
      changed_when: false
      delegate_to: "{{ inventory_hostname }}"

    - name: Select last partition on /dev/sda
      ansible.builtin.raw: /usr/sbin/parted /dev/sda print|awk NF |tail -1|cut -d' ' -f2           # noqa: var-naming[no-role-prefix]
      become: true
      changed_when: false
      register: _partnum
      delegate_to: "{{ inventory_hostname }}"

    - name: Resize the root partition             # noqa: no-handler
      ansible.builtin.raw: "growpart /dev/sda {{ _partnum.stdout | quote }}"
      become: true
      changed_when: true
      when: _resize_root.changed
      delegate_to: "{{ inventory_hostname }}"

    - name: Resize xfs filesystem when not a LVM based system            # noqa: no-handler
      ansible.builtin.raw: "xfsgrow_fs /dev/sda{{ _partnum.stdout | quote }}"
      become: true
      changed_when: true
      when:
        - _resize_root.changed
        - hostvars[inventory_hostname]['ansible_facts']['devices']['dm-0'] is not defined
      delegate_to: "{{ inventory_hostname }}"

    - name: Get LV name for root
      ansible.builtin.raw: /usr/sbin/lvs | grep root         # noqa: var-naming[no-role-prefix]
      become: true
      changed_when: false
      register: _lv_list
      delegate_to: "{{ inventory_hostname }}"

    - name: Resize root LV in case of LVM
      ansible.builtin.raw: "lvextend -r -l +100%FREE /dev/mapper/{{ _lv_list.stdout.split()[1] | quote }}-{{ _lv_list.stdout.split()[0] | quote }}"
      become: true
      changed_when: true
      when:
        - _resize_root.changed
        - hostvars[inventory_hostname]['ansible_facts']['devices']['dm-0'] is defined
      delegate_to: "{{ inventory_hostname }}"

    - name: Create new disk in VM (do not rewrite in case it exists already)
      community.proxmox.proxmox_disk:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        validate_certs: false
        disk: "{{ disk['name'] }}"
        backup: true
        cache: none
        storage: "{{ infrastructure_proxmox_qemu_storage }}"
        size: "{{ disk['size'] }}"
        state: present
      loop: "{{ disks }}"
      loop_control:
        loop_var: disk
      when: disks is defined

    - name: Stop VM after config
      community.proxmox.proxmox_kvm:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        validate_certs: false
        state: stopped

    # Remove cloudinit disk and reboot
    - name: Remove cdrom device
      community.proxmox.proxmox_disk:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        disk: ide2
        validate_certs: false
        state: absent

    - name: Start the vm again
      community.proxmox.proxmox_kvm:
        api_host: "{{ infrastructure_proxmox_qemu_proxmox_api_node }}"
        api_user: "{{ proxmox_user }}"
        api_password: "{{ proxmox_password | default(omit) }}"
        api_port: "{{ proxmox_api_port }}"
        vmid: "{{ infrastructure_proxmox_qemu_id }}"
        validate_certs: false
        state: started

    - name: Wait for ssh to be ready
      ansible.builtin.wait_for:
        port: 22
        host: "{{ infrastructure_proxmox_qemu_host_name }}"
        delay: 10
        state: started
        timeout: 120

    - name: Sleep for 10 secs to let host start, before next step
      ansible.builtin.pause:
        seconds: 10

templates/cloud-init.yaml.j2

This is where you insert the configuration into the VM template.

#cloud-config

{% if rh_subscription is defined %}
rh_subscription:
  username: {{ rh_subscription.username }}
  password: {{ rh_subscription.password }}
{% endif %}

hostname: {{ infrastructure_proxmox_qemu_host_name }}
timezone: 'Europe/Amsterdam'

users:
  - name: ansible
    sudo: "ALL=(ALL)   NOPASSWD  :ALL"
    ssh_authorized_keys:
      - {{ ssh_pub_key }}

manage_etc_hosts: localhost
fqdn: {{ infrastructure_proxmox_qemu_host_name }}

chpasswd:
  expire: False

config:
  - type: physical
    name: eth0
  - type: nameserver
    address:
      - {{ dns_servers }}
    search:
      - {{ dns_domain }}

{% if cloud_packages is defined %}
packages: {{ cloud_packages }}
{% endif %}
package_update: false
package_upgrade: false

ssh_pwauth: false

{% if cloud_commands is defined %}
cloud_config_modules:
  - runcmd

cloud_final_modules:
  - scripts-user

runcmd: {{ cloud_commands }}
{% endif %}

role_infrastructure_lvm

Always add additional filesystems as LVM devices, this simplifies management. This role will create the LVM devices and mountpoints, it ensures the order of creation and mount ing is correct. This play will work on almost any linux distribution.

README.md

#Infrastructure LVM role
This playbook reads the LVM (<http://www.tldp.org/HOWTO/LVM-HOWTO/>) definition from the host variables of an instance and sets up the physical volumes, volume groups and logical volumes accordingly.

General rule for this role: you can only use this playbook to *create* and *extend* disk space in logical volumes. Shrinking and removing disks is **not** supported.


__Requirements__
This playbook cannot be called without a defined `instances` variable. 
If the variable `disks` is not defined in the hostvars of vm, the play will end. 
If `disks` is present, `lvm` is also expected and the play will continue. If `lvm` is not present and `lvm` is, the play will fail.

The minimal version to run this playbook is ANSIBLE 2.8, as some tasks use options that are not availliable in lower versions of ansible. 

__Usage__
In order for this playbook to start making changes, the following items are required:

an `instances` variable (list), filled with hostnames in FQDN, in example:
* node1.domain
* node2.domain

An LVM definition in the host variables (additional disks and pv/vg/lv definitions), in example:
```yml
disks:
- size: 110
  vg: oracle
hostname: ora-hb-1.localdomain
lvm:
  - lv_name: u01
    mountpoint: /u01
    size: '40'
    vg_name: oracle
  - lv_name: oradata
    mountpoint: /u01/app/oracle/oradata
    size: '20'
    vg_name: oracle
  - lv_name: orafra
    mountpoint: /u01/app/oracle/fast_recovery_area
    size: '30'
    vg_name: oracle
  - lv_name: oraredo
    mountpoint: /u01/app/oracle/oraredo
    size: '20'
    vg_name: oracle
````
The definition snippet above will result in:
* one disks of 110GB attached to the virtual machine, all partitioned to contain a single physical volume
* one volume group (oracle on the physical volume)
* four logical volumes that use 100% of the volume group size (oracle_u01 with size 40GB, oracle_oradata with size 20GB, oracle_oraredo with size 20GB and oracle_orafra with size of 30GB)
* A formatted XFS filesystem on all logical volumes
* Creation of mount points on root filesystem
* Modification of /etc/fstab so that the volumes are mounted on boot
* All logical volumes are mounted on the specified mountpoint

The disk size can be:
* 100%: The logical volume will use all available space in the volume group. When this setting is used the volume group **must** contain only **one** logical volume
* an integer: the created logical volume will have a disk size of the provided integer in GB's. So if size is defined as followed: `"size": 2` then the resulting logical volume will have a size of 2GB.

The mountpoint is the location on the root filesystem where the disk will be mounted (in example, /data).
###### Mandatory keys
Both the disks and the lvm keys and their definitions are mandatory for this playbook to work. The minimal working definition consists of a single disk and a single LV on that disk.


__Role Variables__
This playbook does not use default variables.

__Dependencies__
This playbook is not dependent on Ansible Galaxy roles.

__License__
tbd

__Author Information__
Wilco Folkers

meta/main.yml

---
# This file is required for the requirements.yaml mechanism to work
galaxy_info:
  role_name: infrastructure_lvm
  namespace: wilcofolkers
  description: This creates the LVM disks for the vm
  author: Wilco Folkers
  license: BSD
  min_ansible_version: '2.9'
  platforms:
    - name: EL
      versions: [all]

tasks/main.yml

The playbook of this role that will take the definitions from the host_vars and create LVM devices and mountpoints.
It will then mount all devices as requested in the correct order.

---
- name: Run as block
  when:
    - disks is defined
    - lvm is defined
  block:
    #
    # Define the list of disk names. Within the VMWare definition it is not possible to define disks by name, so we need to have
    # the correlation somewhere. The N'th additional disk in instance defintion has name diskname[N], so the first disk has name
    # /dev/sdb, and so on
    #
    - name: Define list of disk names
      ansible.builtin.set_fact:         # noqa: var-naming[no-role-prefix]
        disknames:
          - '/dev/sdb'
          - '/dev/sdc'
          - '/dev/sdd'
          - '/dev/sde'
          - '/dev/sdf'
          - '/dev/sdg'
          - '/dev/sdh'
          - '/dev/sdi'
          - '/dev/sdj'
          - '/dev/sdk'
          - '/dev/sdl'
          - '/dev/sdm'
          - '/dev/sdn'
          - '/dev/sdo'
          - '/dev/sdp'
    #
    # Create LVM partitions on the additional disks. with_indexed items returns the index of the item and the item itself, so
    # single_disk wil contain:
    #
    # single_disk.0 = index value (0 for the first additional disk, 1 for the second, and so on)
    # single_disk.1 = disk list item (not used here, but used later in the play)
    #
    - name: Use parted to create LVM partitions of all referenced disks on this host
      become: true
      loop_control:
        loop_var: single_disk
      community.general.parted:
        device: "{{ disknames[single_disk.0] }}"
        number: 1
        flags: [lvm]
        state: present
      with_indexed_items:
        - "{{ disks }}"

    - name: Create dictionary string with all referenced volume groups with their disks
      ansible.builtin.set_fact:                  # noqa: var-naming[no-role-prefix]
        _vgs: |-
          {%- for d in disks -%}
            {{ d['vg'] }},{{ disknames[loop.index0] }}1
          {%- if not loop.last -%};
          {%- endif -%}
          {%- endfor -%}

    - name: Get the list of already created vgs
      ansible.builtin.command: 'lsblk -lno NAME'
      register: _active_vgs                 # noqa: var-naming[no-role-prefix]
      changed_when: false

    - name: Create the VG's
      become: true
      community.general.lvg:
        vg: "{{ lv.split(',')[0] }}"
        pvs: "{{ lv.split(',')[-1] }}"
      loop: "{{ _vgs | split(';') }}"
      loop_control:
        loop_var: lv
      when: lv.split(',')[0] not in _active_vgs.stdout

    #
    # Create the lv's in the inventory file
    #

    - name: Get active_mountpoints
      ansible.builtin.command: 'lsblk -lno MOUNTPOINT'
      register: _active_mounts                    # noqa: var-naming[no-role-prefix]
      changed_when: false

    - name: Run as block using admin rights
      become: true
      block:

        - name: Create logical volume and update them when they are defined as 100%
          community.general.lvol:
            lv: "{{ lv_lvol.lv_name }}"
            shrink: false
            size: |-
              {%- if lv_lvol.size == '100%' -%}100%VG{%- else -%}{{ lv_lvol.size }}G{%- endif -%}
            vg: "{{ lv_lvol.vg_name }}"
          loop_control:
            loop_var: lv_lvol
          loop: "{{ lvm }}"
          when: lv_lvol.mountpoint not in _active_mounts.stdout
        #
        # Format volume. vg is the volume group name, single_lv.key is the lv name. Resize the filesystem when required (only
        # relevant on updates)
        #
        - name: Format the logical volume
          community.general.filesystem:
            dev: "/dev/{{ lv_fmt.vg_name }}/{{ lv_fmt.lv_name }}"
            force: false
            fstype: xfs
            resizefs: true
          loop_control:
            loop_var: lv_fmt
          loop: "{{ lvm }}"
          when: lv_fmt.mountpoint not in _active_mounts.stdout
        #
        # Mount volume
        #
        - name: Mount the logical volume
          ansible.posix.mount:
            fstype: xfs
            path: "{{ lv_mnt.mountpoint }}"
            src: "/dev/{{ lv_mnt.vg_name }}/{{ lv_mnt.lv_name }}"
            state: mounted
          loop_control:
            loop_var: lv_mnt
          loop: "{{ lvm }}"
          when: lv_mnt.mountpoint not in _active_mounts.stdout