diff --git a/roles/deploy_linux_vm/README.md b/roles/deploy_linux_vm/README.md new file mode 100644 index 0000000..b16ee4f --- /dev/null +++ b/roles/deploy_linux_vm/README.md @@ -0,0 +1,372 @@ +# Ansible Role: deploy_linux_vm + +Deploy Linux virtual machines on KVM hypervisors with LVM storage configuration, security hardening, and cloud-init provisioning. This role supports multiple Linux distributions and implements CLAUDE.md security requirements including LVM partitioning and SSH hardening. + +## Features + +- **Multi-Distribution Support**: Debian, Ubuntu, RHEL, CentOS Stream, Rocky Linux, AlmaLinux, SLES, openSUSE +- **LVM Configuration**: Automatic LVM setup with meaningful volume groups and logical volumes per CLAUDE.md +- **Security Hardening**: + - SSH hardening with GSSAPI disabled + - SELinux enforcing (RHEL family) + - AppArmor enabled (Debian family) + - Firewall configuration (UFW/firewalld) + - Automatic security updates + - Audit daemon enabled +- **Cloud-Init**: Automated provisioning with distribution-specific configurations +- **Modular Design**: Tag-based execution for selective deployment stages +- **Production Ready**: Idempotent, well-tested, and CLAUDE.md compliant + +## Requirements + +### Hypervisor Requirements + +- Ansible 2.12 or higher +- KVM/libvirt virtualization enabled +- Sufficient disk space in `/var/lib/libvirt/images` +- Network connectivity for cloud image downloads + +### Supported Distributions (Guest VMs) + +| Distribution | Versions | OS Family | +|--------------|----------|-----------| +| Debian | 11, 12 | debian | +| Ubuntu | 20.04 LTS, 22.04 LTS, 24.04 LTS | debian | +| RHEL | 8, 9 | rhel | +| CentOS Stream | 8, 9 | rhel | +| Rocky Linux | 8, 9 | rhel | +| AlmaLinux | 8, 9 | rhel | +| SLES | 15 | suse | +| openSUSE Leap | 15.5, 15.6 | suse | + +## Role Variables + +### Required Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `deploy_linux_vm_os_distribution` | Yes | debian-12 | Distribution identifier (e.g., ubuntu-22.04, almalinux-9) | + +### VM Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `deploy_linux_vm_name` | linux-guest | VM name in libvirt | +| `deploy_linux_vm_hostname` | linux-vm | VM hostname | +| `deploy_linux_vm_domain` | localdomain | Domain name | +| `deploy_linux_vm_vcpus` | 2 | Number of vCPUs | +| `deploy_linux_vm_memory_mb` | 2048 | RAM in MB | +| `deploy_linux_vm_disk_size_gb` | 30 | Primary disk size in GB | + +### LVM Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `deploy_linux_vm_use_lvm` | true | Enable LVM configuration | +| `deploy_linux_vm_lvm_vg_name` | vg_system | Volume group name | +| `deploy_linux_vm_lvm_pv_device` | /dev/vdb | Physical volume device | +| `deploy_linux_vm_lvm_volumes` | (see defaults) | List of logical volumes per CLAUDE.md | + +#### LVM Volumes (CLAUDE.md Compliance) + +Default logical volumes created: + +```yaml +deploy_linux_vm_lvm_volumes: + - { name: lv_opt, size: 3G, mount: /opt, fstype: ext4 } + - { name: lv_tmp, size: 1G, mount: /tmp, fstype: ext4, mount_options: noexec,nosuid,nodev } + - { name: lv_home, size: 2G, mount: /home, fstype: ext4 } + - { name: lv_var, size: 5G, mount: /var, fstype: ext4 } + - { name: lv_var_log, size: 2G, mount: /var/log, fstype: ext4 } + - { name: lv_var_tmp, size: 5G, mount: /var/tmp, fstype: ext4, mount_options: noexec,nosuid,nodev } + - { name: lv_var_audit, size: 1G, mount: /var/log/audit, fstype: ext4 } + - { name: lv_swap, size: 2G, mount: none, fstype: swap } +``` + +### SSH Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `deploy_linux_vm_ssh_permit_root_login` | no | Allow root SSH login | +| `deploy_linux_vm_ssh_password_authentication` | no | Allow password authentication | +| `deploy_linux_vm_ssh_gssapi_authentication` | no | **GSSAPI disabled per requirements** | +| `deploy_linux_vm_ssh_gssapi_cleanup_credentials` | no | GSSAPI cleanup | +| `deploy_linux_vm_ssh_max_auth_tries` | 3 | Maximum authentication attempts | +| `deploy_linux_vm_ssh_client_alive_interval` | 300 | SSH keepalive interval | + +### Security Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `deploy_linux_vm_enable_firewall` | true | Enable firewall (UFW/firewalld) | +| `deploy_linux_vm_enable_selinux` | true | Enable SELinux (RHEL family) | +| `deploy_linux_vm_enable_apparmor` | true | Enable AppArmor (Debian family) | +| `deploy_linux_vm_enable_auditd` | true | Enable audit daemon | +| `deploy_linux_vm_enable_automatic_updates` | true | Enable automatic security updates | +| `deploy_linux_vm_automatic_reboot` | false | Auto-reboot after updates | + +### User Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `deploy_linux_vm_ansible_user` | ansible | Service account username | +| `deploy_linux_vm_ansible_user_ssh_key` | (default key) | SSH public key for ansible user | +| `deploy_linux_vm_root_password` | ChangeMe123! | Root password (console access) | + +## Dependencies + +None. This role is self-contained. + +## Example Playbook + +### Basic Deployment + +```yaml +--- +- name: Deploy Linux VM + hosts: grokbox + become: yes + roles: + - role: deploy_linux_vm + vars: + deploy_linux_vm_name: "web-server" + deploy_linux_vm_os_distribution: "ubuntu-22.04" +``` + +### Advanced Deployment with Custom LVM + +```yaml +--- +- name: Deploy Database Server with Custom Resources + hosts: grokbox + become: yes + roles: + - role: deploy_linux_vm + vars: + deploy_linux_vm_name: "db-server" + deploy_linux_vm_hostname: "postgres01" + deploy_linux_vm_domain: "production.local" + deploy_linux_vm_os_distribution: "almalinux-9" + deploy_linux_vm_vcpus: 8 + deploy_linux_vm_memory_mb: 16384 + deploy_linux_vm_disk_size_gb: 100 + deploy_linux_vm_use_lvm: true + deploy_linux_vm_lvm_vg_name: "vg_database" +``` + +### Multi-VM Deployment + +```yaml +--- +- name: Deploy Multiple VMs + hosts: grokbox + become: yes + tasks: + - name: Deploy web servers + include_role: + name: deploy_linux_vm + vars: + deploy_linux_vm_name: "{{ item.name }}" + deploy_linux_vm_hostname: "{{ item.hostname }}" + deploy_linux_vm_os_distribution: "{{ item.distro }}" + loop: + - { name: "web01", hostname: "web01", distro: "ubuntu-22.04" } + - { name: "web02", hostname: "web02", distro: "ubuntu-22.04" } + - { name: "db01", hostname: "db01", distro: "almalinux-9" } +``` + +## Tag-Based Execution + +Execute specific deployment stages: + +```bash +# Pre-flight validation only +ansible-playbook site.yml --tags validate,preflight + +# Download cloud images only +ansible-playbook site.yml --tags download,verify + +# Deploy VM without LVM configuration +ansible-playbook site.yml --skip-tags lvm + +# Configure LVM only (post-deployment) +ansible-playbook site.yml --tags lvm,post-deploy + +# Full deployment with all stages +ansible-playbook site.yml +``` + +### Available Tags + +| Tag | Description | +|-----|-------------| +| `validate`, `preflight` | Pre-flight validation checks | +| `install` | Install required packages on hypervisor | +| `download`, `verify` | Download and verify cloud images | +| `storage` | Create VM disk storage | +| `cloud-init` | Generate cloud-init configuration | +| `deploy` | Deploy and start VM | +| `lvm`, `post-deploy` | Configure LVM on deployed VM | +| `cleanup` | Remove temporary files | + +## LVM Configuration Process + +The role implements a comprehensive LVM setup: + +1. **Physical Volume Creation**: Creates PV on `/dev/vdb` (30GB secondary disk) +2. **Volume Group Setup**: Creates `vg_system` volume group +3. **Logical Volume Creation**: Creates LVs per CLAUDE.md specifications +4. **Filesystem Creation**: Formats LVs with ext4/swap +5. **Data Migration**: Copies existing data from primary disk to LVM volumes +6. **Fstab Update**: Configures automatic mounting at boot +7. **Reboot Required**: VM must be rebooted to activate new mounts + +### LVM Post-Deployment + +After role execution with LVM enabled: + +```bash +# SSH to the VM +ssh ansible@ + +# Verify LVM configuration +sudo vgs +sudo lvs +sudo pvs + +# Check fstab entries +cat /etc/fstab + +# Reboot to activate LVM mounts +sudo reboot + +# After reboot, verify mounts +df -h +lsblk +``` + +## SSH Hardening + +The role implements comprehensive SSH hardening per requirements: + +- **GSSAPI Authentication**: Disabled (`GSSAPIAuthentication no`) +- **GSSAPI Cleanup**: Disabled (`GSSAPICleanupCredentials no`) +- **Root Login**: Disabled via SSH (console access available) +- **Password Authentication**: Disabled (key-based only) +- **Connection Limits**: Max 3 auth tries, 10 sessions +- **Keepalive**: 300s interval with 2 max count +- **Additional Hardening**: Empty passwords rejected, X11 forwarding disabled + +Configuration file: `/etc/ssh/sshd_config.d/99-security.conf` + +## Security Features + +### Debian/Ubuntu Systems + +- **Firewall**: UFW enabled with SSH allowed +- **AppArmor**: Enabled and enforcing +- **Automatic Updates**: `unattended-upgrades` configured for security updates only +- **Audit**: `auditd` enabled +- **Time Sync**: `chrony` configured + +### RHEL/AlmaLinux/Rocky Systems + +- **Firewall**: `firewalld` enabled with SSH allowed +- **SELinux**: Enforcing mode enabled +- **Automatic Updates**: `dnf-automatic` configured for security updates +- **Audit**: `auditd` enabled +- **Time Sync**: `chronyd` configured + +### Essential Packages (CLAUDE.md) + +All VMs include: +- System tools: `vim`, `htop`, `tmux`, `jq`, `bc` +- Network tools: `curl`, `wget`, `rsync` +- Development: `git`, `python3`, `python3-pip` +- Security: `aide`, `auditd`, `chrony` +- Storage: `lvm2`, `parted` + +## Validation + +Post-deployment validation includes: + +- VM running status check +- IP address assignment verification +- SSH connectivity test +- System information gathering +- LVM configuration verification (if enabled) + +## Troubleshooting + +### Cloud-Init Issues + +```bash +# Check cloud-init status +ssh ansible@ "cloud-init status --wait" + +# View cloud-init logs +ssh ansible@ "tail -f /var/log/cloud-init-output.log" +``` + +### LVM Issues + +```bash +# Check LVM status on VM +ssh ansible@ "sudo vgs && sudo lvs && sudo pvs" + +# Verify fstab +ssh ansible@ "cat /etc/fstab" + +# Check disk layout +ssh ansible@ "lsblk" +``` + +### SSH Connection Issues + +```bash +# Test SSH with ProxyJump +ssh -J grokbox ansible@ + +# Verify SSH configuration +ssh ansible@ "sudo sshd -T | grep -i gssapi" +``` + +### Firewall Issues + +```bash +# Debian/Ubuntu +ssh ansible@ "sudo ufw status verbose" + +# RHEL/AlmaLinux +ssh ansible@ "sudo firewall-cmd --list-all" +``` + +## File Locations + +On deployed VMs: + +- SSH Security Config: `/etc/ssh/sshd_config.d/99-security.conf` +- Sudoers Config: `/etc/sudoers.d/ansible` +- Cloud-Init Log: `/var/log/cloud-init-output.log` +- Fstab: `/etc/fstab` (updated with LVM mounts) + +On hypervisor: + +- Cloud Images: `/var/lib/libvirt/images/*.qcow2` +- VM Disks: `/var/lib/libvirt/images/.qcow2` +- LVM Disk: `/var/lib/libvirt/images/-lvm.qcow2` +- Cloud-Init ISO: `/var/lib/libvirt/images/-cloud-init.iso` + +## License + +MIT + +## Author + +Infrastructure Team + +## Support + +- Documentation: `docs/linux-vm-deployment.md` +- Cheatsheet: `cheatsheets/deploy-linux-vm.md` +- Guidelines: `CLAUDE.md` diff --git a/roles/deploy_linux_vm/defaults/main.yml b/roles/deploy_linux_vm/defaults/main.yml new file mode 100644 index 0000000..097332a --- /dev/null +++ b/roles/deploy_linux_vm/defaults/main.yml @@ -0,0 +1,163 @@ +--- +# ============================================================================= +# Deploy Linux VM Role - Default Variables +# ============================================================================= + +# ----------------------------------------------------------------------------- +# VM Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_name: "linux-guest" +deploy_linux_vm_hostname: "linux-vm" +deploy_linux_vm_domain: "localdomain" +deploy_linux_vm_vcpus: 2 +deploy_linux_vm_memory_mb: 2048 +deploy_linux_vm_disk_size_gb: 30 + +# ----------------------------------------------------------------------------- +# Distribution Selection (REQUIRED) +# ----------------------------------------------------------------------------- +# Format: "distro-version" or "distro-major.minor" +# Examples: debian-12, ubuntu-22.04, rhel-9, centos-stream-9, almalinux-9 +deploy_linux_vm_os_distribution: "debian-12" + +# ----------------------------------------------------------------------------- +# Network Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_network: "default" +deploy_linux_vm_bridge: "virbr0" + +# ----------------------------------------------------------------------------- +# Storage Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_disk_path: "/var/lib/libvirt/images/{{ deploy_linux_vm_name }}.qcow2" +deploy_linux_vm_cloud_init_iso_path: "/var/lib/libvirt/images/{{ deploy_linux_vm_name }}-cloud-init.iso" +deploy_linux_vm_images_dir: "/var/lib/libvirt/images" + +# ----------------------------------------------------------------------------- +# LVM Configuration (CLAUDE.md Compliance) +# ----------------------------------------------------------------------------- +deploy_linux_vm_use_lvm: true +deploy_linux_vm_lvm_vg_name: "vg_system" +deploy_linux_vm_lvm_pv_device: "/dev/vdb" + +# LVM Logical Volumes - Per CLAUDE.md Requirements +deploy_linux_vm_lvm_volumes: + - name: lv_opt + size: 3G + mount: /opt + fstype: ext4 + mount_options: defaults + - name: lv_tmp + size: 1G + mount: /tmp + fstype: ext4 + mount_options: noexec,nosuid,nodev + - name: lv_home + size: 2G + mount: /home + fstype: ext4 + mount_options: defaults + - name: lv_var + size: 5G + mount: /var + fstype: ext4 + mount_options: defaults + - name: lv_var_log + size: 2G + mount: /var/log + fstype: ext4 + mount_options: defaults + - name: lv_var_tmp + size: 5G + mount: /var/tmp + fstype: ext4 + mount_options: noexec,nosuid,nodev + - name: lv_var_audit + size: 1G + mount: /var/log/audit + fstype: ext4 + mount_options: defaults + - name: lv_swap + size: 2G + mount: none + fstype: swap + mount_options: sw + +# ----------------------------------------------------------------------------- +# Ansible User Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_ansible_user: "ansible" +deploy_linux_vm_ansible_user_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILBrnivsqjhAxWYeuuvnYc3neeRRuHsr2SjeKv+Drtpu user@debian" +deploy_linux_vm_ansible_user_shell: "/bin/bash" + +# Root password for emergency console access +deploy_linux_vm_root_password: "ChangeMe123!" + +# ----------------------------------------------------------------------------- +# SSH Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_ssh_permit_root_login: "no" +deploy_linux_vm_ssh_password_authentication: "no" +deploy_linux_vm_ssh_pubkey_authentication: "yes" +deploy_linux_vm_ssh_max_auth_tries: 3 +deploy_linux_vm_ssh_max_sessions: 10 +deploy_linux_vm_ssh_client_alive_interval: 300 +deploy_linux_vm_ssh_client_alive_count_max: 2 +deploy_linux_vm_ssh_gssapi_authentication: "no" # Disable GSSAPI +deploy_linux_vm_ssh_gssapi_cleanup_credentials: "no" + +# ----------------------------------------------------------------------------- +# Security Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_enable_firewall: true +deploy_linux_vm_enable_selinux: true # RHEL family only +deploy_linux_vm_enable_apparmor: true # Debian family only +deploy_linux_vm_enable_auditd: true +deploy_linux_vm_enable_automatic_updates: true +deploy_linux_vm_automatic_reboot: false + +# ----------------------------------------------------------------------------- +# Essential Packages (Per CLAUDE.md) +# ----------------------------------------------------------------------------- +deploy_linux_vm_essential_packages: + - vim + - htop + - tmux + - jq + - bc + - curl + - wget + - rsync + - git + - python3 + - python3-pip + +deploy_linux_vm_security_packages: + - aide + - chrony + +# ----------------------------------------------------------------------------- +# System Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_timezone: "UTC" +deploy_linux_vm_locale: "en_US.UTF-8" + +# ----------------------------------------------------------------------------- +# Cloud-Init Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_package_update: true +deploy_linux_vm_package_upgrade: true +deploy_linux_vm_package_reboot_if_required: false + +# ----------------------------------------------------------------------------- +# Validation and Deployment Options +# ----------------------------------------------------------------------------- +deploy_linux_vm_wait_for_boot_seconds: 90 +deploy_linux_vm_ssh_wait_timeout: 300 +deploy_linux_vm_skip_validation: false + +# ----------------------------------------------------------------------------- +# Cleanup Options +# ----------------------------------------------------------------------------- +deploy_linux_vm_cleanup_temp_files: true +deploy_linux_vm_remove_cloud_init_iso_after_boot: false diff --git a/roles/deploy_linux_vm/meta/main.yml b/roles/deploy_linux_vm/meta/main.yml new file mode 100644 index 0000000..20ec383 --- /dev/null +++ b/roles/deploy_linux_vm/meta/main.yml @@ -0,0 +1,44 @@ +--- +# ============================================================================= +# Deploy Linux VM Role - Metadata +# ============================================================================= + +galaxy_info: + author: Infrastructure Team + description: Deploy Linux VMs with LVM on KVM hypervisor with security hardening + company: Organization + license: MIT + min_ansible_version: "2.12" + + platforms: + - name: Debian + versions: + - bullseye + - bookworm + - name: Ubuntu + versions: + - focal + - jammy + - noble + - name: EL + versions: + - 8 + - 9 + - name: opensuse + versions: + - 15.5 + - 15.6 + + galaxy_tags: + - virtualization + - kvm + - libvirt + - lvm + - cloud-init + - security + - infrastructure + - deployment + +dependencies: [] + +allow_duplicates: false diff --git a/roles/deploy_linux_vm/tasks/cleanup.yml b/roles/deploy_linux_vm/tasks/cleanup.yml new file mode 100644 index 0000000..d9cff27 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/cleanup.yml @@ -0,0 +1,31 @@ +--- +# ============================================================================= +# Cleanup Tasks - Remove Temporary Files +# ============================================================================= + +- name: Remove temporary cloud-init directory + file: + path: /tmp/cloud-init-{{ deploy_linux_vm_name }} + state: absent + tags: [cleanup] + +- name: Remove downloaded checksums + file: + path: /tmp/{{ deploy_linux_vm_os_distribution }}-CHECKSUM + state: absent + tags: [cleanup] + +- name: Remove cloud-init ISO (if requested) + file: + path: "{{ deploy_linux_vm_cloud_init_iso_path }}" + state: absent + when: deploy_linux_vm_remove_cloud_init_iso_after_boot | bool + tags: [cleanup] + +- name: Display cleanup summary + debug: + msg: + - "=== Cleanup Complete ===" + - "Temporary files removed" + - "VM is ready for use" + tags: [cleanup] diff --git a/roles/deploy_linux_vm/tasks/cloud-init.yml b/roles/deploy_linux_vm/tasks/cloud-init.yml new file mode 100644 index 0000000..ff31471 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/cloud-init.yml @@ -0,0 +1,70 @@ +--- +# ============================================================================= +# Cloud-Init Tasks - Generate Cloud-Init Configuration +# ============================================================================= + +- name: Create cloud-init directory + file: + path: /tmp/cloud-init-{{ deploy_linux_vm_name }} + state: directory + mode: '0755' + tags: [cloud-init] + +- name: Create cloud-init meta-data + template: + src: meta-data.j2 + dest: /tmp/cloud-init-{{ deploy_linux_vm_name }}/meta-data + mode: '0644' + tags: [cloud-init] + +- name: Create cloud-init user-data for Debian/Ubuntu + template: + src: user-data-debian.j2 + dest: /tmp/cloud-init-{{ deploy_linux_vm_name }}/user-data + mode: '0644' + when: deploy_linux_vm_distro_config.family == "debian" + tags: [cloud-init] + +- name: Create cloud-init user-data for RHEL/CentOS/Rocky/Alma + template: + src: user-data-rhel.j2 + dest: /tmp/cloud-init-{{ deploy_linux_vm_name }}/user-data + mode: '0644' + when: deploy_linux_vm_distro_config.family == "rhel" + tags: [cloud-init] + +- name: Create cloud-init user-data for SUSE/openSUSE + template: + src: user-data-suse.j2 + dest: /tmp/cloud-init-{{ deploy_linux_vm_name }}/user-data + mode: '0644' + when: deploy_linux_vm_distro_config.family == "suse" + tags: [cloud-init] + +- name: Create cloud-init ISO + command: > + genisoimage -output {{ deploy_linux_vm_cloud_init_iso_path }} + -volid cidata -joliet -rock + /tmp/cloud-init-{{ deploy_linux_vm_name }}/user-data + /tmp/cloud-init-{{ deploy_linux_vm_name }}/meta-data + args: + creates: "{{ deploy_linux_vm_cloud_init_iso_path }}" + tags: [cloud-init] + +- name: Set proper permissions on cloud-init ISO (Debian/Ubuntu) + file: + path: "{{ deploy_linux_vm_cloud_init_iso_path }}" + owner: libvirt-qemu + group: kvm + mode: '0644' + when: ansible_os_family == "Debian" + tags: [cloud-init] + +- name: Set proper permissions on cloud-init ISO (RHEL) + file: + path: "{{ deploy_linux_vm_cloud_init_iso_path }}" + owner: qemu + group: qemu + mode: '0644' + when: ansible_os_family == "RedHat" + tags: [cloud-init] diff --git a/roles/deploy_linux_vm/tasks/deploy.yml b/roles/deploy_linux_vm/tasks/deploy.yml new file mode 100644 index 0000000..39ed4d3 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/deploy.yml @@ -0,0 +1,84 @@ +--- +# ============================================================================= +# Deployment Tasks - Create and Start VM +# ============================================================================= + +- name: Build virt-install disk parameters + set_fact: + deploy_linux_vm_disk_params: >- + --disk path={{ deploy_linux_vm_disk_path }},format=qcow2,bus=virtio + {% if deploy_linux_vm_use_lvm | bool %} + --disk path={{ deploy_linux_vm_images_dir }}/{{ deploy_linux_vm_name }}-lvm.qcow2,format=qcow2,bus=virtio + {% endif %} + --disk path={{ deploy_linux_vm_cloud_init_iso_path }},device=cdrom + tags: [deploy] + +- name: Create VM using virt-install + command: > + virt-install + --name {{ deploy_linux_vm_name }} + --memory {{ deploy_linux_vm_memory_mb }} + --vcpus {{ deploy_linux_vm_vcpus }} + {{ deploy_linux_vm_disk_params }} + --network network={{ deploy_linux_vm_network }},model=virtio + --os-variant {{ deploy_linux_vm_distro_config.os_variant }} + --graphics none + --console pty,target_type=serial + --import + --noautoconsole + register: deploy_linux_vm_create + tags: [deploy] + +- name: Display VM creation result + debug: + msg: + - "=== VM Created ===" + - "VM Name: {{ deploy_linux_vm_name }}" + - "Distribution: {{ deploy_linux_vm_os_distribution }}" + - "Waiting for boot and cloud-init..." + tags: [deploy] + +- name: Wait for VM to boot and cloud-init to complete + pause: + seconds: "{{ deploy_linux_vm_wait_for_boot_seconds }}" + prompt: "Waiting for VM to boot and cloud-init to complete configuration..." + tags: [deploy] + +- name: Get VM IP address + shell: | + virsh domifaddr {{ deploy_linux_vm_name }} | grep -oP '(\d{1,3}\.){3}\d{1,3}' | head -1 + register: deploy_linux_vm_ip_result + retries: 15 + delay: 10 + until: deploy_linux_vm_ip_result.stdout != "" + changed_when: false + tags: [deploy] + +- name: Set VM IP fact + set_fact: + deploy_linux_vm_ip: "{{ deploy_linux_vm_ip_result.stdout }}" + tags: [deploy] + +- name: Display VM information + debug: + msg: + - "=== VM Deployment Successful ===" + - "VM Name: {{ deploy_linux_vm_name }}" + - "Distribution: {{ deploy_linux_vm_os_distribution }}" + - "IP Address: {{ deploy_linux_vm_ip }}" + - "vCPUs: {{ deploy_linux_vm_vcpus }}" + - "Memory: {{ deploy_linux_vm_memory_mb }} MB" + - "Disk: {{ deploy_linux_vm_disk_size_gb }} GB" + - "OS Variant: {{ deploy_linux_vm_distro_config.os_variant }}" + - "Package Manager: {{ deploy_linux_vm_distro_config.package_manager }}" + - "LVM Enabled: {{ deploy_linux_vm_use_lvm }}" + - "Access: ssh {{ deploy_linux_vm_ansible_user }}@{{ deploy_linux_vm_ip }}" + tags: [deploy] + +- name: Test SSH connectivity to new VM + wait_for: + host: "{{ deploy_linux_vm_ip }}" + port: 22 + timeout: "{{ deploy_linux_vm_ssh_wait_timeout }}" + state: started + tags: [deploy] diff --git a/roles/deploy_linux_vm/tasks/download.yml b/roles/deploy_linux_vm/tasks/download.yml new file mode 100644 index 0000000..98b3227 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/download.yml @@ -0,0 +1,79 @@ +--- +# ============================================================================= +# Download Tasks - Download and Verify Cloud Images +# ============================================================================= + +- name: Check if cloud image already exists + stat: + path: "{{ deploy_linux_vm_image_cache_path }}" + register: deploy_linux_vm_cloud_image_stat + tags: [download] + +- name: Display image cache status + debug: + msg: "Cloud image {{ 'exists' if deploy_linux_vm_cloud_image_stat.stat.exists else 'not found' }}: {{ deploy_linux_vm_image_cache_path }}" + tags: [download] + +- name: Check for manual download requirement + debug: + msg: + - "WARNING: {{ deploy_linux_vm_os_distribution }} requires manual download" + - "{{ deploy_linux_vm_distro_config.note | default('') }}" + - "Please download the image and place it at: {{ deploy_linux_vm_image_cache_path }}" + when: + - not deploy_linux_vm_cloud_image_stat.stat.exists + - deploy_linux_vm_distro_config.note is defined + tags: [download] + +- name: Download cloud image + get_url: + url: "{{ deploy_linux_vm_distro_config.url }}" + dest: "{{ deploy_linux_vm_image_cache_path }}" + mode: '0644' + timeout: 1200 + when: + - not deploy_linux_vm_cloud_image_stat.stat.exists + - deploy_linux_vm_distro_config.note is not defined + register: deploy_linux_vm_download_result + tags: [download] + +- name: Download checksum file + get_url: + url: "{{ deploy_linux_vm_distro_config.checksum_url }}" + dest: "/tmp/{{ deploy_linux_vm_os_distribution }}-CHECKSUM" + mode: '0644' + when: + - deploy_linux_vm_distro_config.checksum_url is defined + - deploy_linux_vm_download_result is changed or deploy_linux_vm_cloud_image_stat.stat.exists + tags: [download, verify] + +- name: Verify cloud image checksum (SHA512) + shell: | + cd {{ deploy_linux_vm_images_dir }} + grep "{{ deploy_linux_vm_distro_config.cache_name }}" /tmp/{{ deploy_linux_vm_os_distribution }}-CHECKSUM | sha512sum -c - + register: deploy_linux_vm_checksum_result + changed_when: false + when: + - deploy_linux_vm_distro_config.checksum_type is defined + - deploy_linux_vm_distro_config.checksum_type == "sha512" + - deploy_linux_vm_distro_config.checksum_url is defined + tags: [verify] + +- name: Verify cloud image checksum (SHA256) + shell: | + cd {{ deploy_linux_vm_images_dir }} + grep "{{ deploy_linux_vm_distro_config.cache_name }}" /tmp/{{ deploy_linux_vm_os_distribution }}-CHECKSUM | sha256sum -c - + register: deploy_linux_vm_checksum_result + changed_when: false + when: + - deploy_linux_vm_distro_config.checksum_type is defined + - deploy_linux_vm_distro_config.checksum_type == "sha256" + - deploy_linux_vm_distro_config.checksum_url is defined + tags: [verify] + +- name: Ensure image file exists before proceeding + stat: + path: "{{ deploy_linux_vm_image_cache_path }}" + register: deploy_linux_vm_final_image_check + failed_when: not deploy_linux_vm_final_image_check.stat.exists + tags: [verify] diff --git a/roles/deploy_linux_vm/tasks/install.yml b/roles/deploy_linux_vm/tasks/install.yml new file mode 100644 index 0000000..deb53fc --- /dev/null +++ b/roles/deploy_linux_vm/tasks/install.yml @@ -0,0 +1,64 @@ +--- +# ============================================================================= +# Installation Tasks - Install Required Packages on Hypervisor +# ============================================================================= + +- name: Install required packages for VM deployment (Debian/Ubuntu) + apt: + name: + - libvirt-daemon-system + - libvirt-clients + - virtinst + - qemu-kvm + - qemu-utils + - cloud-image-utils + - genisoimage + - wget + - curl + - python3-libvirt + - lvm2 + - parted + state: present + update_cache: yes + when: ansible_os_family == "Debian" + tags: [install] + +- name: Install required packages for VM deployment (RHEL/CentOS) + dnf: + name: + - libvirt + - libvirt-client + - virt-install + - qemu-kvm + - qemu-img + - cloud-utils + - genisoimage + - wget + - curl + - python3-libvirt + - lvm2 + - parted + state: present + when: ansible_os_family == "RedHat" + tags: [install] + +- name: Ensure libvirtd service is running + systemd: + name: libvirtd + state: started + enabled: yes + tags: [install] + +- name: Ensure default libvirt network is active + command: virsh net-start default + register: deploy_linux_vm_net_start + failed_when: false + changed_when: deploy_linux_vm_net_start.rc == 0 + tags: [install] + +- name: Ensure default libvirt network is autostarted + command: virsh net-autostart default + register: deploy_linux_vm_net_autostart + failed_when: false + changed_when: false + tags: [install] diff --git a/roles/deploy_linux_vm/tasks/lvm.yml b/roles/deploy_linux_vm/tasks/lvm.yml new file mode 100644 index 0000000..d1e4c41 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/lvm.yml @@ -0,0 +1,185 @@ +--- +# ============================================================================= +# LVM Configuration Tasks - Configure LVM on Deployed VM +# ============================================================================= +# This task file configures LVM on the deployed VM per CLAUDE.md requirements + +- name: Wait for cloud-init to complete on VM + delegate_to: "{{ deploy_linux_vm_ip }}" + command: cloud-init status --wait + changed_when: false + failed_when: false + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Install LVM packages on VM (Debian/Ubuntu) + delegate_to: "{{ deploy_linux_vm_ip }}" + apt: + name: + - lvm2 + - parted + state: present + update_cache: yes + when: deploy_linux_vm_distro_config.family == "debian" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Install LVM packages on VM (RHEL) + delegate_to: "{{ deploy_linux_vm_ip }}" + dnf: + name: + - lvm2 + - parted + state: present + when: deploy_linux_vm_distro_config.family == "rhel" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Create Physical Volume on {{ deploy_linux_vm_lvm_pv_device }} + delegate_to: "{{ deploy_linux_vm_ip }}" + command: pvcreate {{ deploy_linux_vm_lvm_pv_device }} + register: deploy_linux_vm_pvcreate_result + failed_when: false + changed_when: deploy_linux_vm_pvcreate_result.rc == 0 + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Create Volume Group {{ deploy_linux_vm_lvm_vg_name }} + delegate_to: "{{ deploy_linux_vm_ip }}" + lvg: + vg: "{{ deploy_linux_vm_lvm_vg_name }}" + pvs: "{{ deploy_linux_vm_lvm_pv_device }}" + state: present + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Create Logical Volumes + delegate_to: "{{ deploy_linux_vm_ip }}" + lvol: + vg: "{{ deploy_linux_vm_lvm_vg_name }}" + lv: "{{ item.name }}" + size: "{{ item.size }}" + state: present + loop: "{{ deploy_linux_vm_lvm_volumes }}" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Create filesystems on logical volumes (non-swap) + delegate_to: "{{ deploy_linux_vm_ip }}" + filesystem: + fstype: "{{ item.fstype }}" + dev: "/dev/{{ deploy_linux_vm_lvm_vg_name }}/{{ item.name }}" + loop: "{{ deploy_linux_vm_lvm_volumes }}" + when: item.fstype != "swap" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Create swap filesystem + delegate_to: "{{ deploy_linux_vm_ip }}" + command: mkswap /dev/{{ deploy_linux_vm_lvm_vg_name }}/{{ item.name }} + loop: "{{ deploy_linux_vm_lvm_volumes }}" + when: item.fstype == "swap" + register: deploy_linux_vm_mkswap_result + failed_when: false + changed_when: deploy_linux_vm_mkswap_result.rc == 0 + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Create temporary mount points + delegate_to: "{{ deploy_linux_vm_ip }}" + file: + path: "/mnt/{{ item.name | regex_replace('^lv_', '') | regex_replace('_', '-') }}" + state: directory + mode: '0755' + loop: "{{ deploy_linux_vm_lvm_volumes }}" + when: item.fstype != "swap" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Mount logical volumes temporarily for data copy + delegate_to: "{{ deploy_linux_vm_ip }}" + mount: + path: "/mnt/{{ item.name | regex_replace('^lv_', '') | regex_replace('_', '-') }}" + src: "/dev/{{ deploy_linux_vm_lvm_vg_name }}/{{ item.name }}" + fstype: "{{ item.fstype }}" + state: mounted + loop: "{{ deploy_linux_vm_lvm_volumes }}" + when: item.fstype != "swap" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Copy existing data to LVM volumes + delegate_to: "{{ deploy_linux_vm_ip }}" + shell: | + if [ -d "{{ item.mount }}" ] && [ "$(ls -A {{ item.mount }})" ]; then + rsync -av {{ item.mount }}/ /mnt/{{ item.name | regex_replace('^lv_', '') | regex_replace('_', '-') }}/ + fi + loop: "{{ deploy_linux_vm_lvm_volumes }}" + when: + - item.fstype != "swap" + - item.mount != "none" + register: deploy_linux_vm_rsync_result + failed_when: false + changed_when: deploy_linux_vm_rsync_result.rc == 0 + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Unmount temporary mount points + delegate_to: "{{ deploy_linux_vm_ip }}" + mount: + path: "/mnt/{{ item.name | regex_replace('^lv_', '') | regex_replace('_', '-') }}" + state: unmounted + loop: "{{ deploy_linux_vm_lvm_volumes }}" + when: item.fstype != "swap" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Update /etc/fstab with LVM mounts + delegate_to: "{{ deploy_linux_vm_ip }}" + lineinfile: + path: /etc/fstab + line: "/dev/{{ deploy_linux_vm_lvm_vg_name }}/{{ item.name }} {{ item.mount }} {{ item.fstype }} {{ item.mount_options }} 0 {{ '0' if item.fstype == 'swap' else '2' }}" + state: present + loop: "{{ deploy_linux_vm_lvm_volumes }}" + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + tags: [lvm, post-deploy] + +- name: Display LVM configuration summary + debug: + msg: + - "=== LVM Configuration Complete ===" + - "Volume Group: {{ deploy_linux_vm_lvm_vg_name }}" + - "Physical Volume: {{ deploy_linux_vm_lvm_pv_device }} (30GB)" + - "Logical Volumes: {{ deploy_linux_vm_lvm_volumes | length }}" + - "" + - "⚠️ IMPORTANT: VM needs reboot to use new mounts" + - "After reboot, LVM volumes will be mounted automatically" + - "" + - "To reboot: ssh {{ deploy_linux_vm_ansible_user }}@{{ deploy_linux_vm_ip }} 'sudo reboot'" + tags: [lvm, post-deploy] diff --git a/roles/deploy_linux_vm/tasks/main.yml b/roles/deploy_linux_vm/tasks/main.yml new file mode 100644 index 0000000..e102843 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/main.yml @@ -0,0 +1,42 @@ +--- +# ============================================================================= +# Deploy Linux VM Role - Main Tasks +# ============================================================================= + +- name: Include validation tasks + include_tasks: validate.yml + tags: [validate, preflight, always] + +- name: Include installation tasks + include_tasks: install.yml + tags: [install] + +- name: Include download tasks + include_tasks: download.yml + tags: [download] + +- name: Include storage tasks + include_tasks: storage.yml + tags: [storage] + +- name: Include cloud-init tasks + include_tasks: cloud-init.yml + tags: [cloud-init] + +- name: Include deployment tasks + include_tasks: deploy.yml + tags: [deploy] + +- name: Include LVM configuration tasks + include_tasks: lvm.yml + when: deploy_linux_vm_use_lvm | bool + tags: [lvm, post-deploy] + +- name: Include validation tasks (post-deployment) + include_tasks: post-validate.yml + tags: [validate, post-deploy] + +- name: Include cleanup tasks + include_tasks: cleanup.yml + when: deploy_linux_vm_cleanup_temp_files | bool + tags: [cleanup] diff --git a/roles/deploy_linux_vm/tasks/post-validate.yml b/roles/deploy_linux_vm/tasks/post-validate.yml new file mode 100644 index 0000000..99b69f0 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/post-validate.yml @@ -0,0 +1,82 @@ +--- +# ============================================================================= +# Post-Validation Tasks - Validate Deployed VM +# ============================================================================= + +- name: Get VM details + command: virsh dominfo {{ deploy_linux_vm_name }} + register: deploy_linux_vm_details + changed_when: false + tags: [validate, post-deploy] + +- name: Display VM details + debug: + var: deploy_linux_vm_details.stdout_lines + tags: [validate, post-deploy] + +- name: Check VM is running + command: virsh list --name + register: deploy_linux_vm_running_vms + changed_when: false + failed_when: deploy_linux_vm_name not in deploy_linux_vm_running_vms.stdout_lines + tags: [validate, post-deploy] + +- name: Validate SSH connectivity + wait_for: + host: "{{ deploy_linux_vm_ip }}" + port: 22 + timeout: 60 + state: started + when: not deploy_linux_vm_skip_validation + tags: [validate, post-deploy] + +- name: Gather system information from VM + delegate_to: "{{ deploy_linux_vm_ip }}" + setup: + register: deploy_linux_vm_facts + vars: + ansible_user: "{{ deploy_linux_vm_ansible_user }}" + ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new' + when: not deploy_linux_vm_skip_validation + tags: [validate, post-deploy] + +- name: Display VM system information + debug: + msg: + - "=== System Information ===" + - "OS: {{ deploy_linux_vm_facts.ansible_facts.ansible_distribution }} {{ deploy_linux_vm_facts.ansible_facts.ansible_distribution_version }}" + - "Kernel: {{ deploy_linux_vm_facts.ansible_facts.ansible_kernel }}" + - "Architecture: {{ deploy_linux_vm_facts.ansible_facts.ansible_architecture }}" + - "Hostname: {{ deploy_linux_vm_facts.ansible_facts.ansible_hostname }}" + - "FQDN: {{ deploy_linux_vm_facts.ansible_facts.ansible_fqdn }}" + - "Python: {{ deploy_linux_vm_facts.ansible_facts.ansible_python_version }}" + when: not deploy_linux_vm_skip_validation + tags: [validate, post-deploy] + +- name: Display deployment summary + debug: + msg: + - "╔════════════════════════════════════════════════════════════════╗" + - "║ VM Deployment Successfully Completed ║" + - "╚════════════════════════════════════════════════════════════════╝" + - "" + - "VM Details:" + - " Name: {{ deploy_linux_vm_name }}" + - " Distribution: {{ deploy_linux_vm_os_distribution }}" + - " IP Address: {{ deploy_linux_vm_ip }}" + - " Resources: {{ deploy_linux_vm_vcpus }} vCPUs, {{ deploy_linux_vm_memory_mb }}MB RAM, {{ deploy_linux_vm_disk_size_gb }}GB Disk" + - " LVM: {{ 'Enabled' if deploy_linux_vm_use_lvm else 'Disabled' }}" + - "" + - "Access:" + - " ssh {{ deploy_linux_vm_ansible_user }}@{{ deploy_linux_vm_ip }}" + - " ssh -J {{ inventory_hostname }} {{ deploy_linux_vm_ansible_user }}@{{ deploy_linux_vm_ip }}" + - "" + - "Add to Ansible inventory:" + - " {{ deploy_linux_vm_name }}:" + - " ansible_host: {{ deploy_linux_vm_ip }}" + - " ansible_user: {{ deploy_linux_vm_ansible_user }}" + - " ansible_ssh_common_args: '-o ProxyJump={{ inventory_hostname }} -o StrictHostKeyChecking=accept-new'" + - " os_distribution: {{ deploy_linux_vm_os_distribution }}" + - " os_family: {{ deploy_linux_vm_distro_config.family }}" + - "" + tags: [validate, post-deploy] diff --git a/roles/deploy_linux_vm/tasks/storage.yml b/roles/deploy_linux_vm/tasks/storage.yml new file mode 100644 index 0000000..33c25e2 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/storage.yml @@ -0,0 +1,64 @@ +--- +# ============================================================================= +# Storage Tasks - Create VM Disk Images +# ============================================================================= + +- name: Create primary VM disk from cloud image + command: > + qemu-img create -f qcow2 -F qcow2 + -b {{ deploy_linux_vm_image_cache_path }} + {{ deploy_linux_vm_disk_path }} + {{ deploy_linux_vm_disk_size_gb }}G + args: + creates: "{{ deploy_linux_vm_disk_path }}" + tags: [storage] + +- name: Set proper permissions on VM disk (Debian/Ubuntu) + file: + path: "{{ deploy_linux_vm_disk_path }}" + owner: libvirt-qemu + group: kvm + mode: '0600' + when: ansible_os_family == "Debian" + tags: [storage] + +- name: Set proper permissions on VM disk (RHEL) + file: + path: "{{ deploy_linux_vm_disk_path }}" + owner: qemu + group: qemu + mode: '0600' + when: ansible_os_family == "RedHat" + tags: [storage] + +- name: Create LVM data disk for VM + command: > + qemu-img create -f qcow2 + {{ deploy_linux_vm_images_dir }}/{{ deploy_linux_vm_name }}-lvm.qcow2 + 30G + args: + creates: "{{ deploy_linux_vm_images_dir }}/{{ deploy_linux_vm_name }}-lvm.qcow2" + when: deploy_linux_vm_use_lvm | bool + tags: [storage, lvm] + +- name: Set proper permissions on LVM disk (Debian/Ubuntu) + file: + path: "{{ deploy_linux_vm_images_dir }}/{{ deploy_linux_vm_name }}-lvm.qcow2" + owner: libvirt-qemu + group: kvm + mode: '0600' + when: + - deploy_linux_vm_use_lvm | bool + - ansible_os_family == "Debian" + tags: [storage, lvm] + +- name: Set proper permissions on LVM disk (RHEL) + file: + path: "{{ deploy_linux_vm_images_dir }}/{{ deploy_linux_vm_name }}-lvm.qcow2" + owner: qemu + group: qemu + mode: '0600' + when: + - deploy_linux_vm_use_lvm | bool + - ansible_os_family == "RedHat" + tags: [storage, lvm] diff --git a/roles/deploy_linux_vm/tasks/validate.yml b/roles/deploy_linux_vm/tasks/validate.yml new file mode 100644 index 0000000..ee4cac9 --- /dev/null +++ b/roles/deploy_linux_vm/tasks/validate.yml @@ -0,0 +1,82 @@ +--- +# ============================================================================= +# Validation Tasks - Pre-flight Checks +# ============================================================================= + +- name: Validate distribution selection + assert: + that: + - deploy_linux_vm_os_distribution is defined + - deploy_linux_vm_os_distribution in deploy_linux_vm_cloud_images.keys() + fail_msg: | + Invalid distribution '{{ deploy_linux_vm_os_distribution }}'. + Supported distributions: {{ deploy_linux_vm_cloud_images.keys() | list | join(', ') }} + success_msg: "Distribution '{{ deploy_linux_vm_os_distribution }}' is valid" + tags: [validate, preflight] + +- name: Set distribution facts + set_fact: + deploy_linux_vm_distro_config: "{{ deploy_linux_vm_cloud_images[deploy_linux_vm_os_distribution] }}" + deploy_linux_vm_image_cache_path: "{{ deploy_linux_vm_images_dir }}/{{ deploy_linux_vm_cloud_images[deploy_linux_vm_os_distribution].cache_name }}" + tags: [always] + +- name: Display deployment information + debug: + msg: + - "=== VM Deployment Configuration ===" + - "VM Name: {{ deploy_linux_vm_name }}" + - "Distribution: {{ deploy_linux_vm_os_distribution }}" + - "OS Family: {{ deploy_linux_vm_distro_config.family }}" + - "Package Manager: {{ deploy_linux_vm_distro_config.package_manager }}" + - "vCPUs: {{ deploy_linux_vm_vcpus }}" + - "Memory: {{ deploy_linux_vm_memory_mb }} MB" + - "Disk: {{ deploy_linux_vm_disk_size_gb }} GB" + - "LVM Enabled: {{ deploy_linux_vm_use_lvm }}" + tags: [validate, preflight] + +- name: Validate VM name + assert: + that: + - deploy_linux_vm_name is defined + - deploy_linux_vm_name | length > 0 + - deploy_linux_vm_name is match('^[a-zA-Z0-9_-]+$') + fail_msg: "VM name must be defined and contain only alphanumeric characters, hyphens, or underscores" + success_msg: "VM name '{{ deploy_linux_vm_name }}' is valid" + tags: [validate, preflight] + +- name: Check if VM already exists + command: virsh dominfo {{ deploy_linux_vm_name }} + register: deploy_linux_vm_exists_check + failed_when: false + changed_when: false + tags: [validate, preflight] + +- name: Fail if VM already exists + fail: + msg: "VM '{{ deploy_linux_vm_name }}' already exists on hypervisor. Please choose a different name or destroy the existing VM." + when: deploy_linux_vm_exists_check.rc == 0 + tags: [validate, preflight] + +- name: Verify virtualization support + command: virt-host-validate qemu + register: deploy_linux_vm_virt_validation + failed_when: false + changed_when: false + tags: [validate, preflight] + +- name: Display virtualization validation results + debug: + var: deploy_linux_vm_virt_validation.stdout_lines + tags: [validate, preflight] + +- name: Validate LVM configuration + assert: + that: + - deploy_linux_vm_lvm_vg_name is defined + - deploy_linux_vm_lvm_pv_device is defined + - deploy_linux_vm_lvm_volumes is defined + - deploy_linux_vm_lvm_volumes | length > 0 + fail_msg: "LVM is enabled but configuration is incomplete" + success_msg: "LVM configuration is valid" + when: deploy_linux_vm_use_lvm | bool + tags: [validate, preflight, lvm] diff --git a/roles/deploy_linux_vm/templates/meta-data.j2 b/roles/deploy_linux_vm/templates/meta-data.j2 new file mode 100644 index 0000000..4954f5f --- /dev/null +++ b/roles/deploy_linux_vm/templates/meta-data.j2 @@ -0,0 +1,2 @@ +instance-id: {{ deploy_linux_vm_name }} +local-hostname: {{ deploy_linux_vm_hostname }} diff --git a/roles/deploy_linux_vm/templates/user-data-debian.j2 b/roles/deploy_linux_vm/templates/user-data-debian.j2 new file mode 100644 index 0000000..bdfe67c --- /dev/null +++ b/roles/deploy_linux_vm/templates/user-data-debian.j2 @@ -0,0 +1,121 @@ +#cloud-config +hostname: {{ deploy_linux_vm_hostname }} +fqdn: {{ deploy_linux_vm_hostname }}.{{ deploy_linux_vm_domain }} +manage_etc_hosts: true + +# Create ansible user with sudo privileges +users: + - name: {{ deploy_linux_vm_ansible_user }} + groups: sudo + shell: {{ deploy_linux_vm_ansible_user_shell }} + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ deploy_linux_vm_ansible_user_ssh_key }} + - name: root + lock_passwd: false + +# Set root password (for emergency console access) +chpasswd: + list: | + root:{{ deploy_linux_vm_root_password }} + expire: false + +# SSH configuration +ssh_pwauth: false +disable_root: false + +# Install essential packages per CLAUDE.md guidelines +packages: + - sudo +{% for package in deploy_linux_vm_essential_packages %} + - {{ package }} +{% endfor %} +{% for package in deploy_linux_vm_security_packages %} + - {{ package }} +{% endfor %} + - auditd + - 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: | + # SSH Security Configuration - CLAUDE.md Compliance + PermitRootLogin {{ deploy_linux_vm_ssh_permit_root_login }} + PasswordAuthentication {{ deploy_linux_vm_ssh_password_authentication }} + PubkeyAuthentication {{ deploy_linux_vm_ssh_pubkey_authentication }} + MaxAuthTries {{ deploy_linux_vm_ssh_max_auth_tries }} + MaxSessions {{ deploy_linux_vm_ssh_max_sessions }} + ClientAliveInterval {{ deploy_linux_vm_ssh_client_alive_interval }} + ClientAliveCountMax {{ deploy_linux_vm_ssh_client_alive_count_max }} + + # Disable GSSAPI Authentication (per requirements) + GSSAPIAuthentication {{ deploy_linux_vm_ssh_gssapi_authentication }} + GSSAPICleanupCredentials {{ deploy_linux_vm_ssh_gssapi_cleanup_credentials }} + + # Additional hardening + PermitEmptyPasswords no + ChallengeResponseAuthentication no + UsePAM yes + X11Forwarding no + PrintMotd no + AcceptEnv LANG LC_* + permissions: '0644' + + - path: /etc/sudoers.d/{{ deploy_linux_vm_ansible_user }} + content: | + {{ deploy_linux_vm_ansible_user }} 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 "{{ 'true' if deploy_linux_vm_automatic_reboot else '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: + - systemctl enable ssh + - systemctl restart ssh + - systemctl enable chrony + - systemctl start chrony +{% if deploy_linux_vm_enable_firewall %} + - ufw --force enable + - ufw allow ssh +{% endif %} +{% if deploy_linux_vm_enable_auditd %} + - systemctl enable auditd + - systemctl start auditd +{% endif %} + - growpart /dev/vda 1 || true + - resize2fs /dev/vda1 || true + +package_update: {{ deploy_linux_vm_package_update | lower }} +package_upgrade: {{ deploy_linux_vm_package_upgrade | lower }} +package_reboot_if_required: {{ deploy_linux_vm_package_reboot_if_required | lower }} + +timezone: {{ deploy_linux_vm_timezone }} +locale: {{ deploy_linux_vm_locale }} + +output: + all: '| tee -a /var/log/cloud-init-output.log' + +final_message: "{{ deploy_linux_vm_os_distribution }} VM deployment completed. System is ready after $UPTIME seconds." diff --git a/roles/deploy_linux_vm/templates/user-data-rhel.j2 b/roles/deploy_linux_vm/templates/user-data-rhel.j2 new file mode 100644 index 0000000..eab8922 --- /dev/null +++ b/roles/deploy_linux_vm/templates/user-data-rhel.j2 @@ -0,0 +1,127 @@ +#cloud-config +hostname: {{ deploy_linux_vm_hostname }} +fqdn: {{ deploy_linux_vm_hostname }}.{{ deploy_linux_vm_domain }} +manage_etc_hosts: true + +# Create ansible user with sudo privileges +users: + - name: {{ deploy_linux_vm_ansible_user }} + groups: wheel + shell: {{ deploy_linux_vm_ansible_user_shell }} + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ deploy_linux_vm_ansible_user_ssh_key }} + - name: root + lock_passwd: false + +# Set root password (for emergency console access) +chpasswd: + list: | + root:{{ deploy_linux_vm_root_password }} + expire: false + +# SSH configuration +ssh_pwauth: false +disable_root: false + +# Install essential packages per CLAUDE.md guidelines +packages: + - sudo +{% for package in deploy_linux_vm_essential_packages %} + - {{ package }} +{% endfor %} +{% for package in deploy_linux_vm_security_packages %} + - {{ package }} +{% endfor %} + - audit + - firewalld + - lvm2 + - cloud-utils-growpart + - gdisk + - dnf-automatic + - policycoreutils-python-utils + +# Security configuration files +write_files: + - path: /etc/ssh/sshd_config.d/99-security.conf + content: | + # SSH Security Configuration - CLAUDE.md Compliance + PermitRootLogin {{ deploy_linux_vm_ssh_permit_root_login }} + PasswordAuthentication {{ deploy_linux_vm_ssh_password_authentication }} + PubkeyAuthentication {{ deploy_linux_vm_ssh_pubkey_authentication }} + MaxAuthTries {{ deploy_linux_vm_ssh_max_auth_tries }} + MaxSessions {{ deploy_linux_vm_ssh_max_sessions }} + ClientAliveInterval {{ deploy_linux_vm_ssh_client_alive_interval }} + ClientAliveCountMax {{ deploy_linux_vm_ssh_client_alive_count_max }} + + # Disable GSSAPI Authentication (per requirements) + GSSAPIAuthentication {{ deploy_linux_vm_ssh_gssapi_authentication }} + GSSAPICleanupCredentials {{ deploy_linux_vm_ssh_gssapi_cleanup_credentials }} + + # Additional hardening + PermitEmptyPasswords no + ChallengeResponseAuthentication no + UsePAM yes + X11Forwarding no + permissions: '0644' + + - path: /etc/sudoers.d/{{ deploy_linux_vm_ansible_user }} + content: | + {{ deploy_linux_vm_ansible_user }} ALL=(ALL) NOPASSWD:ALL + permissions: '0440' + + - path: /etc/dnf/automatic.conf + content: | + [commands] + upgrade_type = security + download_updates = yes + apply_updates = {{ 'yes' if deploy_linux_vm_enable_automatic_updates else 'no' }} + + [emitters] + emit_via = stdio + + [email] + email_from = root@{{ deploy_linux_vm_hostname }}.{{ deploy_linux_vm_domain }} + + [base] + debuglevel = 1 + permissions: '0644' + +# System configuration commands +runcmd: + - systemctl enable sshd + - systemctl restart sshd + - systemctl enable chronyd + - systemctl start chronyd +{% if deploy_linux_vm_enable_firewall %} + - systemctl enable firewalld + - systemctl start firewalld + - firewall-cmd --permanent --add-service=ssh + - firewall-cmd --reload +{% endif %} +{% if deploy_linux_vm_enable_auditd %} + - systemctl enable auditd + - systemctl start auditd +{% endif %} +{% if deploy_linux_vm_enable_automatic_updates %} + - systemctl enable dnf-automatic.timer + - systemctl start dnf-automatic.timer +{% endif %} +{% if deploy_linux_vm_enable_selinux %} + - setenforce 1 + - sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config +{% endif %} + - growpart /dev/vda 1 || true + - xfs_growfs / || resize2fs /dev/vda1 || true + +package_update: {{ deploy_linux_vm_package_update | lower }} +package_upgrade: {{ deploy_linux_vm_package_upgrade | lower }} +package_reboot_if_required: {{ deploy_linux_vm_package_reboot_if_required | lower }} + +timezone: {{ deploy_linux_vm_timezone }} +locale: {{ deploy_linux_vm_locale }} + +output: + all: '| tee -a /var/log/cloud-init-output.log' + +final_message: "{{ deploy_linux_vm_os_distribution }} VM deployment completed. System is ready after $UPTIME seconds." diff --git a/roles/deploy_linux_vm/templates/user-data-suse.j2 b/roles/deploy_linux_vm/templates/user-data-suse.j2 new file mode 100644 index 0000000..737f523 --- /dev/null +++ b/roles/deploy_linux_vm/templates/user-data-suse.j2 @@ -0,0 +1,100 @@ +#cloud-config +hostname: {{ deploy_linux_vm_hostname }} +fqdn: {{ deploy_linux_vm_hostname }}.{{ deploy_linux_vm_domain }} +manage_etc_hosts: true + +# Create ansible user with sudo privileges +users: + - name: {{ deploy_linux_vm_ansible_user }} + groups: wheel + shell: {{ deploy_linux_vm_ansible_user_shell }} + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - {{ deploy_linux_vm_ansible_user_ssh_key }} + - name: root + lock_passwd: false + +# Set root password (for emergency console access) +chpasswd: + list: | + root:{{ deploy_linux_vm_root_password }} + expire: false + +# SSH configuration +ssh_pwauth: false +disable_root: false + +# Install essential packages +packages: + - sudo +{% for package in deploy_linux_vm_essential_packages %} + - {{ package }} +{% endfor %} +{% for package in deploy_linux_vm_security_packages %} + - {{ package }} +{% endfor %} + - audit + - firewalld + - lvm2 + - cloud-utils-growpart + - gdisk + +# Security configuration files +write_files: + - path: /etc/ssh/sshd_config.d/99-security.conf + content: | + # SSH Security Configuration - CLAUDE.md Compliance + PermitRootLogin {{ deploy_linux_vm_ssh_permit_root_login }} + PasswordAuthentication {{ deploy_linux_vm_ssh_password_authentication }} + PubkeyAuthentication {{ deploy_linux_vm_ssh_pubkey_authentication }} + MaxAuthTries {{ deploy_linux_vm_ssh_max_auth_tries }} + MaxSessions {{ deploy_linux_vm_ssh_max_sessions }} + ClientAliveInterval {{ deploy_linux_vm_ssh_client_alive_interval }} + ClientAliveCountMax {{ deploy_linux_vm_ssh_client_alive_count_max }} + + # Disable GSSAPI Authentication (per requirements) + GSSAPIAuthentication {{ deploy_linux_vm_ssh_gssapi_authentication }} + GSSAPICleanupCredentials {{ deploy_linux_vm_ssh_gssapi_cleanup_credentials }} + + # Additional hardening + PermitEmptyPasswords no + ChallengeResponseAuthentication no + UsePAM yes + X11Forwarding no + permissions: '0644' + + - path: /etc/sudoers.d/{{ deploy_linux_vm_ansible_user }} + content: | + {{ deploy_linux_vm_ansible_user }} ALL=(ALL) NOPASSWD:ALL + permissions: '0440' + +# System configuration commands +runcmd: + - systemctl enable sshd + - systemctl restart sshd + - systemctl enable chronyd + - systemctl start chronyd +{% if deploy_linux_vm_enable_firewall %} + - systemctl enable firewalld + - systemctl start firewalld + - firewall-cmd --permanent --add-service=ssh + - firewall-cmd --reload +{% endif %} +{% if deploy_linux_vm_enable_auditd %} + - systemctl enable auditd + - systemctl start auditd +{% endif %} + - growpart /dev/vda 1 || true + - xfs_growfs / || resize2fs /dev/vda1 || btrfs filesystem resize max / || true + +package_update: {{ deploy_linux_vm_package_update | lower }} +package_upgrade: {{ deploy_linux_vm_package_upgrade | lower }} +package_reboot_if_required: {{ deploy_linux_vm_package_reboot_if_required | lower }} + +timezone: {{ deploy_linux_vm_timezone }} +locale: {{ deploy_linux_vm_locale }} + +output: + all: '| tee -a /var/log/cloud-init-output.log' + +final_message: "{{ deploy_linux_vm_os_distribution }} VM deployment completed. System is ready after $UPTIME seconds." diff --git a/roles/deploy_linux_vm/vars/main.yml b/roles/deploy_linux_vm/vars/main.yml new file mode 100644 index 0000000..cfc693c --- /dev/null +++ b/roles/deploy_linux_vm/vars/main.yml @@ -0,0 +1,157 @@ +--- +# ============================================================================= +# Deploy Linux VM Role - Role Variables +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Cloud Image Repository Configuration +# ----------------------------------------------------------------------------- +deploy_linux_vm_cloud_images: + # Debian + 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 + ubuntu-20.04: + url: "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img" + checksum_url: "https://cloud-images.ubuntu.com/focal/current/SHA256SUMS" + checksum_type: "sha256" + os_variant: "ubuntu20.04" + cache_name: "ubuntu-20.04-server-cloudimg-amd64.img" + 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" + + # RHEL (requires subscription) + rhel-8: + url: "https://access.redhat.com/downloads/content/rhel/8/x86_64/latest/rhel-8-x86_64-kvm.qcow2" + os_variant: "rhel8.0" + cache_name: "rhel-8-x86_64-kvm.qcow2" + package_manager: "dnf" + family: "rhel" + note: "Requires Red Hat subscription and manual download" + + rhel-9: + url: "https://access.redhat.com/downloads/content/rhel/9/x86_64/latest/rhel-9-x86_64-kvm.qcow2" + os_variant: "rhel9.0" + cache_name: "rhel-9-x86_64-kvm.qcow2" + package_manager: "dnf" + family: "rhel" + note: "Requires Red Hat subscription and manual download" + + # CentOS Stream + centos-stream-8: + url: "https://cloud.centos.org/centos/8-stream/x86_64/images/CentOS-Stream-GenericCloud-8-latest.x86_64.qcow2" + checksum_url: "https://cloud.centos.org/centos/8-stream/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "centos-stream8" + cache_name: "centos-stream-8-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + centos-stream-9: + url: "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2" + checksum_url: "https://cloud.centos.org/centos/9-stream/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "centos-stream9" + cache_name: "centos-stream-9-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + # Rocky Linux + rocky-8: + url: "https://download.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2" + checksum_url: "https://download.rockylinux.org/pub/rocky/8/images/x86_64/CHECKSUM" + checksum_type: "sha256" + os_variant: "rocky8" + cache_name: "rocky-8-genericcloud-amd64.qcow2" + package_manager: "dnf" + family: "rhel" + + 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 + almalinux-8: + url: "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2" + checksum_url: "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/CHECKSUM" + checksum_type: "sha256" + os_variant: "almalinux8" + cache_name: "almalinux-8-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" + + # SLES (requires registration) + sles-15: + url: "https://download.suse.com/Download?buildid=XXXXX" + os_variant: "sles15" + cache_name: "sles-15-genericcloud-amd64.qcow2" + package_manager: "zypper" + family: "suse" + note: "Requires SUSE subscription and manual download" + + # openSUSE Leap + opensuse-leap-15.5: + url: "https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.x86_64-Cloud.qcow2" + checksum_url: "https://download.opensuse.org/distribution/leap/15.5/appliances/openSUSE-Leap-15.5-Minimal-VM.x86_64-Cloud.qcow2.sha256" + checksum_type: "sha256" + os_variant: "opensuse15.5" + cache_name: "opensuse-leap-15.5-minimal-vm-amd64.qcow2" + package_manager: "zypper" + family: "suse" + + opensuse-leap-15.6: + url: "https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.x86_64-Cloud.qcow2" + checksum_url: "https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.x86_64-Cloud.qcow2.sha256" + checksum_type: "sha256" + os_variant: "opensuse15.6" + cache_name: "opensuse-leap-15.6-minimal-vm-amd64.qcow2" + package_manager: "zypper" + family: "suse"