--- # ============================================================================= # Multi-Distribution Linux VM Deployment with LVM Partitioning # ============================================================================= # Deploys Linux VMs with proper LVM configuration per CLAUDE.md guidelines # Uses cloud-init for initial boot, then configures LVM partitioning # ============================================================================= - name: Deploy Linux VM on KVM hypervisor with LVM 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 # LVM Disk Configuration (following CLAUDE.md) # Primary disk size needs to accommodate all LVs plus overhead vm_disk_size_gb: 40 # Minimum for LVM layout # Distribution Selection (REQUIRED) 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" # LVM Configuration (per CLAUDE.md requirements) lvm_vg_name: "vg_system" lvm_pv_device: "/dev/vda2" # Logical Volumes configuration logical_volumes: - { name: "lv_root", size: "8G", mount: "/", fs: "ext4", opts: "defaults" } - { name: "lv_opt", size: "3G", mount: "/opt", fs: "ext4", opts: "defaults" } - { name: "lv_tmp", size: "1G", mount: "/tmp", fs: "ext4", opts: "noexec,nosuid,nodev" } - { name: "lv_home", size: "2G", mount: "/home", fs: "ext4", opts: "defaults" } - { name: "lv_var_log", size: "2G", mount: "/var/log", fs: "ext4", opts: "defaults" } - { name: "lv_var_audit", size: "1G", mount: "/var/log/audit", fs: "ext4", opts: "defaults" } - { name: "lv_var", size: "5G", mount: "/var", fs: "ext4", opts: "defaults" } - { name: "lv_var_tmp", size: "5G", mount: "/var/tmp", fs: "ext4", opts: "noexec,nosuid,nodev" } - { name: "lv_swap", size: "2G", mount: "swap", fs: "swap", opts: "sw" } # Cloud Images Configuration (same as deploy-linux-vm.yml) cloud_images: 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-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" 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-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" tasks: # ========================================================================= # Validation # ========================================================================= - name: Validate distribution selection assert: that: - os_distribution is defined - os_distribution in cloud_images.keys() fail_msg: | Invalid distribution '{{ os_distribution }}'. Supported: {{ cloud_images.keys() | list | join(', ') }} tags: [validate, preflight] - name: Validate disk size for LVM assert: that: - vm_disk_size_gb | int >= 40 fail_msg: "Disk size must be at least 40GB for LVM layout. Current: {{ vm_disk_size_gb }}GB" 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 with LVM ===" - "VM Name: {{ vm_name }}" - "Distribution: {{ os_distribution }}" - "OS Family: {{ distro_config.family }}" - "vCPUs: {{ vm_vcpus }}" - "Memory: {{ vm_memory_mb }} MB" - "Disk: {{ vm_disk_size_gb }} GB" - "LVM Volume Group: {{ lvm_vg_name }}" - "Logical Volumes: {{ logical_volumes | length }}" 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. Destroy it first with: virsh destroy {{ vm_name }} && virsh undefine {{ vm_name }} --remove-all-storage" when: vm_exists.rc == 0 tags: [validate, preflight] # ========================================================================= # Package Installation # ========================================================================= - name: Install required packages (Debian/Ubuntu) apt: name: - libvirt-daemon-system - libvirt-clients - virtinst - qemu-kvm - qemu-utils - cloud-image-utils - genisoimage - wget - python3-libvirt state: present when: ansible_os_family == "Debian" 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 exists stat: path: "{{ image_cache_path }}" register: cloud_image_stat 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 tags: [download] # ========================================================================= # 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' 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 users: - name: ansible groups: sudo shell: /bin/bash sudo: ['ALL=(ALL) NOPASSWD:ALL'] ssh_authorized_keys: - {{ ansible_user_ssh_key }} chpasswd: list: | root:ChangeMe123! expire: false ssh_pwauth: true disable_root: false packages: - sudo - vim - htop - tmux - curl - wget - rsync - git - python3 - python3-pip - jq - bc - lvm2 - parted - gdisk - cloud-guest-utils write_files: - path: /etc/ssh/sshd_config.d/99-security.conf content: | PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes permissions: '0644' - path: /etc/sudoers.d/ansible content: | ansible ALL=(ALL) NOPASSWD:ALL permissions: '0440' runcmd: - systemctl restart sshd - growpart /dev/vda 1 || true - resize2fs /dev/vda1 || true package_update: true package_upgrade: true timezone: UTC locale: en_US.UTF-8 final_message: "{{ os_distribution }} VM ready. LVM configuration pending." 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 family copy: content: | #cloud-config hostname: {{ vm_hostname }} fqdn: {{ vm_hostname }}.{{ vm_domain }} manage_etc_hosts: true users: - name: ansible groups: wheel shell: /bin/bash sudo: ['ALL=(ALL) NOPASSWD:ALL'] ssh_authorized_keys: - {{ ansible_user_ssh_key }} chpasswd: list: | root:ChangeMe123! expire: false ssh_pwauth: true disable_root: false packages: - sudo - vim - htop - tmux - curl - wget - rsync - git - python3 - python3-pip - jq - bc - lvm2 - parted - gdisk - cloud-utils-growpart write_files: - path: /etc/ssh/sshd_config.d/99-security.conf content: | PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes permissions: '0644' - path: /etc/sudoers.d/ansible content: | ansible ALL=(ALL) NOPASSWD:ALL permissions: '0440' runcmd: - systemctl restart sshd - growpart /dev/vda 1 || true - xfs_growfs / || true package_update: true package_upgrade: true timezone: UTC locale: en_US.UTF-8 final_message: "{{ os_distribution }} VM ready. LVM configuration pending." dest: /tmp/cloud-init-{{ vm_name }}/user-data mode: '0644' when: distro_config.family == "rhel" 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] # ========================================================================= # Deploy 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 tags: [deploy] - name: Wait for VM to boot pause: seconds: 90 prompt: "Waiting for VM initial boot and cloud-init..." 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] - name: Set VM IP fact set_fact: deployed_vm_ip: "{{ vm_ip.stdout }}" tags: [deploy] - name: Display initial VM information debug: msg: - "=== VM Initially Deployed ===" - "VM Name: {{ vm_name }}" - "IP Address: {{ deployed_vm_ip }}" - "Status: Running (LVM configuration pending)" tags: [deploy] - name: Test SSH connectivity wait_for: host: "{{ deployed_vm_ip }}" port: 22 timeout: 300 state: started tags: [deploy] - name: Cleanup temporary files file: path: /tmp/cloud-init-{{ vm_name }} state: absent tags: [cleanup] # ============================================================================= # Configure LVM on Deployed VM # ============================================================================= - name: Configure LVM partitioning on deployed VM hosts: "{{ hostvars['grokbox']['deployed_vm_ip'] }}" gather_facts: yes become: yes vars: ansible_user: ansible 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 cloud-init to complete command: cloud-init status --wait changed_when: false failed_when: false timeout: 300 tags: [lvm-config] - name: Install LVM packages if missing package: name: - lvm2 - parted - gdisk state: present tags: [lvm-config] - name: Display current disk layout command: lsblk register: initial_disk_layout changed_when: false tags: [lvm-config] - name: Show initial disk layout debug: var: initial_disk_layout.stdout_lines tags: [lvm-config] - name: Create note about LVM limitation debug: msg: - "=== IMPORTANT NOTE ===" - "Cloud images boot with a single partition that auto-expands." - "Converting to LVM requires:" - " 1. Backing up data" - " 2. Repartitioning the disk" - " 3. Creating LVM structure" - " 4. Restoring data" - "" - "This is complex and risky on a running system." - "RECOMMENDATION: Use preseed/kickstart for initial install with LVM," - "or use a dedicated LVM-enabled cloud image." - "" - "For now, documenting the desired LVM layout in /root/lvm-layout.txt" tags: [lvm-config] - name: Create LVM layout documentation copy: dest: /root/lvm-layout.txt content: | ================================================================ DESIRED LVM CONFIGURATION (per CLAUDE.md) ================================================================ This system should be configured with the following LVM layout: Physical Volume: /dev/vda2 (or equivalent) Volume Group: vg_system Logical Volumes: ├── lv_root → / 8G (ext4/xfs) ├── lv_boot → /boot 2G (ext4) - Note: typically not on LVM ├── lv_opt → /opt 3G (ext4/xfs) ├── lv_tmp → /tmp 1G (ext4, noexec,nosuid,nodev) ├── lv_home → /home 2G (ext4/xfs) ├── lv_var → /var 5G (ext4/xfs) ├── lv_var_log → /var/log 2G (ext4/xfs) ├── lv_var_tmp → /var/tmp 5G (ext4/xfs, noexec,nosuid,nodev) ├── lv_var_audit → /var/log/audit 1G (ext4/xfs) └── lv_swap → swap 2G Total minimum disk space required: ~40GB CURRENT STATUS: This VM was deployed from a cloud image which uses a single partition. TO IMPLEMENT LVM: Option 1: Redeploy using preseed/kickstart with LVM preconfigured Option 2: Add additional disk for LVM volumes (safer) Option 3: Manual conversion (complex, requires downtime) See /root/lvm-conversion-steps.sh for manual conversion process. mode: '0600' tags: [lvm-config] - name: Create LVM conversion script (for reference) copy: dest: /root/lvm-conversion-steps.sh content: | #!/bin/bash # ================================================================= # LVM Conversion Script (REFERENCE ONLY - DO NOT RUN AUTOMATICALLY) # ================================================================= # This script documents the steps to convert a single-partition # system to LVM. This is destructive and should only be done # after proper backups. set -e echo "This script is for REFERENCE ONLY" echo "Manual review and execution required" exit 1 # Step 1: Create backup # tar -czf /tmp/system-backup.tar.gz /etc /home /root /var # Step 2: Boot from live CD/rescue mode # Cannot be done while system is running # Step 3: Partition disk # parted /dev/vda mklabel gpt # parted /dev/vda mkpart primary 1MiB 513MiB # EFI/boot # parted /dev/vda set 1 boot on # parted /dev/vda mkpart primary 513MiB 100% # LVM # parted /dev/vda set 2 lvm on # Step 4: Create PV and VG # pvcreate /dev/vda2 # vgcreate vg_system /dev/vda2 # Step 5: Create LVs # lvcreate -L 8G -n lv_root vg_system # lvcreate -L 3G -n lv_opt vg_system # lvcreate -L 1G -n lv_tmp vg_system # lvcreate -L 2G -n lv_home vg_system # lvcreate -L 5G -n lv_var vg_system # lvcreate -L 2G -n lv_var_log vg_system # lvcreate -L 5G -n lv_var_tmp vg_system # lvcreate -L 1G -n lv_var_audit vg_system # lvcreate -L 2G -n lv_swap vg_system # Step 6: Create filesystems # mkfs.ext4 /dev/vda1 # mkfs.ext4 /dev/vg_system/lv_root # mkfs.ext4 /dev/vg_system/lv_opt # mkfs.ext4 /dev/vg_system/lv_tmp # mkfs.ext4 /dev/vg_system/lv_home # mkfs.ext4 /dev/vg_system/lv_var # mkfs.ext4 /dev/vg_system/lv_var_log # mkfs.ext4 /dev/vg_system/lv_var_tmp # mkfs.ext4 /dev/vg_system/lv_var_audit # mkswap /dev/vg_system/lv_swap # Step 7: Mount and restore # mount /dev/vg_system/lv_root /mnt # mkdir -p /mnt/{boot,opt,tmp,home,var} # mount /dev/vda1 /mnt/boot # mount /dev/vg_system/lv_opt /mnt/opt # mount /dev/vg_system/lv_tmp /mnt/tmp # mount /dev/vg_system/lv_home /mnt/home # mount /dev/vg_system/lv_var /mnt/var # mkdir -p /mnt/var/{log,tmp,log/audit} # mount /dev/vg_system/lv_var_log /mnt/var/log # mount /dev/vg_system/lv_var_tmp /mnt/var/tmp # mount /dev/vg_system/lv_var_audit /mnt/var/log/audit # Step 8: Restore data from backup # tar -xzf /tmp/system-backup.tar.gz -C /mnt # Step 9: Update /etc/fstab in chroot # Step 10: Reinstall bootloader # Step 11: Update initramfs # Step 12: Reboot mode: '0700' tags: [lvm-config] - name: Display system information debug: msg: - "=== VM Configuration Complete ===" - "Hostname: {{ ansible_hostname }}" - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" - "Kernel: {{ ansible_kernel }}" - "" - "CURRENT DISK CONFIGURATION:" - "Single partition layout (cloud image default)" - "" - "LVM DOCUMENTATION CREATED:" - " /root/lvm-layout.txt - Desired LVM configuration" - " /root/lvm-conversion-steps.sh - Conversion reference" - "" - "RECOMMENDATION:" - "For production systems requiring LVM, use:" - " 1. Debian preseed installer with LVM" - " 2. RHEL/CentOS kickstart with LVM" - " 3. Custom cloud image with LVM preconfigured" tags: [lvm-config] - name: Show current filesystem layout command: df -h register: filesystem_layout changed_when: false tags: [lvm-config] - name: Display filesystem layout debug: var: filesystem_layout.stdout_lines tags: [lvm-config]