--- # ============================================================================= # 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]