Files
infra-automation/plays/deploy-linux-vm-lvm.yml
Infrastructure Team 47df4035c3 Add LVM-enabled VM deployment playbooks
- Add deploy-debian-lvm-netinst.yml for Debian with native LVM
  - Uses network installer with preseed configuration
  - Full LVM partitioning per infrastructure guidelines
  - Creates vg_system with 8 logical volumes
  - Separate /boot, /opt, /tmp, /home, /var, /var/log, /var/tmp, /var/log/audit
  - Security mount options (noexec,nosuid,nodev on /tmp and /var/tmp)

- Add deploy-linux-vm-lvm.yml for multi-distro with post-config LVM
  - Supports all distributions from deploy-linux-vm.yml
  - Deploys VM with secondary 30GB disk for LVM
  - Post-deployment LVM configuration on /dev/vdb
  - Data migration from primary disk to LVM volumes
  - Automatic fstab updates
2025-11-10 22:51:40 +01:00

692 lines
23 KiB
YAML

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