Intro

Creating cloud-init enabled templates is a great and easy way to deploy future nodes in my homelab. I created a small ansible playbook to automatically do this for me using a few variables to customize the image.

Finding the images

Most of the distros have a cloud image that can be used which contains cloud-init already. I only use these kinds of images so that makes my life a little easier. Using variables makes this pretty modular. Example for Arch:

# Image
image_name: Arch-Linux-x86_64-cloudimg.qcow2
image_link: "https://geo.mirror.pkgbuild.com/images/latest/{{ image_name }}"

Creating the playbook

The first parts of the playbook will check if the image or vm_id already exist:

    - name: Check if image already exists
      ansible.builtin.stat:
        path: "/home/root/disks/{{ image_name }}"
      register: image_location

    - name: Check if vm_id already exists
      ansible.builtin.shell: "qm list | grep {{ vm_id }} | cut -d ' ' -f 8"
      ignore_errors: true
      register: id_check
      changed_when: false

If the image does not yet exist it will be downloaded:

    - name: Download image
      ansible.builtin.get_url:
        url: "{{ image_link }}"
        dest: /home/root/disks/
        mode: '0644'
      when: not image_location.stat.exists

And once that’s done, the template gets created:

    - name: Create template
      when: id_check.stdout == ""
      block:
        - name: Create temp VM
          ansible.builtin.command: "{{ item }}"
          loop:
            - "qm create {{ vm_id }} --name {{ vm_name }} --memory {{ vm_ram }} --net0 {{ vm_network }}"
            - "qm importdisk {{ vm_id }} /home/root/disks/{{ image_name }} local-lvm"
            - "qm set {{ vm_id }} --scsihw {{ vm_scsihw }} --scsi0 {{ vm_scsi0 }}"
            - "qm set {{ vm_id }} --sockets {{ vm_cpu_sockets }} --cores {{ vm_cpu_cores }}"
            - "qm set {{ vm_id }} --ide2 {{ vm_ide2 }}"
            - "qm set {{ vm_id }} --boot order={{ vm_bootdisk }}"
            - "qm set {{ vm_id }} --serial0 {{ vm_serial0 }} --vga {{ vm_vga }}"
            - "qm set {{ vm_id }} --ipconfig0 ip={{ vm_ip }}"
            - "qm set {{ vm_id }} --ciuser {{ user }}"
            - "qm set {{ vm_id }} --sshkeys {{ ssh_key }}"
          changed_when: false

        - name: Convert to template
          ansible.builtin.command: "{{ item }}"
          loop:
            - "qm resize {{ vm_id }} {{ vm_bootdisk }} {{ vm_disksize }}"
            - "qm template {{ vm_id }}"
          changed_when: false

Most of the options are using variables, there are even more options for extra customization of the template but I kept it pretty basic.

Full files

Variables:

---
# Default values

# Image
image_name: Arch-Linux-x86_64-cloudimg.qcow2
image_link: "https://geo.mirror.pkgbuild.com/images/latest/{{ image_name }}"

# VM
vm_id: 906
vm_name: archlinux

# Settings
vm_ram: 2048
vm_network: virtio,bridge=vmbr0
vm_cpu_sockets: 1
vm_cpu_cores: 2
vm_scsihw: virtio-scsi-pci
vm_scsi0: "local-lvm:vm-{{ vm_id }}-disk-0"
vm_ide2: local-lvm:cloudinit
vm_boot: C
vm_bootdisk: scsi0
vm_serial0: socket
vm_vga: serial0
vm_ip: dhcp
vm_disksize: 50G
user: rein
ssh_key: "~/.ssh/authorized_keys"

Playbook:

---
# Playbook

- name: Proxmox template
  hosts: proxmox

  vars_files:
    - defaults.yml

  tasks:
    - name: Make sure image directory exists
      ansible.builtin.file:
        path: /home/root/disks/
        state: directory
        mode: '0755'

    - name: Check if image already exists
      ansible.builtin.stat:
        path: "/home/root/disks/{{ image_name }}"
      register: image_location

    - name: Check if vm_id already exists
      ansible.builtin.shell: "qm list | grep {{ vm_id }} | cut -d ' ' -f 8"
      ignore_errors: true
      register: id_check
      changed_when: false

    - name: Download image
      ansible.builtin.get_url:
        url: "{{ image_link }}"
        dest: /home/root/disks/
        mode: '0644'
      when: not image_location.stat.exists

    - name: Create template
      when: id_check.stdout == ""
      block:
        - name: Create temp VM
          ansible.builtin.command: "{{ item }}"
          loop:
            - "qm create {{ vm_id }} --name {{ vm_name }} --memory {{ vm_ram }} --net0 {{ vm_network }}"
            - "qm importdisk {{ vm_id }} /home/root/disks/{{ image_name }} local-lvm"
            - "qm set {{ vm_id }} --scsihw {{ vm_scsihw }} --scsi0 {{ vm_scsi0 }}"
            - "qm set {{ vm_id }} --sockets {{ vm_cpu_sockets }} --cores {{ vm_cpu_cores }}"
            - "qm set {{ vm_id }} --ide2 {{ vm_ide2 }}"
            - "qm set {{ vm_id }} --boot order={{ vm_bootdisk }}"
            - "qm set {{ vm_id }} --serial0 {{ vm_serial0 }} --vga {{ vm_vga }}"
            - "qm set {{ vm_id }} --ipconfig0 ip={{ vm_ip }}"
            - "qm set {{ vm_id }} --ciuser {{ user }}"
            - "qm set {{ vm_id }} --sshkeys {{ ssh_key }}"
          changed_when: false

        - name: Convert to template
          ansible.builtin.command: "{{ item }}"
          loop:
            - "qm resize {{ vm_id }} {{ vm_bootdisk }} {{ vm_disksize }}"
            - "qm template {{ vm_id }}"
          changed_when: false