From 498018d3d7f452ee3e3966c7d847ffb9470eecf0 Mon Sep 17 00:00:00 2001 From: Holger Mueller Date: Mon, 8 Jun 2026 21:32:55 +0200 Subject: [PATCH] Initial commit --- ansible-kvm-vms/README.md | 83 +++++++++++++++++++ ansible-kvm-vms/ansible.cfg | 4 + ansible-kvm-vms/group_vars/all.yml | 13 +++ ansible-kvm-vms/inventory | 2 + ansible-kvm-vms/playbooks/create_vms.yml | 30 +++++++ .../roles/kvm_host_setup/tasks/main.yml | 61 ++++++++++++++ .../roles/os_config/tasks/main.yml | 12 +++ .../os_config/templates/ignition.json.j2 | 26 ++++++ .../os_config/templates/user-data.yaml.j2 | 8 ++ .../roles/vm_provision/tasks/main.yml | 69 +++++++++++++++ ansible-kvm-vms/vars/vms.yml | 22 +++++ 11 files changed, 330 insertions(+) create mode 100644 ansible-kvm-vms/README.md create mode 100644 ansible-kvm-vms/ansible.cfg create mode 100644 ansible-kvm-vms/group_vars/all.yml create mode 100644 ansible-kvm-vms/inventory create mode 100644 ansible-kvm-vms/playbooks/create_vms.yml create mode 100644 ansible-kvm-vms/roles/kvm_host_setup/tasks/main.yml create mode 100644 ansible-kvm-vms/roles/os_config/tasks/main.yml create mode 100644 ansible-kvm-vms/roles/os_config/templates/ignition.json.j2 create mode 100644 ansible-kvm-vms/roles/os_config/templates/user-data.yaml.j2 create mode 100644 ansible-kvm-vms/roles/vm_provision/tasks/main.yml create mode 100644 ansible-kvm-vms/vars/vms.yml diff --git a/ansible-kvm-vms/README.md b/ansible-kvm-vms/README.md new file mode 100644 index 0000000..c49ec16 --- /dev/null +++ b/ansible-kvm-vms/README.md @@ -0,0 +1,83 @@ +# Ansible KVM Immutable OS Provisioner + +This project provides an Ansible-based framework to automatically provision virtual machines using KVM on a Linux host. It specifically targets immutable operating systems: **Fedora CoreOS**, **Flatcar Container Linux**, and **openSUSE MicroOS**. + +## 🚀 Features + +- **Automated Host Setup**: Installs and configures `libvirt`, `qemu-kvm`, and `libguestfs-tools`. +- **Immutable OS Support**: Handles the specific boot-time configuration requirements for: + - **CoreOS/Flatcar**: Generates and injects Ignition JSON configurations. + - **MicroOS**: Generates and injects Cloud-init user-data. +- **Custom User Provisioning**: Automatically creates a default user with a hashed password and injects your SSH public key. +- **Modular Design**: Uses Ansible roles for host preparation, configuration generation, and VM provisioning. + +## 📂 Project Structure + +```text +ansible-kvm-vms/ +├── inventory # Defines the KVM host (defaults to localhost) +├── group_vars/ +│ └── all.yml # Global settings: user, password, and SSH key path +├── vars/ +│ └── vms.yml # List of VMs to create with CPU, RAM, and Disk specs +├── roles/ +│ ├── kvm_host_setup/ # Installs virtualization dependencies on the host +│ ├── os_config/ # Generates Ignition/Cloud-init config files +│ └── vm_provision/ # Downloads images and creates VMs via virt-install +└── playbooks/ + └── create_vms.yml # Main orchestration playbook +``` + +## 🛠 Prerequisites + +Before running the playbooks, ensure the following: + +1. **Hardware Virtualization**: Enabled in your BIOS/UEFI (VT-x or AMD-V). +2. **Ansible**: Installed on your control node. +3. **Sudo Access**: The user running the playbook must have sudo privileges on the KVM host. +4. **SSH Key**: You should have an SSH public key generated (usually at `~/.ssh/id_vms.pub`). + +## ⚙️ Configuration + +### 1. Global Settings +Edit `group_vars/all.yml` to set your desired credentials: +- `vm_user`: The username for the VM. +- `vm_password`: The password for the user (will be hashed automatically). +- `vm_ssh_public_key`: The absolute path to your `.pub` key file. + +### 2. VM Definitions +Edit `vars/vms.yml` to add or modify the VMs you wish to deploy. You can specify: +- `name`: Unique name for the VM. +- `os_type`: One of `coreos`, `flatcar`, or `microos`. +- `os_variant`: The `virt-install` OS variant string. +- `cpu`, `ram`, `disk`: Resource allocations. + +## 📖 Usage + +1. **Navigate to the project directory**: + ```bash + cd ansible-kvm-vms + ``` + +2. **Run the deployment playbook**: + ```bash + ansible-playbook -i inventory playbooks/create_vms.yml --ask-become-pass + ``` + +## 🔍 How it Works + +Since immutable OSs do not use traditional installers, this setup uses a "seed" approach: +1. **Config Generation**: The `os_config` role creates a JSON (Ignition) or YAML (Cloud-init) file based on your variables. +2. **Image Customization**: The `vm_provision` role downloads the official `.qcow2` cloud image and uses `virt-customize` (from `libguestfs-tools`) to inject the configuration directly into the disk image before the VM is started. +3. **Deployment**: `virt-install` is used to create the VM with UEFI boot and the customized disk. + +## 🌐 Accessing your VMs + +The VMs are created on the default KVM NAT network. To find the IP address of your new VMs, run: +```bash +sudo virsh net-dhcp-leases-all default +``` +Then SSH into them using your configured user: +```bash +ssh kvmuser@ +``` diff --git a/ansible-kvm-vms/ansible.cfg b/ansible-kvm-vms/ansible.cfg new file mode 100644 index 0000000..e3f35f3 --- /dev/null +++ b/ansible-kvm-vms/ansible.cfg @@ -0,0 +1,4 @@ +[defaults] +inventory = inventory +roles_path = ./roles +host_key_checking = False diff --git a/ansible-kvm-vms/group_vars/all.yml b/ansible-kvm-vms/group_vars/all.yml new file mode 100644 index 0000000..eab1e10 --- /dev/null +++ b/ansible-kvm-vms/group_vars/all.yml @@ -0,0 +1,13 @@ +# Global VM settings +ansible_python_interpreter: /usr/bin/python3 +vm_user: "kvmuser" +vm_password: "Password123!" # In a real scenario, use ansible-vault to encrypt this +vm_ssh_public_key: "~/.ssh/id_vms.pub" # Path to your public key for SSH access + +# Default VM resources +default_cpu: 2 +default_ram: 2048 +default_disk: "20G" + +# Storage path for images +vm_images_dir: "/var/lib/libvirt/images" diff --git a/ansible-kvm-vms/inventory b/ansible-kvm-vms/inventory new file mode 100644 index 0000000..a7cb2d0 --- /dev/null +++ b/ansible-kvm-vms/inventory @@ -0,0 +1,2 @@ +[kvm_hosts] +localhost ansible_connection=local diff --git a/ansible-kvm-vms/playbooks/create_vms.yml b/ansible-kvm-vms/playbooks/create_vms.yml new file mode 100644 index 0000000..751e717 --- /dev/null +++ b/ansible-kvm-vms/playbooks/create_vms.yml @@ -0,0 +1,30 @@ +--- +- name: Setup KVM VMs + hosts: all + become: yes + vars_files: + - ../vars/vms.yml + + roles: + - kvm_host_setup + + tasks: + - name: Provision each VM + include_role: + name: os_config + vars: + vm_name: "{{ item.name }}" + os_type: "{{ item.os_type }}" + loop: "{{ vms }}" + + - name: Launch each VM + include_role: + name: vm_provision + vars: + vm_name: "{{ item.name }}" + os_type: "{{ item.os_type }}" + os_variant: "{{ item.os_variant }}" + cpu: "{{ item.cpu }}" + ram: "{{ item.ram }}" + disk: "{{ item.disk }}" + loop: "{{ vms }}" diff --git a/ansible-kvm-vms/roles/kvm_host_setup/tasks/main.yml b/ansible-kvm-vms/roles/kvm_host_setup/tasks/main.yml new file mode 100644 index 0000000..1a7218e --- /dev/null +++ b/ansible-kvm-vms/roles/kvm_host_setup/tasks/main.yml @@ -0,0 +1,61 @@ + +--- + +- name: Install KVM and virtualization tools + package: + name: + - qemu-kvm + - libvirt-daemon-system + - libvirt-clients + - bridge-utils + - virtinst + - virt-manager + - libguestfs-tools + - xz-utils + state: present + when: ansible_facts['os_family'] == "Debian" + +- name: Install KVM and virtualization tools (RedHat/Fedora) + package: + name: + - qemu-kvm + - libvirt + - virt-install + - virt-manager + - libguestfs-tools + - xz + state: present + when: ansible_facts['os_family'] == "RedHat" + +- name: Install KVM and virtualization tools (Arch/CachyOS) + package: + name: + - qemu-full + - libvirt + - virt-manager + - iproute2 + - libguestfs + - guestfs-tools + - xz + state: present + when: ansible_facts['os_family'] == "Archlinux" + +- name: Ensure libvirtd is started and enabled + service: + name: libvirtd + state: started + enabled: yes + +- name: Ensure KVM default network is active and autostarts + shell: | + virsh net-start default || true + virsh net-autostart default || true + become: yes + +- name: Add current user to libvirt group + user: + name: "{{ ansible_facts['user_id'] }}" + groups: libvirt + append: yes + become: yes + ignore_errors: yes # Some distros use different group names (e.g., kvm) diff --git a/ansible-kvm-vms/roles/os_config/tasks/main.yml b/ansible-kvm-vms/roles/os_config/tasks/main.yml new file mode 100644 index 0000000..af1439c --- /dev/null +++ b/ansible-kvm-vms/roles/os_config/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Generate Ignition config for CoreOS/Flatcar + template: + src: ignition.json.j2 + dest: "/tmp/{{ vm_name }}_ignition.json" + when: os_type == "coreos" or os_type == "flatcar" + +- name: Generate Cloud-init config for MicroOS + template: + src: user-data.yaml.j2 + dest: "/tmp/{{ vm_name }}_user-data" + when: os_type == "microos" diff --git a/ansible-kvm-vms/roles/os_config/templates/ignition.json.j2 b/ansible-kvm-vms/roles/os_config/templates/ignition.json.j2 new file mode 100644 index 0000000..8483a29 --- /dev/null +++ b/ansible-kvm-vms/roles/os_config/templates/ignition.json.j2 @@ -0,0 +1,26 @@ +{ + "ignition": { + "version": "0.3.0" + }, + "passwd": { + "users": [ + { + "name": "{{ vm_user }}", + "password_hash": "{{ vm_password | password_hash('sha512') }}", + "ssh_public_keys": [ + "{{ lookup('file', vm_ssh_public_key) }}" + ] + } + ] + }, + "storage": { + "files": [ + { + "path": "/etc/ssh/sshd_config.d/permit_root_login.conf", + "contents": { + "source": "data:text/plain;charset=utf-8,PermitRootLogin yes" + } + } + ] + } +} diff --git a/ansible-kvm-vms/roles/os_config/templates/user-data.yaml.j2 b/ansible-kvm-vms/roles/os_config/templates/user-data.yaml.j2 new file mode 100644 index 0000000..da7496b --- /dev/null +++ b/ansible-kvm-vms/roles/os_config/templates/user-data.yaml.j2 @@ -0,0 +1,8 @@ +#cloud-config +users: + - name: {{ vm_user }} + passwd: {{ vm_password | password_hash('sha512') }} + ssh_authorized_keys: + - {{ lookup('file', vm_ssh_public_key) }} + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: false diff --git a/ansible-kvm-vms/roles/vm_provision/tasks/main.yml b/ansible-kvm-vms/roles/vm_provision/tasks/main.yml new file mode 100644 index 0000000..aaaa889 --- /dev/null +++ b/ansible-kvm-vms/roles/vm_provision/tasks/main.yml @@ -0,0 +1,69 @@ +--- + +- name: Define image URLs + set_fact: + os_images: + coreos: "https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/44.20260510.3.1/x86_64/fedora-coreos-44.20260510.3.1-qemu.x86_64.qcow2.xz" + flatcar: "https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_qemu_uefi_image.img" + microos: "https://ftp.halifax.rwth-aachen.de/opensuse/tumbleweed/appliances/openSUSE-MicroOS.x86_64-kvm-and-xen.qcow2" + +- name: Verify internet connectivity + uri: + url: http://google.com + return_content: no + timeout: 10 + +- name: Download OS image + get_url: + url: "{{ os_images[os_type] }}" + dest: "{{ vm_images_dir }}/{{ vm_name }}.download" + mode: '0644' + become: yes + +- name: Handle compressed or raw images + shell: | + DOWNLOAD_FILE="{{ vm_images_dir }}/{{ vm_name }}.download" + FINAL_FILE="{{ vm_images_dir }}/{{ vm_name }}.qcow2" + + # 1. Handle XZ compression + if [[ "{{ os_images[os_type] }}" == *.xz ]]; then + echo "Decompressing XZ image..." + unxz -c "$DOWNLOAD_FILE" > "$FINAL_FILE" + elif [[ "{{ os_images[os_type] }}" == *.img ]]; then + echo "Converting RAW image to QCOW2..." + qemu-img convert -f raw -O qcow2 "$DOWNLOAD_FILE" "$FINAL_FILE" + else + echo "Moving QCOW2 image to final destination..." + mv "$DOWNLOAD_FILE" "$FINAL_FILE" + fi + rm -f "$DOWNLOAD_FILE" + become: yes + args: + creates: "{{ vm_images_dir }}/{{ vm_name }}.qcow2" + +- name: Provision VM using virt-install + shell: | + virt-install \ + --name {{ vm_name }} \ + --vcpus {{ cpu | default(default_cpu) }} \ + --memory {{ ram | default(default_ram) }} \ + --disk path={{ vm_images_dir }}/{{ vm_name }}.qcow2,bus=virtio \ + --import \ + --os-variant {{ os_variant }} \ + --network network=default \ + --graphics none \ + --noautoconsole \ + --boot uefi \ + {% if os_type == 'coreos' or os_type == 'flatcar' %} + --cloud-init user-data=/tmp/{{ vm_name }}_ignition.json + {% elif os_type == 'microos' %} + --cloud-init user-data=/tmp/{{ vm_name }}_user-data + {% endif %} + args: + creates: "/etc/libvirt/qemu/{{ vm_name }}.xml" + +- name: Attach configuration to VM + debug: + msg: "Configuration is now handled by virt-install --cloud-init flag." + when: false # This task is now obsolete + become: yes diff --git a/ansible-kvm-vms/vars/vms.yml b/ansible-kvm-vms/vars/vms.yml new file mode 100644 index 0000000..ae3ec73 --- /dev/null +++ b/ansible-kvm-vms/vars/vms.yml @@ -0,0 +1,22 @@ +--- +vms: + - name: coreos-vm + os_type: coreos + os_variant: "fedora-coreos-stable" + cpu: 2 + ram: 2048 + disk: "20G" + + - name: flatcar-vm + os_type: flatcar + os_variant: "fedora-coreos-stable" + cpu: 2 + ram: 2048 + disk: "20G" + + - name: microos-vm + os_type: microos + os_variant: "opensusemicroos" + cpu: 2 + ram: 2048 + disk: "20G"