From a5337029ff656b9c8abfe260b74a3d65afeccac7 Mon Sep 17 00:00:00 2001 From: Infrastructure Team Date: Mon, 10 Nov 2025 22:51:30 +0100 Subject: [PATCH] Add multi-distribution VM deployment playbooks - Add deploy-debian12-vm.yml for basic Debian 12 deployment - Add deploy-linux-vm.yml for multi-distribution support - Support for Debian, Ubuntu, RHEL, CentOS, Rocky, Alma, SUSE - Cloud-init based provisioning - Distribution-specific security hardening - Automatic security updates configuration - UFW/firewalld setup per OS family - SELinux enforcing for RHEL family --- plays/deploy-debian12-vm.yml | 479 +++++++++++++++++ plays/deploy-linux-vm.yml | 976 +++++++++++++++++++++++++++++++++++ 2 files changed, 1455 insertions(+) create mode 100644 plays/deploy-debian12-vm.yml create mode 100644 plays/deploy-linux-vm.yml diff --git a/plays/deploy-debian12-vm.yml b/plays/deploy-debian12-vm.yml new file mode 100644 index 0000000..49ec5c9 --- /dev/null +++ b/plays/deploy-debian12-vm.yml @@ -0,0 +1,479 @@ +--- +# ============================================================================= +# Debian 12 VM Deployment Playbook +# ============================================================================= +# Deploys a new Debian 12 guest VM on grokbox KVM hypervisor +# Uses libvirt/KVM with cloud-init for unattended configuration +# ============================================================================= + +- name: Deploy Debian 12 VM on grokbox hypervisor + hosts: grokbox + gather_facts: yes + become: yes + + vars: + # VM Configuration + vm_name: "debian12-guest" + vm_hostname: "debian12" + vm_domain: "localdomain" + vm_vcpus: 2 + vm_memory_mb: 2048 + vm_disk_size_gb: 20 + + # Network Configuration + vm_network: "default" + vm_bridge: "virbr0" + + # Storage Configuration + vm_disk_path: "/var/lib/libvirt/images/{{ vm_name }}.qcow2" + cloud_init_iso_path: "/var/lib/libvirt/images/{{ vm_name }}-cloud-init.iso" + + # Debian 12 Cloud Image + debian_cloud_image_url: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2" + debian_cloud_image_checksum_url: "https://cloud.debian.org/images/cloud/bookworm/latest/SHA512SUMS" + debian_image_cache: "/var/lib/libvirt/images/debian-12-generic-amd64.qcow2" + + # Ansible User Configuration + ansible_user_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILBrnivsqjhAxWYeuuvnYc3neeRRuHsr2SjeKv+Drtpu user@debian" + + tasks: + # ========================================================================= + # Pre-flight Checks + # ========================================================================= + + - name: Check if VM already exists + command: virsh dominfo {{ vm_name }} + register: vm_exists + failed_when: false + changed_when: false + tags: [validate, preflight] + + - name: Fail if VM already exists + fail: + msg: "VM '{{ vm_name }}' already exists on hypervisor. Please choose a different name or destroy the existing VM." + when: vm_exists.rc == 0 + tags: [validate, preflight] + + - name: Verify virtualization support + command: virt-host-validate qemu + register: virt_validation + failed_when: false + changed_when: false + tags: [validate, preflight] + + - name: Display virtualization validation results + debug: + var: virt_validation.stdout_lines + tags: [validate, preflight] + + # ========================================================================= + # Package Installation + # ========================================================================= + + - name: Install required packages for VM deployment + package: + name: + - libvirt-daemon-system + - libvirt-clients + - virtinst + - qemu-kvm + - qemu-utils + - cloud-image-utils + - genisoimage + - wget + - python3-libvirt + state: present + tags: [install] + + - name: Ensure libvirtd service is running + systemd: + name: libvirtd + state: started + enabled: yes + tags: [install] + + # ========================================================================= + # Download Debian Cloud Image + # ========================================================================= + + - name: Check if Debian cloud image already exists + stat: + path: "{{ debian_image_cache }}" + register: debian_image_stat + tags: [download] + + - name: Download Debian 12 cloud image + get_url: + url: "{{ debian_cloud_image_url }}" + dest: "{{ debian_image_cache }}" + mode: '0644' + timeout: 600 + when: not debian_image_stat.stat.exists + tags: [download] + + - name: Download Debian 12 checksums + get_url: + url: "{{ debian_cloud_image_checksum_url }}" + dest: "/tmp/debian-12-SHA512SUMS" + mode: '0644' + tags: [download, verify] + + - name: Verify cloud image checksum + shell: | + cd /var/lib/libvirt/images + grep "debian-12-generic-amd64.qcow2" /tmp/debian-12-SHA512SUMS | sha512sum -c - + register: checksum_result + changed_when: false + tags: [download, verify] + + # ========================================================================= + # Create VM Disk + # ========================================================================= + + - name: Create VM disk from cloud image + command: > + qemu-img create -f qcow2 -F qcow2 + -b {{ debian_image_cache }} + {{ vm_disk_path }} + {{ vm_disk_size_gb }}G + args: + creates: "{{ vm_disk_path }}" + tags: [storage] + + - name: Set proper permissions on VM disk + file: + path: "{{ vm_disk_path }}" + owner: libvirt-qemu + group: kvm + mode: '0600' + tags: [storage] + + # ========================================================================= + # Create Cloud-Init Configuration + # ========================================================================= + + - name: Create cloud-init directory + file: + path: /tmp/cloud-init-{{ vm_name }} + state: directory + mode: '0755' + tags: [cloud-init] + + - name: Create cloud-init meta-data + copy: + content: | + instance-id: {{ vm_name }} + local-hostname: {{ vm_hostname }} + dest: /tmp/cloud-init-{{ vm_name }}/meta-data + mode: '0644' + tags: [cloud-init] + + - name: Create cloud-init user-data + copy: + content: | + #cloud-config + hostname: {{ vm_hostname }} + fqdn: {{ vm_hostname }}.{{ vm_domain }} + manage_etc_hosts: true + + # Create ansible user with sudo privileges + users: + - name: ansible + groups: sudo + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ ansible_user_ssh_key }} + - name: root + lock_passwd: false + + # Set root password (for emergency console access) + chpasswd: + list: | + root:debian + expire: false + + # SSH configuration - secure by default + ssh_pwauth: false + disable_root: false + + # Install essential packages per CLAUDE.md guidelines + packages: + - sudo + - vim + - htop + - tmux + - curl + - wget + - rsync + - git + - python3 + - python3-pip + - jq + - bc + - aide + - auditd + - chrony + - ufw + - lvm2 + - cloud-guest-utils + - parted + - unattended-upgrades + - apt-listchanges + + # Security configuration files + write_files: + - path: /etc/ssh/sshd_config.d/99-security.conf + content: | + PermitRootLogin no + PasswordAuthentication no + PubkeyAuthentication yes + MaxAuthTries 3 + MaxSessions 10 + ClientAliveInterval 300 + ClientAliveCountMax 2 + permissions: '0644' + + - path: /etc/sudoers.d/ansible + content: | + ansible ALL=(ALL) NOPASSWD:ALL + permissions: '0440' + + - path: /etc/apt/apt.conf.d/50unattended-upgrades + content: | + Unattended-Upgrade::Allowed-Origins { + "${distro_id}:${distro_codename}-security"; + }; + Unattended-Upgrade::AutoFixInterruptedDpkg "true"; + Unattended-Upgrade::MinimalSteps "true"; + Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; + Unattended-Upgrade::Remove-Unused-Dependencies "true"; + Unattended-Upgrade::Automatic-Reboot "false"; + permissions: '0644' + + - path: /etc/apt/apt.conf.d/20auto-upgrades + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; + permissions: '0644' + + # System configuration commands + runcmd: + # SSH service + - systemctl enable ssh + - systemctl restart ssh + + # Time synchronization + - systemctl enable chrony + - systemctl start chrony + + # Firewall - enable but allow SSH + - ufw --force enable + - ufw allow ssh + + # Audit daemon + - systemctl enable auditd + - systemctl start auditd + + # Grow root partition + - growpart /dev/vda 1 || true + - resize2fs /dev/vda1 || true + + # Package updates + package_update: true + package_upgrade: true + package_reboot_if_required: false + + # Set timezone + timezone: UTC + + # Locale + locale: en_US.UTF-8 + + # Enable cloud-init logging + output: + all: '| tee -a /var/log/cloud-init-output.log' + + # Final message + final_message: "Debian 12 VM deployment completed. System is ready after $UPTIME seconds." + dest: /tmp/cloud-init-{{ vm_name }}/user-data + mode: '0644' + tags: [cloud-init] + + - name: Create cloud-init ISO + command: > + genisoimage -output {{ cloud_init_iso_path }} + -volid cidata -joliet -rock + /tmp/cloud-init-{{ vm_name }}/user-data + /tmp/cloud-init-{{ vm_name }}/meta-data + args: + creates: "{{ cloud_init_iso_path }}" + tags: [cloud-init] + + - name: Set proper permissions on cloud-init ISO + file: + path: "{{ cloud_init_iso_path }}" + owner: libvirt-qemu + group: kvm + mode: '0644' + tags: [cloud-init] + + # ========================================================================= + # Create and Start VM + # ========================================================================= + + - name: Create VM using virt-install + command: > + virt-install + --name {{ vm_name }} + --memory {{ vm_memory_mb }} + --vcpus {{ vm_vcpus }} + --disk path={{ vm_disk_path }},format=qcow2,bus=virtio + --disk path={{ cloud_init_iso_path }},device=cdrom + --network network={{ vm_network }},model=virtio + --os-variant debian11 + --graphics none + --console pty,target_type=serial + --import + --noautoconsole + register: vm_create + tags: [deploy] + + - name: Wait for VM to boot and cloud-init to complete + pause: + seconds: 60 + prompt: "Waiting for VM to boot and cloud-init to complete configuration..." + tags: [deploy] + + - name: Get VM IP address + shell: | + virsh domifaddr {{ vm_name }} | grep -oP '(\d{1,3}\.){3}\d{1,3}' | head -1 + register: vm_ip + retries: 10 + delay: 10 + until: vm_ip.stdout != "" + changed_when: false + tags: [deploy, validate] + + - name: Display VM information + debug: + msg: + - "=== VM Deployment Successful ===" + - "VM Name: {{ vm_name }}" + - "IP Address: {{ vm_ip.stdout }}" + - "vCPUs: {{ vm_vcpus }}" + - "Memory: {{ vm_memory_mb }} MB" + - "Disk: {{ vm_disk_size_gb }} GB" + - "Access: ssh ansible@{{ vm_ip.stdout }}" + - "" + - "Note: Add this VM to your Ansible inventory:" + - " {{ vm_name }}:" + - " ansible_host: {{ vm_ip.stdout }}" + - " ansible_user: ansible" + - " ansible_ssh_common_args: '-o ProxyJump=grokbox -o StrictHostKeyChecking=accept-new'" + tags: [deploy, validate] + + # ========================================================================= + # Validation and Health Check + # ========================================================================= + + - name: Test SSH connectivity to new VM + wait_for: + host: "{{ vm_ip.stdout }}" + port: 22 + timeout: 300 + state: started + tags: [validate] + + - name: Display VM console log (for troubleshooting) + command: virsh console {{ vm_name }} --force + async: 5 + poll: 0 + failed_when: false + changed_when: false + tags: [never, debug] + + - name: Get VM details + command: virsh dominfo {{ vm_name }} + register: vm_details + changed_when: false + tags: [validate] + + - name: Display VM details + debug: + var: vm_details.stdout_lines + tags: [validate] + + # ========================================================================= + # Cleanup + # ========================================================================= + + - name: Remove temporary cloud-init directory + file: + path: /tmp/cloud-init-{{ vm_name }} + state: absent + tags: [cleanup] + + - name: Remove downloaded checksums + file: + path: /tmp/debian-12-SHA512SUMS + state: absent + tags: [cleanup] + +# ============================================================================= +# Post-Deployment Configuration +# ============================================================================= +# After VM is deployed, run additional configuration playbooks + +- name: Configure deployed VM + hosts: "{{ vm_ip.stdout }}" + gather_facts: yes + become: yes + vars: + ansible_user: ansible + ansible_ssh_common_args: '-o ProxyJump=grokbox -o StrictHostKeyChecking=accept-new' + + tasks: + - name: Wait for cloud-init to complete + command: cloud-init status --wait + changed_when: false + tags: [validate] + + - name: Gather system facts + setup: + tags: [validate] + + - name: Display system information + debug: + msg: + - "=== System Information ===" + - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" + - "Kernel: {{ ansible_kernel }}" + - "Architecture: {{ ansible_architecture }}" + - "Hostname: {{ ansible_hostname }}" + - "FQDN: {{ ansible_fqdn }}" + - "Python: {{ ansible_python_version }}" + tags: [validate] + + - name: Gather disk usage + command: df -h + register: disk_usage + changed_when: false + tags: [validate] + + - name: Display disk usage + debug: + var: disk_usage.stdout_lines + tags: [validate] + + - name: Gather memory usage + command: free -h + register: memory_usage + changed_when: false + tags: [validate] + + - name: Display memory usage + debug: + var: memory_usage.stdout_lines + tags: [validate] diff --git a/plays/deploy-linux-vm.yml b/plays/deploy-linux-vm.yml new file mode 100644 index 0000000..0a9f58a --- /dev/null +++ b/plays/deploy-linux-vm.yml @@ -0,0 +1,976 @@ +--- +# ============================================================================= +# Multi-Distribution Linux VM Deployment Playbook +# ============================================================================= +# Deploys Linux VMs on KVM hypervisor with support for: +# - Debian (11, 12) +# - Ubuntu (20.04 LTS, 22.04 LTS, 24.04 LTS) +# - RHEL (8, 9) +# - CentOS Stream (8, 9) +# - Rocky Linux (8, 9) +# - AlmaLinux (8, 9) +# - SLES (15) +# - openSUSE Leap (15) +# +# Uses libvirt/KVM with cloud-init for unattended configuration +# ============================================================================= + +- name: Deploy Linux VM on KVM hypervisor + hosts: grokbox + gather_facts: yes + become: yes + + vars: + # VM Configuration + vm_name: "linux-guest" + vm_hostname: "linux-vm" + vm_domain: "localdomain" + vm_vcpus: 2 + vm_memory_mb: 2048 + vm_disk_size_gb: 20 + + # Distribution Selection (REQUIRED - set via -e flag) + # Format: "distro-version" or "distro-major.minor" + # Examples: debian-12, ubuntu-22.04, rhel-9, centos-stream-9, sles-15 + os_distribution: "debian-12" + + # Network Configuration + vm_network: "default" + vm_bridge: "virbr0" + + # Storage Configuration + vm_disk_path: "/var/lib/libvirt/images/{{ vm_name }}.qcow2" + cloud_init_iso_path: "/var/lib/libvirt/images/{{ vm_name }}-cloud-init.iso" + + # Ansible User Configuration + ansible_user_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILBrnivsqjhAxWYeuuvnYc3neeRRuHsr2SjeKv+Drtpu user@debian" + + # ========================================================================== + # Cloud Image Repository Configuration + # ========================================================================== + cloud_images: + # Debian + debian-11: + url: "https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-generic-amd64.qcow2" + checksum_url: "https://cloud.debian.org/images/cloud/bullseye/latest/SHA512SUMS" + checksum_type: "sha512" + os_variant: "debian11" + cache_name: "debian-11-generic-amd64.qcow2" + package_manager: "apt" + family: "debian" + + debian-12: + url: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2" + checksum_url: "https://cloud.debian.org/images/cloud/bookworm/latest/SHA512SUMS" + checksum_type: "sha512" + os_variant: "debian12" + cache_name: "debian-12-generic-amd64.qcow2" + package_manager: "apt" + family: "debian" + + # Ubuntu + ubuntu-20.04: + url: "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img" + checksum_url: "https://cloud-images.ubuntu.com/focal/current/SHA256SUMS" + checksum_type: "sha256" + os_variant: "ubuntu20.04" + cache_name: "ubuntu-20.04-server-cloudimg-amd64.img" + package_manager: "apt" + family: "debian" + + ubuntu-22.04: + url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" + checksum_url: "https://cloud-images.ubuntu.com/jammy/current/SHA256SUMS" + checksum_type: "sha256" + os_variant: "ubuntu22.04" + cache_name: "ubuntu-22.04-server-cloudimg-amd64.img" + package_manager: "apt" + family: "debian" + + ubuntu-24.04: + url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" + checksum_url: "https://cloud-images.ubuntu.com/noble/current/SHA256SUMS" + checksum_type: "sha256" + os_variant: "ubuntu24.04" + cache_name: "ubuntu-24.04-server-cloudimg-amd64.img" + package_manager: "apt" + family: "debian" + + # RHEL (requires subscription) + rhel-8: + url: "https://access.redhat.com/downloads/content/rhel/8/x86_64/latest/rhel-8-x86_64-kvm.qcow2" + os_variant: "rhel8.0" + cache_name: "rhel-8-x86_64-kvm.qcow2" + package_manager: "dnf" + family: "rhel" + note: "Requires Red Hat subscription and manual download" + + rhel-9: + url: "https://access.redhat.com/downloads/content/rhel/9/x86_64/latest/rhel-9-x86_64-kvm.qcow2" + os_variant: "rhel9.0" + cache_name: "rhel-9-x86_64-kvm.qcow2" + package_manager: "dnf" + family: "rhel" + note: "Requires Red Hat subscription and manual download" + + # CentOS Stream + centos-stream-8: + url: "https://cloud.centos.org/centos/8-stream/x86_64/images/CentOS-Stream-GenericCloud-8-latest.x86_64.qcow2" + checksum_url: "https://cloud.centos.org/centos/8-stream/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "centos-stream8" + cache_name: "centos-stream-8-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + centos-stream-9: + url: "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2" + checksum_url: "https://cloud.centos.org/centos/9-stream/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "centos-stream9" + cache_name: "centos-stream-9-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + # Rocky Linux + rocky-8: + url: "https://download.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2" + checksum_url: "https://download.rockylinux.org/pub/rocky/8/images/x86_64/CHECKSUM" + checksum_type: "sha256" + os_variant: "rocky8" + cache_name: "rocky-8-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + rocky-9: + url: "https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2" + checksum_url: "https://download.rockylinux.org/pub/rocky/9/images/x86_64/CHECKSUM" + checksum_type: "sha256" + os_variant: "rocky9" + cache_name: "rocky-9-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + # AlmaLinux + almalinux-8: + url: "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2" + checksum_url: "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "almalinux8" + cache_name: "almalinux-8-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + almalinux-9: + url: "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2" + checksum_url: "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "almalinux9" + cache_name: "almalinux-9-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + # SLES (requires registration) + sles-15: + url: "https://download.suse.com/Download?buildid=XXXXX" + os_variant: "sles15" + cache_name: "sles-15-genericcloud-amd64.qcow2" + package_manager: "zypper" + family: "suse" + note: "Requires SUSE subscription and manual download" + + # openSUSE Leap + opensuse-leap-15.5: + url: "https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.x86_64-Cloud.qcow2" + checksum_url: "https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.x86_64-Cloud.qcow2.sha256" + checksum_type: "sha256" + os_variant: "opensuse15.5" + cache_name: "opensuse-leap-15.5-minimal-vm-amd64.qcow2" + package_manager: "zypper" + family: "suse" + + opensuse-leap-15.6: + url: "https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.x86_64-Cloud.qcow2" + checksum_url: "https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.x86_64-Cloud.qcow2.sha256" + checksum_type: "sha256" + os_variant: "opensuse15.6" + cache_name: "opensuse-leap-15.6-minimal-vm-amd64.qcow2" + package_manager: "zypper" + family: "suse" + + tasks: + # ========================================================================= + # Validation and Setup + # ========================================================================= + + - name: Validate distribution selection + assert: + that: + - os_distribution is defined + - os_distribution in cloud_images.keys() + fail_msg: | + Invalid distribution '{{ os_distribution }}'. + Supported distributions: {{ cloud_images.keys() | list | join(', ') }} + tags: [validate, preflight] + + - name: Set distribution facts + set_fact: + distro_config: "{{ cloud_images[os_distribution] }}" + image_cache_path: "/var/lib/libvirt/images/{{ cloud_images[os_distribution].cache_name }}" + tags: [always] + + - name: Display deployment information + debug: + msg: + - "=== VM Deployment Configuration ===" + - "VM Name: {{ vm_name }}" + - "Distribution: {{ os_distribution }}" + - "OS Family: {{ distro_config.family }}" + - "Package Manager: {{ distro_config.package_manager }}" + - "vCPUs: {{ vm_vcpus }}" + - "Memory: {{ vm_memory_mb }} MB" + - "Disk: {{ vm_disk_size_gb }} GB" + tags: [validate, preflight] + + - name: Check if VM already exists + command: virsh dominfo {{ vm_name }} + register: vm_exists + failed_when: false + changed_when: false + tags: [validate, preflight] + + - name: Fail if VM already exists + fail: + msg: "VM '{{ vm_name }}' already exists on hypervisor. Please choose a different name or destroy the existing VM." + when: vm_exists.rc == 0 + tags: [validate, preflight] + + - name: Verify virtualization support + command: virt-host-validate qemu + register: virt_validation + failed_when: false + changed_when: false + tags: [validate, preflight] + + - name: Display virtualization validation results + debug: + var: virt_validation.stdout_lines + tags: [validate, preflight] + + # ========================================================================= + # Package Installation + # ========================================================================= + + - name: Install required packages for VM deployment (Debian/Ubuntu) + apt: + name: + - libvirt-daemon-system + - libvirt-clients + - virtinst + - qemu-kvm + - qemu-utils + - cloud-image-utils + - genisoimage + - wget + - curl + - python3-libvirt + state: present + update_cache: yes + when: ansible_os_family == "Debian" + tags: [install] + + - name: Install required packages for VM deployment (RHEL/CentOS) + dnf: + name: + - libvirt + - libvirt-client + - virt-install + - qemu-kvm + - qemu-img + - cloud-utils + - genisoimage + - wget + - curl + - python3-libvirt + state: present + when: ansible_os_family == "RedHat" + tags: [install] + + - name: Ensure libvirtd service is running + systemd: + name: libvirtd + state: started + enabled: yes + tags: [install] + + # ========================================================================= + # Download Cloud Image + # ========================================================================= + + - name: Check if cloud image already exists + stat: + path: "{{ image_cache_path }}" + register: cloud_image_stat + tags: [download] + + - name: Display image cache status + debug: + msg: "Cloud image {{ 'exists' if cloud_image_stat.stat.exists else 'not found' }}: {{ image_cache_path }}" + tags: [download] + + - name: Check for manual download requirement + debug: + msg: + - "WARNING: {{ os_distribution }} requires manual download" + - "{{ distro_config.note | default('') }}" + - "Please download the image and place it at: {{ image_cache_path }}" + when: + - not cloud_image_stat.stat.exists + - distro_config.note is defined + tags: [download] + + - name: Download cloud image + get_url: + url: "{{ distro_config.url }}" + dest: "{{ image_cache_path }}" + mode: '0644' + timeout: 1200 + when: + - not cloud_image_stat.stat.exists + - distro_config.note is not defined + register: download_result + tags: [download] + + - name: Download checksum file + get_url: + url: "{{ distro_config.checksum_url }}" + dest: "/tmp/{{ os_distribution }}-CHECKSUM" + mode: '0644' + when: + - distro_config.checksum_url is defined + - download_result is changed or cloud_image_stat.stat.exists + tags: [download, verify] + + - name: Verify cloud image checksum (SHA512) + shell: | + cd /var/lib/libvirt/images + grep "{{ distro_config.cache_name }}" /tmp/{{ os_distribution }}-CHECKSUM | sha512sum -c - + register: checksum_result + changed_when: false + when: + - distro_config.checksum_type is defined + - distro_config.checksum_type == "sha512" + - distro_config.checksum_url is defined + tags: [verify] + + - name: Verify cloud image checksum (SHA256) + shell: | + cd /var/lib/libvirt/images + grep "{{ distro_config.cache_name }}" /tmp/{{ os_distribution }}-CHECKSUM | sha256sum -c - + register: checksum_result + changed_when: false + when: + - distro_config.checksum_type is defined + - distro_config.checksum_type == "sha256" + - distro_config.checksum_url is defined + tags: [verify] + + - name: Ensure image file exists before proceeding + stat: + path: "{{ image_cache_path }}" + register: final_image_check + failed_when: not final_image_check.stat.exists + tags: [verify] + + # ========================================================================= + # Create VM Disk + # ========================================================================= + + - name: Create VM disk from cloud image + command: > + qemu-img create -f qcow2 -F qcow2 + -b {{ image_cache_path }} + {{ vm_disk_path }} + {{ vm_disk_size_gb }}G + args: + creates: "{{ vm_disk_path }}" + tags: [storage] + + - name: Set proper permissions on VM disk + file: + path: "{{ vm_disk_path }}" + owner: libvirt-qemu + group: kvm + mode: '0600' + when: ansible_os_family == "Debian" + tags: [storage] + + - name: Set proper permissions on VM disk (RHEL) + file: + path: "{{ vm_disk_path }}" + owner: qemu + group: qemu + mode: '0600' + when: ansible_os_family == "RedHat" + tags: [storage] + + # ========================================================================= + # Create Cloud-Init Configuration + # ========================================================================= + + - name: Create cloud-init directory + file: + path: /tmp/cloud-init-{{ vm_name }} + state: directory + mode: '0755' + tags: [cloud-init] + + - name: Create cloud-init meta-data + copy: + content: | + instance-id: {{ vm_name }} + local-hostname: {{ vm_hostname }} + dest: /tmp/cloud-init-{{ vm_name }}/meta-data + mode: '0644' + tags: [cloud-init] + + - name: Create cloud-init user-data for Debian/Ubuntu + copy: + content: | + #cloud-config + hostname: {{ vm_hostname }} + fqdn: {{ vm_hostname }}.{{ vm_domain }} + manage_etc_hosts: true + + # Create ansible user with sudo privileges + users: + - name: ansible + groups: sudo + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ ansible_user_ssh_key }} + - name: root + lock_passwd: false + + # Set root password (for emergency console access) + chpasswd: + list: | + root:ChangeMe123! + expire: false + + # SSH configuration + ssh_pwauth: false + disable_root: false + + # Install essential packages per CLAUDE.md guidelines + packages: + - sudo + - vim + - htop + - tmux + - curl + - wget + - rsync + - git + - python3 + - python3-pip + - jq + - bc + - aide + - auditd + - chrony + - ufw + - lvm2 + - cloud-guest-utils + - parted + - unattended-upgrades + - apt-listchanges + + # Security configuration files + write_files: + - path: /etc/ssh/sshd_config.d/99-security.conf + content: | + PermitRootLogin no + PasswordAuthentication no + PubkeyAuthentication yes + MaxAuthTries 3 + MaxSessions 10 + ClientAliveInterval 300 + ClientAliveCountMax 2 + permissions: '0644' + + - path: /etc/sudoers.d/ansible + content: | + ansible ALL=(ALL) NOPASSWD:ALL + permissions: '0440' + + - path: /etc/apt/apt.conf.d/50unattended-upgrades + content: | + Unattended-Upgrade::Allowed-Origins { + "${distro_id}:${distro_codename}-security"; + }; + Unattended-Upgrade::AutoFixInterruptedDpkg "true"; + Unattended-Upgrade::MinimalSteps "true"; + Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; + Unattended-Upgrade::Remove-Unused-Dependencies "true"; + Unattended-Upgrade::Automatic-Reboot "false"; + permissions: '0644' + + - path: /etc/apt/apt.conf.d/20auto-upgrades + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; + permissions: '0644' + + # System configuration commands + runcmd: + - systemctl enable ssh + - systemctl restart ssh + - systemctl enable chrony + - systemctl start chrony + - ufw --force enable + - ufw allow ssh + - systemctl enable auditd + - systemctl start auditd + - growpart /dev/vda 1 || true + - resize2fs /dev/vda1 || true + + package_update: true + package_upgrade: true + package_reboot_if_required: false + + timezone: UTC + locale: en_US.UTF-8 + + output: + all: '| tee -a /var/log/cloud-init-output.log' + + final_message: "{{ os_distribution }} VM deployment completed. System is ready after $UPTIME seconds." + dest: /tmp/cloud-init-{{ vm_name }}/user-data + mode: '0644' + when: distro_config.family == "debian" + tags: [cloud-init] + + - name: Create cloud-init user-data for RHEL/CentOS/Rocky/Alma + copy: + content: | + #cloud-config + hostname: {{ vm_hostname }} + fqdn: {{ vm_hostname }}.{{ vm_domain }} + manage_etc_hosts: true + + # Create ansible user with sudo privileges + users: + - name: ansible + groups: wheel + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ ansible_user_ssh_key }} + - name: root + lock_passwd: false + + # Set root password (for emergency console access) + chpasswd: + list: | + root:ChangeMe123! + expire: false + + # SSH configuration + ssh_pwauth: false + disable_root: false + + # Install essential packages per CLAUDE.md guidelines + packages: + - sudo + - vim + - htop + - tmux + - curl + - wget + - rsync + - git + - python3 + - python3-pip + - jq + - bc + - aide + - audit + - chrony + - firewalld + - lvm2 + - cloud-utils-growpart + - gdisk + - dnf-automatic + - policycoreutils-python-utils + + # Security configuration files + write_files: + - path: /etc/ssh/sshd_config.d/99-security.conf + content: | + PermitRootLogin no + PasswordAuthentication no + PubkeyAuthentication yes + MaxAuthTries 3 + MaxSessions 10 + ClientAliveInterval 300 + ClientAliveCountMax 2 + permissions: '0644' + + - path: /etc/sudoers.d/ansible + content: | + ansible ALL=(ALL) NOPASSWD:ALL + permissions: '0440' + + - path: /etc/dnf/automatic.conf + content: | + [commands] + upgrade_type = security + download_updates = yes + apply_updates = yes + + [emitters] + emit_via = stdio + + [email] + email_from = root@{{ vm_hostname }}.{{ vm_domain }} + + [base] + debuglevel = 1 + permissions: '0644' + + # System configuration commands + runcmd: + - systemctl enable sshd + - systemctl restart sshd + - systemctl enable chronyd + - systemctl start chronyd + - systemctl enable firewalld + - systemctl start firewalld + - firewall-cmd --permanent --add-service=ssh + - firewall-cmd --reload + - systemctl enable auditd + - systemctl start auditd + - systemctl enable dnf-automatic.timer + - systemctl start dnf-automatic.timer + - setenforce 1 + - sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config + - growpart /dev/vda 1 || true + - xfs_growfs / || resize2fs /dev/vda1 || true + + package_update: true + package_upgrade: true + package_reboot_if_required: false + + timezone: UTC + locale: en_US.UTF-8 + + output: + all: '| tee -a /var/log/cloud-init-output.log' + + final_message: "{{ os_distribution }} VM deployment completed. System is ready after $UPTIME seconds." + dest: /tmp/cloud-init-{{ vm_name }}/user-data + mode: '0644' + when: distro_config.family == "rhel" + tags: [cloud-init] + + - name: Create cloud-init user-data for SUSE/openSUSE + copy: + content: | + #cloud-config + hostname: {{ vm_hostname }} + fqdn: {{ vm_hostname }}.{{ vm_domain }} + manage_etc_hosts: true + + # Create ansible user with sudo privileges + users: + - name: ansible + groups: wheel + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ ansible_user_ssh_key }} + - name: root + lock_passwd: false + + # Set root password (for emergency console access) + chpasswd: + list: | + root:ChangeMe123! + expire: false + + # SSH configuration + ssh_pwauth: false + disable_root: false + + # Install essential packages + packages: + - sudo + - vim + - htop + - tmux + - curl + - wget + - rsync + - git + - python3 + - python3-pip + - jq + - bc + - aide + - audit + - chrony + - firewalld + - lvm2 + - cloud-utils-growpart + - gdisk + + # Security configuration files + write_files: + - path: /etc/ssh/sshd_config.d/99-security.conf + content: | + PermitRootLogin no + PasswordAuthentication no + PubkeyAuthentication yes + MaxAuthTries 3 + MaxSessions 10 + ClientAliveInterval 300 + ClientAliveCountMax 2 + permissions: '0644' + + - path: /etc/sudoers.d/ansible + content: | + ansible ALL=(ALL) NOPASSWD:ALL + permissions: '0440' + + # System configuration commands + runcmd: + - systemctl enable sshd + - systemctl restart sshd + - systemctl enable chronyd + - systemctl start chronyd + - systemctl enable firewalld + - systemctl start firewalld + - firewall-cmd --permanent --add-service=ssh + - firewall-cmd --reload + - systemctl enable auditd + - systemctl start auditd + - growpart /dev/vda 1 || true + - xfs_growfs / || resize2fs /dev/vda1 || btrfs filesystem resize max / || true + + package_update: true + package_upgrade: true + package_reboot_if_required: false + + timezone: UTC + locale: en_US.UTF-8 + + output: + all: '| tee -a /var/log/cloud-init-output.log' + + final_message: "{{ os_distribution }} VM deployment completed. System is ready after $UPTIME seconds." + dest: /tmp/cloud-init-{{ vm_name }}/user-data + mode: '0644' + when: distro_config.family == "suse" + tags: [cloud-init] + + - name: Create cloud-init ISO + command: > + genisoimage -output {{ cloud_init_iso_path }} + -volid cidata -joliet -rock + /tmp/cloud-init-{{ vm_name }}/user-data + /tmp/cloud-init-{{ vm_name }}/meta-data + args: + creates: "{{ cloud_init_iso_path }}" + tags: [cloud-init] + + - name: Set proper permissions on cloud-init ISO (Debian/Ubuntu) + file: + path: "{{ cloud_init_iso_path }}" + owner: libvirt-qemu + group: kvm + mode: '0644' + when: ansible_os_family == "Debian" + tags: [cloud-init] + + - name: Set proper permissions on cloud-init ISO (RHEL) + file: + path: "{{ cloud_init_iso_path }}" + owner: qemu + group: qemu + mode: '0644' + when: ansible_os_family == "RedHat" + tags: [cloud-init] + + # ========================================================================= + # Create and Start VM + # ========================================================================= + + - name: Create VM using virt-install + command: > + virt-install + --name {{ vm_name }} + --memory {{ vm_memory_mb }} + --vcpus {{ vm_vcpus }} + --disk path={{ vm_disk_path }},format=qcow2,bus=virtio + --disk path={{ cloud_init_iso_path }},device=cdrom + --network network={{ vm_network }},model=virtio + --os-variant {{ distro_config.os_variant }} + --graphics none + --console pty,target_type=serial + --import + --noautoconsole + register: vm_create + tags: [deploy] + + - name: Wait for VM to boot and cloud-init to complete + pause: + seconds: 90 + prompt: "Waiting for VM to boot and cloud-init to complete configuration..." + tags: [deploy] + + - name: Get VM IP address + shell: | + virsh domifaddr {{ vm_name }} | grep -oP '(\d{1,3}\.){3}\d{1,3}' | head -1 + register: vm_ip + retries: 15 + delay: 10 + until: vm_ip.stdout != "" + changed_when: false + tags: [deploy, validate] + + - name: Display VM information + debug: + msg: + - "=== VM Deployment Successful ===" + - "VM Name: {{ vm_name }}" + - "Distribution: {{ os_distribution }}" + - "IP Address: {{ vm_ip.stdout }}" + - "vCPUs: {{ vm_vcpus }}" + - "Memory: {{ vm_memory_mb }} MB" + - "Disk: {{ vm_disk_size_gb }} GB" + - "OS Variant: {{ distro_config.os_variant }}" + - "Package Manager: {{ distro_config.package_manager }}" + - "Access: ssh ansible@{{ vm_ip.stdout }}" + - "" + - "Add to inventory:" + - " {{ vm_name }}:" + - " ansible_host: {{ vm_ip.stdout }}" + - " ansible_user: ansible" + - " ansible_ssh_common_args: '-o ProxyJump=grokbox -o StrictHostKeyChecking=accept-new'" + - " os_distribution: {{ os_distribution }}" + - " os_family: {{ distro_config.family }}" + tags: [deploy, validate] + + # ========================================================================= + # Validation + # ========================================================================= + + - name: Test SSH connectivity to new VM + wait_for: + host: "{{ vm_ip.stdout }}" + port: 22 + timeout: 300 + state: started + tags: [validate] + + - name: Get VM details + command: virsh dominfo {{ vm_name }} + register: vm_details + changed_when: false + tags: [validate] + + - name: Display VM details + debug: + var: vm_details.stdout_lines + tags: [validate] + + # ========================================================================= + # Cleanup + # ========================================================================= + + - name: Remove temporary cloud-init directory + file: + path: /tmp/cloud-init-{{ vm_name }} + state: absent + tags: [cleanup] + + - name: Remove downloaded checksums + file: + path: /tmp/{{ os_distribution }}-CHECKSUM + state: absent + tags: [cleanup] + +# ============================================================================= +# Post-Deployment Validation (Optional) +# ============================================================================= + +- name: Validate deployed VM + hosts: "{{ hostvars['grokbox']['vm_ip'].stdout }}" + gather_facts: yes + become: yes + vars: + ansible_user: ansible + ansible_ssh_common_args: '-o ProxyJump=grokbox -o StrictHostKeyChecking=accept-new' + + tasks: + - name: Wait for cloud-init to complete + command: cloud-init status --wait + changed_when: false + failed_when: false + tags: [validate] + + - name: Gather system facts + setup: + tags: [validate] + + - name: Display system information + debug: + msg: + - "=== System Information ===" + - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" + - "Kernel: {{ ansible_kernel }}" + - "Architecture: {{ ansible_architecture }}" + - "Hostname: {{ ansible_hostname }}" + - "FQDN: {{ ansible_fqdn }}" + - "Python: {{ ansible_python_version }}" + - "Package Manager: {{ ansible_pkg_mgr }}" + tags: [validate] + + - name: Gather disk usage + command: df -h + register: disk_usage + changed_when: false + tags: [validate] + + - name: Display disk usage + debug: + var: disk_usage.stdout_lines + tags: [validate] + + - name: Gather memory usage + command: free -h + register: memory_usage + changed_when: false + tags: [validate] + + - name: Display memory usage + debug: + var: memory_usage.stdout_lines + tags: [validate] + + - name: Check SELinux status (RHEL family) + command: getenforce + register: selinux_status + changed_when: false + failed_when: false + when: ansible_os_family == "RedHat" + tags: [validate] + + - name: Display SELinux status + debug: + msg: "SELinux Status: {{ selinux_status.stdout }}" + when: ansible_os_family == "RedHat" + tags: [validate]