--- # ============================================================================= # Debian VM Deployment with LVM using Network Installer # ============================================================================= # Deploys Debian 12 with proper LVM partitioning per CLAUDE.md # Uses preseed for automated installation # ============================================================================= - name: Deploy Debian 12 VM with LVM on KVM 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: 40 # Network Configuration vm_network: "default" vm_bridge: "virbr0" # Storage Configuration vm_disk_path: "/var/lib/libvirt/images/{{ vm_name }}.qcow2" # Debian Network Installer debian_netinst_url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.8.0-amd64-netinst.iso" debian_netinst_path: "/var/lib/libvirt/images/debian-12.8.0-amd64-netinst.iso" # 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. Destroy it first." when: vm_exists.rc == 0 tags: [validate, preflight] - name: Display deployment information debug: msg: - "=== Debian 12 VM Deployment with LVM ===" - "VM Name: {{ vm_name }}" - "Method: Network installer with preseed" - "Disk: {{ vm_disk_size_gb }} GB with LVM" - "Installation time: ~10-15 minutes" - "" - "LVM Layout (per CLAUDE.md):" - " VG: vg_system" - " LVs: root(8G), opt(3G), tmp(1G), home(2G)" - " var(5G), var_log(2G), var_tmp(5G), var_audit(1G), swap(2G)" tags: [validate, preflight] # ========================================================================= # Package Installation # ========================================================================= - name: Install required packages apt: name: - libvirt-daemon-system - libvirt-clients - virtinst - qemu-kvm - qemu-utils - wget - genisoimage - python3-libvirt state: present tags: [install] - name: Ensure libvirtd is running systemd: name: libvirtd state: started enabled: yes tags: [install] # ========================================================================= # Download Debian Network Installer # ========================================================================= - name: Check if Debian netinst ISO exists stat: path: "{{ debian_netinst_path }}" register: netinst_stat tags: [download] - name: Download Debian network installer ISO get_url: url: "{{ debian_netinst_url }}" dest: "{{ debian_netinst_path }}" mode: '0644' timeout: 1200 when: not netinst_stat.stat.exists tags: [download] # ========================================================================= # Create Preseed Configuration # ========================================================================= - name: Create preseed directory file: path: /tmp/preseed-{{ vm_name }} state: directory mode: '0755' tags: [preseed] - name: Create preseed configuration file copy: dest: /tmp/preseed-{{ vm_name }}/preseed.cfg mode: '0644' content: | #### Debian 12 Preseed Configuration with LVM #### Per CLAUDE.md Requirements ### Localization d-i debian-installer/locale string en_US.UTF-8 d-i keyboard-configuration/xkb-keymap select us ### Network configuration d-i netcfg/choose_interface select auto d-i netcfg/get_hostname string {{ vm_hostname }} d-i netcfg/get_domain string {{ vm_domain }} d-i netcfg/wireless_wep string ### Mirror settings d-i mirror/country string manual d-i mirror/http/hostname string deb.debian.org d-i mirror/http/directory string /debian d-i mirror/http/proxy string ### Account setup d-i passwd/root-login boolean true d-i passwd/root-password password ChangeMe123! d-i passwd/root-password-again password ChangeMe123! # Create ansible user d-i passwd/user-fullname string Ansible User d-i passwd/username string ansible d-i passwd/user-password password ansible123 d-i passwd/user-password-again password ansible123 d-i passwd/user-default-groups string sudo ### Clock and time zone d-i clock-setup/utc boolean true d-i time/zone string UTC d-i clock-setup/ntp boolean true ### Partitioning with LVM d-i partman-auto/method string lvm d-i partman-lvm/device_remove_lvm boolean true d-i partman-md/device_remove_md boolean true d-i partman-lvm/confirm boolean true d-i partman-lvm/confirm_nooverwrite boolean true # Create custom LVM recipe d-i partman-auto/expert_recipe string \ boot-lvm :: \ 512 512 512 ext4 \ $primary{ } $bootable{ } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /boot } \ . \ 8192 8192 8192 ext4 \ $defaultignore{ } \ $primary{ } \ method{ lvm } \ vg_name{ vg_system } \ . \ 8192 8192 8192 ext4 \ $lvmok{ } \ lv_name{ lv_root } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ / } \ . \ 3072 3072 3072 ext4 \ $lvmok{ } \ lv_name{ lv_opt } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /opt } \ . \ 1024 1024 1024 ext4 \ $lvmok{ } \ lv_name{ lv_tmp } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /tmp } \ options/noexec{ noexec } \ options/nosuid{ nosuid } \ options/nodev{ nodev } \ . \ 2048 2048 2048 ext4 \ $lvmok{ } \ lv_name{ lv_home } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /home } \ . \ 5120 5120 5120 ext4 \ $lvmok{ } \ lv_name{ lv_var } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /var } \ . \ 2048 2048 2048 ext4 \ $lvmok{ } \ lv_name{ lv_var_log } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /var/log } \ . \ 5120 5120 5120 ext4 \ $lvmok{ } \ lv_name{ lv_var_tmp } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /var/tmp } \ options/noexec{ noexec } \ options/nosuid{ nosuid } \ options/nodev{ nodev } \ . \ 1024 1024 1024 ext4 \ $lvmok{ } \ lv_name{ lv_var_audit } \ in_vg{ vg_system } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /var/log/audit } \ . \ 2048 2048 2048 linux-swap \ $lvmok{ } \ lv_name{ lv_swap } \ in_vg{ vg_system } \ method{ swap } format{ } \ . d-i partman-auto-lvm/guided_size string max d-i partman-auto-lvm/new_vg_name string vg_system d-i partman/choose_partition select finish d-i partman/confirm boolean true d-i partman/confirm_nooverwrite boolean true ### Package selection tasksel tasksel/first multiselect standard, ssh-server d-i pkgsel/include string sudo vim htop tmux curl wget rsync git python3 python3-pip jq bc lvm2 aide auditd chrony ufw cloud-init d-i pkgsel/upgrade select full-upgrade popularity-contest popularity-contest/participate boolean false ### Boot loader d-i grub-installer/only_debian boolean true d-i grub-installer/with_other_os boolean true d-i grub-installer/bootdev string default ### Finishing up d-i finish-install/reboot_in_progress note ### Late command - configure ansible user and SSH d-i preseed/late_command string \ in-target mkdir -p /home/ansible/.ssh; \ in-target chmod 700 /home/ansible/.ssh; \ in-target sh -c 'echo "{{ ansible_user_ssh_key }}" > /home/ansible/.ssh/authorized_keys'; \ in-target chown -R ansible:ansible /home/ansible/.ssh; \ in-target chmod 600 /home/ansible/.ssh/authorized_keys; \ in-target sh -c 'echo "ansible ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ansible'; \ in-target chmod 440 /etc/sudoers.d/ansible; \ in-target mkdir -p /etc/ssh/sshd_config.d; \ in-target sh -c 'echo -e "PermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes" > /etc/ssh/sshd_config.d/99-security.conf' tags: [preseed] - name: Create preseed ISO command: > genisoimage -r -J -o /tmp/preseed-{{ vm_name }}.iso -V "PRESEED" /tmp/preseed-{{ vm_name }}/ args: creates: /tmp/preseed-{{ vm_name }}.iso tags: [preseed] # ========================================================================= # Create VM Disk # ========================================================================= - name: Create VM disk command: > qemu-img create -f qcow2 {{ vm_disk_path }} {{ vm_disk_size_gb }}G args: creates: "{{ vm_disk_path }}" tags: [storage] - name: Set permissions on VM disk file: path: "{{ vm_disk_path }}" owner: libvirt-qemu group: kvm mode: '0600' tags: [storage] # ========================================================================= # Install VM # ========================================================================= - name: Display installation notice debug: msg: - "=== Starting Debian Network Installation ===" - "This will take approximately 10-15 minutes" - "The VM will automatically install and configure:" - " - Debian 12 (Bookworm)" - " - LVM partitioning per CLAUDE.md" - " - ansible user with SSH keys" - " - Essential packages" - "" - "You can monitor progress with:" - " ssh grokbox 'sudo virsh console {{ vm_name }}'" tags: [install-vm] - name: Create VM with network installer command: > virt-install --name {{ vm_name }} --memory {{ vm_memory_mb }} --vcpus {{ vm_vcpus }} --disk path={{ vm_disk_path }},format=qcow2,bus=virtio --cdrom {{ debian_netinst_path }} --disk path=/tmp/preseed-{{ vm_name }}.iso,device=cdrom --network network={{ vm_network }},model=virtio --os-variant debian12 --graphics none --console pty,target_type=serial --extra-args "auto=true priority=critical console=ttyS0,115200n8 serial preseed/file=/cdrom/preseed.cfg" --noautoconsole tags: [install-vm] - name: Wait for installation to complete pause: minutes: 15 prompt: "Waiting for Debian installation (15 minutes)... You can monitor with: ssh grokbox 'sudo virsh console {{ vm_name }}'" tags: [install-vm] - name: Check if VM is running command: virsh domstate {{ vm_name }} register: vm_state changed_when: false tags: [validate] - name: Display VM state debug: var: vm_state.stdout tags: [validate] - name: Start VM if not running command: virsh start {{ vm_name }} when: vm_state.stdout != "running" failed_when: false tags: [validate] - name: Wait a bit for network pause: seconds: 30 tags: [validate] - 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: [validate] - name: Set VM IP fact set_fact: deployed_vm_ip: "{{ vm_ip.stdout }}" tags: [validate] - name: Display VM information debug: msg: - "=== VM Installation Complete ===" - "VM Name: {{ vm_name }}" - "IP Address: {{ deployed_vm_ip }}" - "Access: ssh ansible@{{ deployed_vm_ip }}" - "Or: ssh -J grokbox ansible@{{ deployed_vm_ip }}" - "" - "Root password: ChangeMe123! (change immediately!)" - "Ansible password: ansible123 (SSH keys configured)" tags: [validate] - name: Cleanup preseed files file: path: "{{ item }}" state: absent loop: - /tmp/preseed-{{ vm_name }} - /tmp/preseed-{{ vm_name }}.iso tags: [cleanup] # ============================================================================= # Validate LVM Configuration # ============================================================================= - name: Validate LVM configuration on deployed VM hosts: "{{ hostvars['grokbox']['deployed_vm_ip'] }}" gather_facts: yes become: yes vars: ansible_user: ansible ansible_password: ansible123 ansible_ssh_common_args: '-o ProxyJump=grokbox -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null' ansible_python_interpreter: /usr/bin/python3 tasks: - name: Wait for SSH to be available wait_for_connection: timeout: 300 tags: [validate-lvm] - name: Gather system facts setup: tags: [validate-lvm] - name: Display system information debug: msg: - "=== System Information ===" - "Hostname: {{ ansible_hostname }}" - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" - "Kernel: {{ ansible_kernel }}" tags: [validate-lvm] - name: Check LVM configuration command: "{{ item }}" register: lvm_checks changed_when: false loop: - pvdisplay - vgdisplay - lvdisplay tags: [validate-lvm] - name: Display LVM configuration debug: msg: "{{ item.stdout_lines }}" loop: "{{ lvm_checks.results }}" tags: [validate-lvm] - name: Check filesystem layout command: df -h register: df_output changed_when: false tags: [validate-lvm] - name: Display filesystem layout debug: var: df_output.stdout_lines tags: [validate-lvm] - name: Verify mount options for /tmp shell: mount | grep ' /tmp ' register: tmp_mount changed_when: false tags: [validate-lvm] - name: Display /tmp mount options debug: msg: "{{ tmp_mount.stdout }}" tags: [validate-lvm] - name: Check installed packages command: dpkg -l register: packages changed_when: false tags: [validate-lvm] - name: Verify essential packages are installed shell: dpkg -l | grep -E 'aide|auditd|chrony|ufw|lvm2' | awk '{print $2, $3}' register: essential_pkgs changed_when: false tags: [validate-lvm] - name: Display essential packages debug: var: essential_pkgs.stdout_lines tags: [validate-lvm] - name: Final validation summary debug: msg: - "=== Validation Complete ===" - "✓ VM deployed with Debian 12" - "✓ LVM configuration applied" - "✓ Volume group: vg_system" - "✓ Logical volumes created per CLAUDE.md" - "✓ Ansible user configured with SSH keys" - "✓ Essential packages installed" - "" - "Next steps:" - " 1. Change root password: ssh ansible@{{ ansible_default_ipv4.address }} sudo passwd root" - " 2. Configure firewall: sudo ufw enable && sudo ufw allow ssh" - " 3. Enable services: sudo systemctl enable --now chrony auditd" - " 4. Run security hardening playbooks" tags: [validate-lvm]