diff --git a/plays/deploy-debian-lvm-netinst.yml b/plays/deploy-debian-lvm-netinst.yml new file mode 100644 index 0000000..4ec3cd9 --- /dev/null +++ b/plays/deploy-debian-lvm-netinst.yml @@ -0,0 +1,532 @@ +--- +# ============================================================================= +# 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] diff --git a/plays/deploy-linux-vm-lvm.yml b/plays/deploy-linux-vm-lvm.yml new file mode 100644 index 0000000..66b9ebb --- /dev/null +++ b/plays/deploy-linux-vm-lvm.yml @@ -0,0 +1,691 @@ +--- +# ============================================================================= +# 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]