From fc0570846661ef486395717dd362f5904fccc6f1 Mon Sep 17 00:00:00 2001 From: Sandwich Date: Wed, 11 Feb 2026 05:37:18 +0100 Subject: [PATCH] refactor(vars): add system/hypervisor dict inputs --- inventory_example.yml | 63 +- inventory_libvirt_example.yml | 69 +- main.yml | 8 +- roles/bootstrap/tasks/almalinux.yml | 41 +- roles/bootstrap/tasks/alpine.yml | 33 + roles/bootstrap/tasks/debian.yml | 46 +- roles/bootstrap/tasks/fedora.yml | 35 +- roles/bootstrap/tasks/main.yml | 24 +- roles/bootstrap/tasks/opensuse.yml | 33 + roles/bootstrap/tasks/rhel.yml | 5 +- roles/bootstrap/tasks/rocky.yml | 38 +- roles/bootstrap/tasks/ubuntu.yml | 47 +- roles/bootstrap/tasks/void.yml | 33 + roles/bootstrap/vars/main.yml | 623 ++++++------------ roles/cis/defaults/main.yml | 6 +- roles/cis/tasks/crypto.yml | 6 +- roles/cis/tasks/security_lines.yml | 32 +- roles/cis/tasks/sshd.yml | 48 +- roles/cleanup/defaults/main.yml | 6 +- roles/cleanup/tasks/libvirt.yml | 2 + roles/cleanup/tasks/proxmox.yml | 4 +- roles/cleanup/tasks/virtual.yml | 193 ++++++ roles/cleanup/tasks/vmware.yml | 4 +- roles/cleanup/tasks/xen.yml | 58 ++ roles/configuration/defaults/main.yml | 5 + roles/configuration/tasks/banner.yml | 56 ++ roles/configuration/tasks/bootloader.yml | 13 +- roles/configuration/tasks/encryption.yml | 12 +- roles/configuration/tasks/encryption/tpm2.yml | 2 +- roles/configuration/tasks/extras.yml | 5 +- roles/configuration/tasks/fstab.yml | 18 +- roles/configuration/tasks/grub.yml | 4 +- roles/configuration/tasks/locales.yml | 22 +- roles/configuration/tasks/main.yml | 1 + roles/configuration/tasks/network.yml | 107 ++- roles/configuration/tasks/selinux.yml | 4 +- roles/configuration/tasks/services.yml | 71 +- roles/configuration/tasks/users.yml | 4 +- roles/configuration/templates/network.j2 | 8 +- roles/environment/tasks/main.yml | 116 +++- roles/global_defaults/defaults/main.yml | 35 +- roles/global_defaults/tasks/hypervisor.yml | 42 ++ roles/global_defaults/tasks/main.yml | 127 ++-- roles/global_defaults/tasks/system.yml | 291 ++++++++ roles/global_defaults/tasks/validation.yml | 234 +++++++ roles/partitioning/defaults/main.yml | 2 +- roles/partitioning/tasks/btrfs.yml | 16 +- roles/partitioning/tasks/ext4.yml | 24 +- roles/partitioning/tasks/extra_disks.yml | 85 +++ roles/partitioning/tasks/main.yml | 23 +- roles/partitioning/tasks/xfs.yml | 10 +- roles/system_check/tasks/main.yml | 188 ++++++ roles/virtualization/defaults/main.yml | 9 +- roles/virtualization/tasks/libvirt.yml | 49 +- roles/virtualization/tasks/proxmox.yml | 38 +- roles/virtualization/tasks/vmware.yml | 54 +- roles/virtualization/tasks/xen.yml | 77 +++ .../templates/cloud-network-config.yml.j2 | 18 +- roles/virtualization/templates/vm.xml.j2 | 14 +- templates/xen.cfg.j2 | 17 + vars_baremetal_example.yml | 5 +- vars_example.yml | 30 +- 62 files changed, 2422 insertions(+), 871 deletions(-) create mode 100644 roles/bootstrap/tasks/alpine.yml create mode 100644 roles/bootstrap/tasks/opensuse.yml create mode 100644 roles/bootstrap/tasks/void.yml create mode 100644 roles/cleanup/tasks/xen.yml create mode 100644 roles/configuration/defaults/main.yml create mode 100644 roles/configuration/tasks/banner.yml create mode 100644 roles/global_defaults/tasks/hypervisor.yml create mode 100644 roles/global_defaults/tasks/system.yml create mode 100644 roles/global_defaults/tasks/validation.yml create mode 100644 roles/partitioning/tasks/extra_disks.yml create mode 100644 roles/system_check/tasks/main.yml create mode 100644 roles/virtualization/tasks/xen.yml create mode 100644 templates/xen.cfg.j2 diff --git a/inventory_example.yml b/inventory_example.yml index 7272c9b..3f8f153 100644 --- a/inventory_example.yml +++ b/inventory_example.yml @@ -2,44 +2,59 @@ all: vars: install_type: "virtual" - hypervisor: "proxmox" + hypervisor: + type: "proxmox" + url: "pve01.example.com" + username: "root@pam" + password: "CHANGE_ME" + node: "pve01" + storage: "local-lvm" install_drive: "/dev/sda" boot_iso: "local:iso/archlinux-x86_64.iso" - vm_nif: "vmbr0" children: proxmox: hosts: app01.example.com: ansible_host: 10.0.0.10 - hostname: "app01.example.com" os: "archlinux" filesystem: "btrfs" - vm_id: 100 - vm_cpus: 2 - vm_memory: 4096 - vm_size: 40 - vm_ip: 10.0.0.10 - vm_nms: 24 - vm_gw: 10.0.0.1 - vm_dns: - - 1.1.1.1 - - 1.0.0.1 + system: + name: "app01.example.com" + id: 100 + cpus: 2 + memory_mb: 4096 + network: "vmbr0" + ip: 10.0.0.10 + prefix: 24 + gateway: 10.0.0.1 + dns_servers: + - 1.1.1.1 + - 1.0.0.1 + disks: + - size: 40 + - size: 80 + mount: /data + fstype: xfs extra_packages: - jq - tmux db01.example.com: ansible_host: 10.0.0.11 - hostname: "db01.example.com" - os: "rhel9" + os: "rhel" + os_version: "9" filesystem: "xfs" - vm_id: 101 - vm_cpus: 4 - vm_memory: 8192 - vm_size: 80 - vm_ip: 10.0.0.11 - vm_nms: 24 - vm_gw: 10.0.0.1 - vm_dns: "1.1.1.1,1.0.0.1" + system: + name: "db01.example.com" + id: 101 + cpus: 4 + memory_mb: 8192 + network: "vmbr0" + ip: 10.0.0.11 + prefix: 24 + gateway: 10.0.0.1 + dns_servers: "1.1.1.1,1.0.0.1" + disks: + - size: 80 rhel_iso: "local:iso/rhel-9.4-x86_64-dvd.iso" luks_enabled: true luks_passphrase: "CHANGE_ME" @@ -47,4 +62,4 @@ all: luks_keyfile_size: 128 cis: true selinux: false - firewalld_enabled: false + firewall_enabled: false diff --git a/inventory_libvirt_example.yml b/inventory_libvirt_example.yml index cf22ce2..3a7c0a2 100644 --- a/inventory_libvirt_example.yml +++ b/inventory_libvirt_example.yml @@ -2,7 +2,8 @@ all: vars: install_type: "virtual" - hypervisor: "libvirt" + hypervisor: + type: "libvirt" install_drive: "/dev/vda" boot_iso: "/var/lib/libvirt/images/archlinux-x86_64.iso" children: @@ -10,47 +11,55 @@ all: hosts: web01.example.com: ansible_host: 192.168.122.10 - hostname: "web01.example.com" - os: "debian12" + os: "debian" + os_version: "12" filesystem: "ext4" - vm_cpus: 2 - vm_memory: 2048 - vm_size: 30 - vm_ip: 192.168.122.10 - vm_nms: 24 - vm_gw: 192.168.122.1 - vm_dns: 1.1.1.1 + system: + name: "web01.example.com" + cpus: 2 + memory_mb: 2048 + ip: 192.168.122.10 + prefix: 24 + gateway: 192.168.122.1 + dns_servers: 1.1.1.1 + disks: + - size: 30 extra_packages: - nginx - fail2ban vault01.example.com: ansible_host: 192.168.122.11 - hostname: "vault01.example.com" os: "ubuntu-lts" filesystem: "btrfs" - vm_cpus: 2 - vm_memory: 4096 - vm_size: 40 - vm_ip: 192.168.122.11 - vm_nms: 24 - vm_gw: 192.168.122.1 - vm_dns_search: "example.com" + system: + name: "vault01.example.com" + cpus: 2 + memory_mb: 4096 + ip: 192.168.122.11 + prefix: 24 + gateway: 192.168.122.1 + dns_search: "example.com" + disks: + - size: 40 luks_enabled: true luks_passphrase: "CHANGE_ME" luks_auto_decrypt_method: "keyfile" - firewalld_enabled: false + firewall_enabled: false rhel9.example.com: ansible_host: 192.168.122.12 - hostname: "rhel9.example.com" - os: "rhel9" + os: "rhel" + os_version: "9" filesystem: "xfs" - vm_cpus: 4 - vm_memory: 8192 - vm_size: 80 - vm_ip: 192.168.122.12 - vm_nms: 24 - vm_gw: 192.168.122.1 - vm_dns: "1.1.1.1,1.0.0.1" - vm_path: "/srv/libvirt/images" + system: + name: "rhel9.example.com" + cpus: 4 + memory_mb: 8192 + vlan: "100" + ip: 192.168.122.12 + prefix: 24 + gateway: 192.168.122.1 + dns_servers: "1.1.1.1,1.0.0.1" + path: "/srv/libvirt/images" + disks: + - size: 80 rhel_iso: "/var/lib/libvirt/images/rhel-9.4-x86_64-dvd.iso" - vlan_name: "100" diff --git a/main.yml b/main.yml index 100f27c..2453593 100644 --- a/main.yml +++ b/main.yml @@ -1,7 +1,7 @@ --- - name: Create and configure VMs hosts: all - strategy: free # noqa: run-once[play] + strategy: free # noqa: run-once[play] gather_facts: false become: true vars_prompt: @@ -29,6 +29,10 @@ ansible.builtin.import_role: name: global_defaults + - name: Perform safety checks + ansible.builtin.import_role: + name: system_check + roles: - role: virtualization when: install_type == "virtual" @@ -62,7 +66,7 @@ post_reboot_can_connect: >- {{ (ansible_connection | default('ssh')) != 'ssh' - or (vm_ip is defined and (vm_ip | string | length) > 0) + or ((system_cfg.ip | default('') | string | length) > 0) or ( install_type == 'physical' and (ansible_host | default('') | string | length) > 0 diff --git a/roles/bootstrap/tasks/almalinux.yml b/roles/bootstrap/tasks/almalinux.yml index 276cda0..973067a 100644 --- a/roles/bootstrap/tasks/almalinux.yml +++ b/roles/bootstrap/tasks/almalinux.yml @@ -1,20 +1,35 @@ --- -- name: Bootstrap AlmaLinux 9 +- name: Bootstrap AlmaLinux vars: - bootstrap_alma_extra: >- + bootstrap_almalinux_extra: >- {{ lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ') }} - ansible.builtin.command: "{{ item }}" - loop: - - >- - dnf --releasever=9 --best --repo=alma-baseos --installroot=/mnt - --setopt=install_weak_deps=False groupinstall -y base core - - ln -sf /run/NetworkManager/resolv.conf /mnt/etc/resolv.conf - - >- - {{ chroot_command }} /mnt dnf --releasever=9 --setopt=install_weak_deps=False - install -y {{ bootstrap_alma_extra }} - register: bootstrap_result - changed_when: bootstrap_result.rc == 0 + block: + - name: Install AlmaLinux base system + ansible.builtin.command: >- + dnf --releasever={{ os_version }} --best --repo=baseos --repo=appstream + --installroot=/mnt --setopt=install_weak_deps=False + groupinstall -y core + register: bootstrap_almalinux_base_result + changed_when: bootstrap_almalinux_base_result.rc == 0 + + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + + - name: Install extra packages + ansible.builtin.command: >- + {{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False + install -y {{ bootstrap_almalinux_extra }} + register: bootstrap_almalinux_extra_result + changed_when: bootstrap_almalinux_extra_result.rc == 0 + + - name: Reinstall kernel core + ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core" + register: bootstrap_almalinux_kernel_result + changed_when: bootstrap_almalinux_kernel_result.rc == 0 diff --git a/roles/bootstrap/tasks/alpine.yml b/roles/bootstrap/tasks/alpine.yml new file mode 100644 index 0000000..5aaabf6 --- /dev/null +++ b/roles/bootstrap/tasks/alpine.yml @@ -0,0 +1,33 @@ +--- +- name: Bootstrap Alpine Linux + vars: + bootstrap_alpine_packages: >- + {{ + lookup('vars', 'bootstrap_alpine') | reject('equalto', '') | join(' ') + }} + block: + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + force: true + + - name: Install Alpine Linux packages + ansible.builtin.command: > + apk --root /mnt --no-cache add alpine-base + register: bootstrap_alpine_bootstrap_result + changed_when: bootstrap_alpine_bootstrap_result.rc == 0 + + - name: Install extra packages + when: bootstrap_alpine_packages | length > 0 + ansible.builtin.command: > + apk --root /mnt add {{ bootstrap_alpine_packages }} + register: bootstrap_alpine_extra_result + changed_when: bootstrap_alpine_extra_result.rc == 0 + + - name: Install bootloader + ansible.builtin.command: > + apk --root /mnt add grub grub-efi efibootmgr + register: bootstrap_alpine_bootloader_result + changed_when: bootstrap_alpine_bootloader_result.rc == 0 diff --git a/roles/bootstrap/tasks/debian.yml b/roles/bootstrap/tasks/debian.yml index 8b6f5ff..10f598c 100644 --- a/roles/bootstrap/tasks/debian.yml +++ b/roles/bootstrap/tasks/debian.yml @@ -3,13 +3,27 @@ vars: bootstrap_debian_release: >- {{ - 'bullseye' if bootstrap_os_key == 'debian11' - else 'bookworm' if bootstrap_os_key == 'debian12' + 'buster' if (os_version | string) == '10' + else 'bullseye' if (os_version | string) == '11' + else 'bookworm' if (os_version | string) == '12' + else 'trixie' if (os_version | string) == '13' + else 'sid' if (os_version | string) == 'unstable' else 'trixie' }} - bootstrap_debian_base_list: "{{ lookup('vars', bootstrap_var_key).base | default([]) }}" - bootstrap_debian_extra_list: "{{ lookup('vars', bootstrap_var_key).extra | default([]) }}" - bootstrap_debian_base: "{{ bootstrap_debian_base_list | reject('equalto', '') | join(',') }}" + bootstrap_debian_version_packages: >- + {{ + lookup('vars', bootstrap_var_key) + | reject('equalto', '') + | list + }} + bootstrap_debian_base_list: >- + {{ + bootstrap_debian_base + | reject('equalto', '') + | list + }} + bootstrap_debian_extra_list: "{{ bootstrap_debian_version_packages | difference(bootstrap_debian_base_list) }}" + bootstrap_debian_base: "{{ bootstrap_debian_base_list | join(',') }}" bootstrap_debian_extra: >- {{ ( @@ -18,12 +32,20 @@ | reject('equalto', '') | join(' ') }} - ansible.builtin.command: "{{ item }}" - loop: - - >- + block: + - name: Install Debian base system + ansible.builtin.command: >- debootstrap --include={{ bootstrap_debian_base }} {{ bootstrap_debian_release }} /mnt http://deb.debian.org/debian/ - - "{{ chroot_command }} /mnt apt install -y {{ bootstrap_debian_extra }}" - - "{{ chroot_command }} /mnt apt remove -y libcups2 libavahi-common3 libavahi-common-data" - register: bootstrap_result - changed_when: bootstrap_result.rc == 0 + register: bootstrap_debian_base_result + changed_when: bootstrap_debian_base_result.rc == 0 + + - name: Install extra packages + ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra }}" + register: bootstrap_debian_extra_result + changed_when: bootstrap_debian_extra_result.rc == 0 + + - name: Remove unnecessary packages + ansible.builtin.command: "{{ chroot_command }} apt remove -y libcups2 libavahi-common3 libavahi-common-data" + register: bootstrap_debian_remove_result + changed_when: bootstrap_debian_remove_result.rc == 0 diff --git a/roles/bootstrap/tasks/fedora.yml b/roles/bootstrap/tasks/fedora.yml index 8f8a17b..9f353de 100644 --- a/roles/bootstrap/tasks/fedora.yml +++ b/roles/bootstrap/tasks/fedora.yml @@ -1,5 +1,5 @@ --- -- name: Bootstrap Fedora 43 +- name: Bootstrap Fedora vars: bootstrap_fedora_extra: >- {{ @@ -7,16 +7,29 @@ | reject('equalto', '') | join(' ') }} - ansible.builtin.command: "{{ item }}" - loop: - - >- - dnf --releasever=43 --best --repo=fedora --repo=fedora-updates + block: + - name: Install Fedora base system + ansible.builtin.command: >- + dnf --releasever={{ os_version }} --best --repo=fedora --repo=fedora-updates --installroot=/mnt --setopt=install_weak_deps=False groupinstall -y critical-path-base core - - ln -sf /run/NetworkManager/resolv.conf /mnt/etc/resolv.conf - - >- - {{ chroot_command }} /mnt dnf --releasever=43 --setopt=install_weak_deps=False + register: bootstrap_fedora_base_result + changed_when: bootstrap_fedora_base_result.rc == 0 + + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + + - name: Install extra packages + ansible.builtin.command: >- + {{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False install -y {{ bootstrap_fedora_extra }} - - "{{ chroot_command }} /mnt dnf reinstall -y kernel-core" - register: bootstrap_result - changed_when: bootstrap_result.rc == 0 + register: bootstrap_fedora_extra_result + changed_when: bootstrap_fedora_extra_result.rc == 0 + + - name: Reinstall kernel core + ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core" + register: bootstrap_fedora_kernel_result + changed_when: bootstrap_fedora_kernel_result.rc == 0 diff --git a/roles/bootstrap/tasks/main.yml b/roles/bootstrap/tasks/main.yml index ea3e055..da3f27f 100644 --- a/roles/bootstrap/tasks/main.yml +++ b/roles/bootstrap/tasks/main.yml @@ -1,27 +1,35 @@ --- - name: Run OS-specific bootstrap process vars: - bootstrap_os_key: "{{ os | lower }}" - bootstrap_var_key: "{{ 'bootstrap_' + (os | lower | replace('-', '_')) }}" + bootstrap_os_key: "{{ (os_resolved | default(os)) | lower }}" + bootstrap_var_key: "{{ 'bootstrap_' + ((os_resolved | default(os)) | lower | replace('-', '_')) }}" block: - name: Include AlmaLinux bootstrap tasks - when: bootstrap_os_key == 'almalinux' + when: bootstrap_os_key in ['almalinux', 'almalinux8', 'almalinux9', 'almalinux10'] ansible.builtin.include_tasks: almalinux.yml + - name: Include Alpine bootstrap tasks + when: bootstrap_os_key == 'alpine' + ansible.builtin.include_tasks: alpine.yml + - name: Include ArchLinux bootstrap tasks when: bootstrap_os_key == 'archlinux' ansible.builtin.include_tasks: archlinux.yml - name: Include Debian bootstrap tasks - when: bootstrap_os_key in ['debian11', 'debian12', 'debian13'] + when: bootstrap_os_key in ['debian10', 'debian11', 'debian12', 'debian13', 'debianunstable'] ansible.builtin.include_tasks: debian.yml - name: Include Fedora bootstrap tasks - when: bootstrap_os_key == 'fedora' + when: bootstrap_os_key in ['fedora', 'fedora40', 'fedora41', 'fedora42', 'fedora43'] ansible.builtin.include_tasks: fedora.yml + - name: Include openSUSE bootstrap tasks + when: bootstrap_os_key == 'opensuse' + ansible.builtin.include_tasks: opensuse.yml + - name: Include Rocky bootstrap tasks - when: bootstrap_os_key == 'rocky' + when: bootstrap_os_key in ['rocky', 'rocky8', 'rocky9', 'rocky10'] ansible.builtin.include_tasks: rocky.yml - name: Include RHEL bootstrap tasks @@ -31,3 +39,7 @@ - name: Include Ubuntu bootstrap tasks when: bootstrap_os_key in ['ubuntu', 'ubuntu-lts'] ansible.builtin.include_tasks: ubuntu.yml + + - name: Include Void bootstrap tasks + when: bootstrap_os_key == 'void' + ansible.builtin.include_tasks: void.yml diff --git a/roles/bootstrap/tasks/opensuse.yml b/roles/bootstrap/tasks/opensuse.yml new file mode 100644 index 0000000..1fbcf06 --- /dev/null +++ b/roles/bootstrap/tasks/opensuse.yml @@ -0,0 +1,33 @@ +--- +- name: Bootstrap openSUSE + vars: + bootstrap_opensuse_packages: >- + {{ + lookup('vars', 'bootstrap_opensuse') | reject('equalto', '') | join(' ') + }} + block: + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + force: true + + - name: Install openSUSE base packages + ansible.builtin.command: > + zypper --root /mnt --non-interactive install -t pattern patterns-base-base + register: bootstrap_opensuse_base_result + changed_when: bootstrap_opensuse_base_result.rc == 0 + + - name: Install openSUSE extra packages + when: bootstrap_opensuse_packages | length > 0 + ansible.builtin.command: > + zypper --root /mnt --non-interactive install {{ bootstrap_opensuse_packages }} + register: bootstrap_opensuse_extra_result + changed_when: bootstrap_opensuse_extra_result.rc == 0 + + - name: Install bootloader + ansible.builtin.command: > + zypper --root /mnt --non-interactive install grub2 grub2-efi efibootmgr + register: bootstrap_opensuse_bootloader_result + changed_when: bootstrap_opensuse_bootloader_result.rc == 0 diff --git a/roles/bootstrap/tasks/rhel.yml b/roles/bootstrap/tasks/rhel.yml index d155808..167364c 100644 --- a/roles/bootstrap/tasks/rhel.yml +++ b/roles/bootstrap/tasks/rhel.yml @@ -17,7 +17,6 @@ src: /run/NetworkManager/resolv.conf dest: /mnt/etc/resolv.conf state: link - force: true - name: Ensure chroot RHEL DVD directory exists ansible.builtin.file: @@ -34,7 +33,7 @@ state: mounted - name: Rebuild RPM database inside chroot - ansible.builtin.command: "{{ chroot_command }} /mnt rpm --rebuilddb" + ansible.builtin.command: "{{ chroot_command }} rpm --rebuilddb" register: bootstrap_rpm_rebuild_result changed_when: bootstrap_rpm_rebuild_result.rc == 0 @@ -55,7 +54,7 @@ | join(' ') }} ansible.builtin.command: >- - {{ chroot_command }} /mnt dnf --releasever={{ bootstrap_rhel_release }} + {{ chroot_command }} dnf --releasever={{ bootstrap_rhel_release }} --setopt=install_weak_deps=False install -y {{ bootstrap_rhel_extra }} register: bootstrap_result changed_when: bootstrap_result.rc == 0 diff --git a/roles/bootstrap/tasks/rocky.yml b/roles/bootstrap/tasks/rocky.yml index 3f2a288..9a801a1 100644 --- a/roles/bootstrap/tasks/rocky.yml +++ b/roles/bootstrap/tasks/rocky.yml @@ -1,5 +1,5 @@ --- -- name: Bootstrap RockyLinux 9 +- name: Bootstrap Rocky Linux vars: bootstrap_rocky_extra: >- {{ @@ -7,15 +7,29 @@ | reject('equalto', '') | join(' ') }} - ansible.builtin.command: "{{ item }}" - loop: - - >- - dnf --releasever=9 --best --repo=rocky-baseos --installroot=/mnt - --setopt=install_weak_deps=False --setopt=optional_metadata_types=filelists - groupinstall -y base core - - ln -sf /run/NetworkManager/resolv.conf /mnt/etc/resolv.conf - - >- - {{ chroot_command }} /mnt dnf --releasever=9 --setopt=install_weak_deps=False + block: + - name: Install Rocky Linux base system + ansible.builtin.command: >- + dnf --releasever={{ os_version }} --best --repo=baseos --repo=appstream + --installroot=/mnt --setopt=install_weak_deps=False + groupinstall -y core + register: bootstrap_rocky_base_result + changed_when: bootstrap_rocky_base_result.rc == 0 + + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + + - name: Install extra packages + ansible.builtin.command: >- + {{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False install -y {{ bootstrap_rocky_extra }} - register: bootstrap_result - changed_when: bootstrap_result.rc == 0 + register: bootstrap_rocky_extra_result + changed_when: bootstrap_rocky_extra_result.rc == 0 + + - name: Reinstall kernel core + ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core" + register: bootstrap_rocky_kernel_result + changed_when: bootstrap_rocky_kernel_result.rc == 0 diff --git a/roles/bootstrap/tasks/ubuntu.yml b/roles/bootstrap/tasks/ubuntu.yml index 41f3553..c658e97 100644 --- a/roles/bootstrap/tasks/ubuntu.yml +++ b/roles/bootstrap/tasks/ubuntu.yml @@ -3,25 +3,38 @@ vars: bootstrap_ubuntu_release: >- {{ 'plucky' if bootstrap_os_key == 'ubuntu' else 'noble' }} - bootstrap_ubuntu_base_list: "{{ lookup('vars', bootstrap_var_key).base | default([]) }}" - bootstrap_ubuntu_extra_list: "{{ lookup('vars', bootstrap_var_key).extra | default([]) }}" - bootstrap_ubuntu_base: "{{ bootstrap_ubuntu_base_list | reject('equalto', '') | join(',') }}" bootstrap_ubuntu_extra: >- {{ - ( - bootstrap_ubuntu_extra_list - ) + lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ') }} - ansible.builtin.command: "{{ item }}" - loop: - - >- - debootstrap --include={{ bootstrap_ubuntu_base }} - {{ bootstrap_ubuntu_release }} /mnt http://archive.ubuntu.com/ubuntu/ - - ln -sf /run/NetworkManager/resolv.conf /mnt/etc/resolv.conf - - "{{ chroot_command }} /mnt sed -i '1s|$| universe|' /etc/apt/sources.list" - - "{{ chroot_command }} /mnt apt update" - - "{{ chroot_command }} /mnt apt install -y {{ bootstrap_ubuntu_extra }}" - register: bootstrap_result - changed_when: bootstrap_result.rc == 0 + block: + - name: Install Ubuntu base system + ansible.builtin.command: >- + debootstrap --include=linux-image-generic + {{ bootstrap_ubuntu_release }} /mnt + http://archive.ubuntu.com/ubuntu/ + register: bootstrap_ubuntu_base_result + changed_when: bootstrap_ubuntu_base_result.rc == 0 + + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + + - name: Enable universe repository + ansible.builtin.command: "{{ chroot_command }} sed -i '1s|$| universe|' /etc/apt/sources.list" + register: bootstrap_ubuntu_repo_result + changed_when: bootstrap_ubuntu_repo_result.rc == 0 + + - name: Update package lists + ansible.builtin.command: "{{ chroot_command }} apt update" + register: bootstrap_ubuntu_update_result + changed_when: bootstrap_ubuntu_update_result.rc == 0 + + - name: Install extra packages + ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra }}" + register: bootstrap_ubuntu_extra_result + changed_when: bootstrap_ubuntu_extra_result.rc == 0 diff --git a/roles/bootstrap/tasks/void.yml b/roles/bootstrap/tasks/void.yml new file mode 100644 index 0000000..07965c1 --- /dev/null +++ b/roles/bootstrap/tasks/void.yml @@ -0,0 +1,33 @@ +--- +- name: Bootstrap Void Linux + vars: + bootstrap_void_packages: >- + {{ + lookup('vars', 'bootstrap_void') | reject('equalto', '') | join(' ') + }} + block: + - name: Ensure chroot has resolv.conf + ansible.builtin.file: + src: /run/NetworkManager/resolv.conf + dest: /mnt/etc/resolv.conf + state: link + force: true + + - name: Install Void Linux base packages + ansible.builtin.command: > + xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current void-repo-nonfree base-system + register: bootstrap_void_base_result + changed_when: bootstrap_void_base_result.rc == 0 + + - name: Install extra packages + when: bootstrap_void_packages | length > 0 + ansible.builtin.command: > + xbps-install -Su -r /mnt {{ bootstrap_void_packages }} + register: bootstrap_void_extra_result + changed_when: bootstrap_void_extra_result.rc == 0 + + - name: Install bootloader + ansible.builtin.command: > + xbps-install -Sy -r /mnt grub-x86_64-efi efibootmgr + register: bootstrap_void_bootloader_result + changed_when: bootstrap_void_bootloader_result.rc == 0 diff --git a/roles/bootstrap/vars/main.yml b/roles/bootstrap/vars/main.yml index e2f6399..3e10958 100644 --- a/roles/bootstrap/vars/main.yml +++ b/roles/bootstrap/vars/main.yml @@ -1,21 +1,20 @@ --- -bootstrap_almalinux: +bootstrap_rhel_base: - bind-utils - - dbus-daemon - dhcp-client - efibootmgr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'nftables' if firewall_toolkit == 'nftables' else '' }}" - glibc-langpack-de - glibc-langpack-en - - grub2 - - grub2-efi - lrzsz - lvm2 - - nc - - nfs-utils - - nfsv4-client-utils - mtr - - ppp + - ncurses-term + - nfs-utils + - policycoreutils-python-utils - shim - tmux - "{{ 'cryptsetup' if luks_enabled else '' }}" @@ -23,10 +22,162 @@ bootstrap_almalinux: - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - vim - - wget - zram-generator - zstd +bootstrap_rhel_versioned: + - grub2 + - "{{ 'grub2-efi-x64' if os_version_major | default('') == '8' else 'grub2-efi' }}" + - "{{ 'grub2-tools-extra' if os_version_major | default('') in ['8', '9'] else '' }}" + - "{{ 'python39' if os_version_major | default('') == '8' else 'python' }}" + - "{{ 'kernel' if os_version_major | default('') == '10' else '' }}" + +bootstrap_rhel_common: "{{ bootstrap_rhel_base + bootstrap_rhel_versioned }}" + +bootstrap_rhel8: "{{ bootstrap_rhel_common }}" +bootstrap_rhel9: "{{ bootstrap_rhel_common }}" +bootstrap_rhel10: "{{ bootstrap_rhel_common }}" + +bootstrap_almalinux: + "{{ bootstrap_rhel_base + ['grub2', 'grub2-efi', 'dbus-daemon', 'lrzsz', 'nfsv4-client-utils', 'nc', 'ppp'] }}" + +bootstrap_rocky: + "{{ bootstrap_rhel_base + ['grub2', 'grub2-efi', 'nfsv4-client-utils', 'nc', 'ppp', 'telnet', 'util-linux-core', 'wget'] }}" + +bootstrap_almalinux8: "{{ bootstrap_almalinux }}" +bootstrap_almalinux9: "{{ bootstrap_almalinux }}" +bootstrap_almalinux10: "{{ bootstrap_almalinux }}" + +bootstrap_rocky8: "{{ bootstrap_rocky }}" +bootstrap_rocky9: "{{ bootstrap_rocky }}" +bootstrap_rocky10: "{{ bootstrap_rocky }}" + +bootstrap_fedora: + - bat + - bind-utils + - btrfs-progs + - cronie + - dhcp-client + - duf + - efibootmgr + - entr + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'nftables' if firewall_toolkit == 'nftables' else '' }}" + - fish + - fzf + - glibc-langpack-de + - glibc-langpack-en + - grub2 + - grub2-efi + - htop + - iperf3 + - logrotate + - lrzsz + - lvm2 + - nc + - nfs-utils + - nfsv4-client-utils + - polkit + - ppp + - ripgrep + - shim + - tmux + - "{{ 'cryptsetup' if luks_enabled else '' }}" + - "{{ 'tpm2-tools' if luks_enabled else '' }}" + - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" + - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" + - vim-default-editor + - wget + - zoxide + - zram-generator + - zstd + +bootstrap_fedora40: "{{ bootstrap_fedora }}" +bootstrap_fedora41: "{{ bootstrap_fedora }}" +bootstrap_fedora42: "{{ bootstrap_fedora }}" +bootstrap_fedora43: "{{ bootstrap_fedora }}" + +bootstrap_debian_base: + - btrfs-progs + - cron + - gnupg + - grub-efi + - grub-efi-amd64-signed + - grub2-common + - "{{ 'cryptsetup' if luks_enabled else '' }}" + - "{{ 'cryptsetup-initramfs' if luks_enabled else '' }}" + - locales + - logrotate + - lvm2 + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'nftables' if firewall_toolkit == 'nftables' else '' }}" + - "{{ 'openssh-server' if ssh_enabled | bool else '' }}" + - python3 + - xfsprogs + +bootstrap_debian_extra: + - apparmor-utils + - bat + - chrony + - curl + - duf + - entr + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - fish + - fzf + - htop + - jq + - libpam-pwquality + - lrzsz + - mtr + - ncdu + - net-tools + - network-manager + - python-is-python3 + - ripgrep + - rsync + - screen + - software-properties-common + - sudo + - syslog-ng + - systemd-zram-generator + - tcpd + - tldr + - "{{ 'tpm2-tools' if luks_enabled else '' }}" + - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" + - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" + - vim + - wget + - zstd + +bootstrap_debian_versioned: + - "{{ 'linux-image-amd64' if (os_version | string) in ['10', '11', '13', 'unstable'] else '' }}" + - "{{ 'fastfetch' if (os_version | string) in ['12', '13', 'unstable'] else '' }}" + - "{{ 'neofetch' if (os_version | string) == '12' else '' }}" + +bootstrap_debian_common: "{{ bootstrap_debian_base + bootstrap_debian_extra + bootstrap_debian_versioned }}" + +bootstrap_debian10: "{{ bootstrap_debian_common }}" +bootstrap_debian11: "{{ bootstrap_debian_common }}" +bootstrap_debian12: "{{ bootstrap_debian_common }}" +bootstrap_debian13: "{{ bootstrap_debian_common }}" +bootstrap_debianunstable: "{{ bootstrap_debian_common }}" + +bootstrap_ubuntu: + "{{ + bootstrap_debian_base + bootstrap_debian_extra + + ['bash-completion', 'dnsutils', 'eza', 'fdupes', 'fio', 'ncurses-term', 'traceroute', 'util-linux-extra', 'yq', 'zoxide'] + }}" + +bootstrap_ubuntu_lts: + "{{ + bootstrap_debian_base + bootstrap_debian_extra + + ['bash-completion', 'dnsutils', 'eza', 'fdupes', 'fio', 'ncurses-term', 'traceroute', 'util-linux-extra', 'yq', 'zoxide'] + }}" + bootstrap_archlinux: - base - btrfs-progs @@ -34,7 +185,10 @@ bootstrap_archlinux: - dhcpcd - efibootmgr - fastfetch - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'iptables-nft' if firewall_toolkit == 'nftables' else '' }}" - fish - fzf - grub @@ -65,436 +219,39 @@ bootstrap_archlinux: - wireguard-tools - zram-generator -bootstrap_debian11: - base: - - apparmor-utils - - btrfs-progs - - chrony - - cron - - gnupg - - grub-efi - - grub-efi-amd64-signed - - grub2-common - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'cryptsetup-initramfs' if luks_enabled else '' }}" - - linux-image-amd64 - - locales - - logrotate - - lvm2 - - net-tools - - "{{ 'openssh-server' if ssh_enabled | bool else '' }}" - - python3 - - sudo - - xfsprogs - - extra: - - bat - - curl - - entr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - fish - - fzf - - htop - - jq - - libpam-pwquality - - lrzsz - - mtr - - ncdu - - neofetch - - network-manager - - python-is-python3 - - ripgrep - - rsync - - screen - - software-properties-common - - syslog-ng - - tcpd - - tldr - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - vim - - wget - - zstd - -bootstrap_debian12: - base: - - btrfs-progs - - cron - - gnupg - - grub-efi - - grub-efi-amd64-signed - - grub2-common - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'cryptsetup-initramfs' if luks_enabled else '' }}" - - linux-image-amd64 - - locales - - logrotate - - lvm2 - - xfsprogs - - extra: - - apparmor-utils - - bat - - chrony - - curl - - duf - - entr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - fish - - fzf - - htop - - jq - - libpam-pwquality - - logrotate - - lrzsz - - mtr - - ncdu - - neofetch - - net-tools - - network-manager - - "{{ 'openssh-server' if ssh_enabled | bool else '' }}" - - python-is-python3 - - python3 - - ripgrep - - rsync - - screen - - software-properties-common - - sudo - - syslog-ng - - systemd-zram-generator - - tcpd - - tldr - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - vim - - wget - - zstd - -bootstrap_debian13: - base: - - btrfs-progs - - cron - - gnupg - - grub-efi - - grub-efi-amd64-signed - - grub2-common - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'cryptsetup-initramfs' if luks_enabled else '' }}" - - linux-image-amd64 - - locales - - logrotate - - lvm2 - - xfsprogs - - extra: - - apparmor-utils - - bat - - chrony - - curl - - duf - - entr - - fastfetch - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - fish - - fzf - - htop - - jq - - libpam-pwquality - - logrotate - - lrzsz - - mtr - - ncdu - - net-tools - - network-manager - - "{{ 'openssh-server' if ssh_enabled | bool else '' }}" - - python-is-python3 - - python3 - - ripgrep - - rsync - - screen - - sudo - - syslog-ng - - systemd-zram-generator - - tcpd - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - vim - - wget - - zstd - -bootstrap_fedora: - - bat - - bind-utils - - btrfs-progs - - cronie - - dhcp-client - - duf - - efibootmgr - - entr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - fish - - fzf - - glibc-langpack-de - - glibc-langpack-en - - grub2 - - grub2-efi - - htop - - iperf3 - - logrotate - - lrzsz - - lvm2 - - nc - - nfs-utils - - nfsv4-client-utils - - polkit - - ppp - - ripgrep - - shim - - tmux - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - vim-default-editor - - wget - - zoxide - - zram-generator - - zstd - -bootstrap_rhel8: - - bind-utils - - dhcp-client - - efibootmgr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - glibc-langpack-de - - glibc-langpack-en - - grub2 - - grub2-efi-x64 - - grub2-tools-extra - - lrzsz - - lvm2 - - mtr - - ncurses-term - - nfs-utils - - policycoreutils-python-utils - - python39 - - shim - - tmux - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" +bootstrap_alpine: + - alpine-base - vim - - zstd - -bootstrap_rhel9: - - bind-utils - - dhcp-client - - efibootmgr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - glibc-langpack-de - - glibc-langpack-en - - grub2 - - grub2-efi - - grub2-tools-extra - - lrzsz - - lvm2 - - mtr - - ncurses-term - - nfs-utils - - policycoreutils-python-utils - - python - - shim - - tmux - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'tpm2-tools' if luks_enabled else '' }}" + - "{{ 'openssh' if ssh_enabled | bool else '' }}" - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - vim - - zram-generator - - zstd - -bootstrap_rhel10: - - bind-utils - - efibootmgr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - glibc-langpack-de - - glibc-langpack-en - - grub2 - - grub2-efi - - kernel - - lrzsz - - lvm2 - - mtr - - ncurses-term - - nfs-utils - - policycoreutils-python-utils - - python - - shim - - tmux + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'nftables' if firewall_toolkit == 'nftables' else '' }}" - "{{ 'cryptsetup' if luks_enabled else '' }}" - "{{ 'tpm2-tools' if luks_enabled else '' }}" + +bootstrap_opensuse: + - vim + - "{{ 'openssh' if ssh_enabled | bool else '' }}" - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - vim - - zram-generator - - zstd - -bootstrap_rocky: - - bind-utils - - dbus-daemon - - dhcp-client - - efibootmgr - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - glibc-langpack-de - - glibc-langpack-en - - grub2 - - grub2-efi - - lrzsz - - lvm2 - - mtr - - nc - - nfs-utils - - nfsv4-client-utils - - ppp - - shim - - telnet - - tmux + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'nftables' if firewall_toolkit == 'nftables' else '' }}" - "{{ 'cryptsetup' if luks_enabled else '' }}" - "{{ 'tpm2-tools' if luks_enabled else '' }}" + +bootstrap_void: + - vim + - "{{ 'openssh' if ssh_enabled | bool else '' }}" - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - util-linux-core - - vim - - wget - - zram-generator - - zstd - -bootstrap_ubuntu: - base: - - btrfs-progs - - cron - - gnupg - - grub-efi - - grub-efi-amd64-signed - - grub2-common - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'cryptsetup-initramfs' if luks_enabled else '' }}" - - linux-image-generic - - locales - - lvm2 - - xfsprogs - - extra: - - apparmor-utils - - bash-completion - - bat - - chrony - - curl - - dnsutils - - duf - - entr - - eza - - fdupes - - fio - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - fish - - htop - - jq - - libpam-pwquality - - logrotate - - lrzsz - - mtr - - ncdu - - ncurses-term - - net-tools - - network-manager - - "{{ 'openssh-server' if ssh_enabled | bool else '' }}" - - python-is-python3 - - python3 - - ripgrep - - rsync - - screen - - software-properties-common - - sudo - - syslog-ng - - systemd-zram-generator - - tcpd - - tldr - - tmux - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - traceroute - - util-linux-extra - - vim - - wget - - yq - - zoxide - - zstd - -bootstrap_ubuntu_lts: - base: - - btrfs-progs - - cron - - gnupg - - grub-efi - - grub-efi-amd64-signed - - grub2-common - - "{{ 'cryptsetup' if luks_enabled else '' }}" - - "{{ 'cryptsetup-initramfs' if luks_enabled else '' }}" - - linux-image-generic - - locales - - lvm2 - - xfsprogs - - extra: - - apparmor-utils - - bash-completion - - bat - - chrony - - curl - - dnsutils - - duf - - entr - - eza - - fdupes - - fio - - "{{ 'firewalld' if firewalld_enabled | bool else '' }}" - - fish - - htop - - jq - - libpam-pwquality - - logrotate - - lrzsz - - mtr - - ncdu - - ncurses-term - - net-tools - - network-manager - - "{{ 'openssh-server' if ssh_enabled | bool else '' }}" - - python-is-python3 - - python3 - - ripgrep - - rsync - - screen - - software-properties-common - - sudo - - syslog-ng - - systemd-zram-generator - - tcpd - - tldr - - tmux - - "{{ 'tpm2-tools' if luks_enabled else '' }}" - - "{{ 'qemu-guest-agent' if hypervisor | lower in ['libvirt', 'proxmox'] else '' }}" - - "{{ 'open-vm-tools' if hypervisor | lower == 'vmware' else '' }}" - - traceroute - - util-linux-extra - - vim - - wget - - yq - - zoxide - - zstd + - "{{ 'firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }}" + - "{{ 'ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }}" + - "{{ 'iptables' if firewall_toolkit == 'iptables' else '' }}" + - "{{ 'nftables' if firewall_toolkit == 'nftables' else '' }}" + - "{{ 'cryptsetup' if luks_enabled else '' }}" + - "{{ 'tpm2-tools' if luks_enabled else '' }}" diff --git a/roles/cis/defaults/main.yml b/roles/cis/defaults/main.yml index ebc2892..b03ec7e 100644 --- a/roles/cis/defaults/main.yml +++ b/roles/cis/defaults/main.yml @@ -10,12 +10,12 @@ cis_permission_targets: >- { "path": "/mnt/etc/cron.d", "mode": "0700" }, { "path": "/mnt/etc/crontab", "mode": "0600" }, { "path": "/mnt/etc/logrotate.conf", "mode": "0644" }, - { "path": "/mnt/usr/sbin/pppd", "mode": "0754" } if os not in ["rhel8", "rhel9", "rhel10"] else None, + { "path": "/mnt/usr/sbin/pppd", "mode": "0754" } if os != "rhel" else None, { "path": "/mnt/usr/bin/" - + ("fusermount3" if os in ["archlinux", "debian12", "fedora", "rhel9", "rhel10", "rocky"] else "fusermount"), + + ("fusermount3" if os in ["archlinux", "fedora", "rocky"] or os == "rhel" or (os == "debian" and (os_version | string) == "12") else "fusermount"), "mode": "755" }, - { "path": "/mnt/usr/bin/" + ("write.ul" if os == "debian11" else "write"), "mode": "755" } + { "path": "/mnt/usr/bin/" + ("write.ul" if os == "debian" and (os_version | string) == "11" else "write"), "mode": "755" } ] | reject("none") }} diff --git a/roles/cis/tasks/crypto.yml b/roles/cis/tasks/crypto.yml index bf546e5..15bfcea 100644 --- a/roles/cis/tasks/crypto.yml +++ b/roles/cis/tasks/crypto.yml @@ -1,12 +1,12 @@ --- - name: Configure System Cryptography Policy - when: os in ["almalinux", "rhel9", "rhel10", "rocky"] - ansible.builtin.command: "{{ chroot_command }} /mnt /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1" + when: os == "rhel" or os in ["almalinux", "rocky"] + ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1" register: cis_crypto_policy_result changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout" - name: Mask Systemd Services ansible.builtin.command: > - {{ chroot_command }} /mnt systemctl mask nftables bluetooth rpcbind + {{ chroot_command }} systemctl mask {{ 'nftables' if firewall_toolkit == 'iptables' else 'iptables' }} bluetooth rpcbind register: cis_mask_services_result changed_when: cis_mask_services_result.rc == 0 diff --git a/roles/cis/tasks/security_lines.yml b/roles/cis/tasks/security_lines.yml index f26522e..46dd142 100644 --- a/roles/cis/tasks/security_lines.yml +++ b/roles/cis/tasks/security_lines.yml @@ -4,21 +4,21 @@ path: "{{ item.path }}" line: "{{ item.content }}" loop: - - {path: /mnt/etc/security/limits.conf, content: "* hard core 0"} - - {path: /mnt/etc/security/pwquality.conf, content: minlen = 14} - - {path: /mnt/etc/security/pwquality.conf, content: dcredit = -1} - - {path: /mnt/etc/security/pwquality.conf, content: ucredit = -1} - - {path: /mnt/etc/security/pwquality.conf, content: ocredit = -1} - - {path: /mnt/etc/security/pwquality.conf, content: lcredit = -1} - - {path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: umask 077} - - {path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: export TMOUT=3000} - - {path: '/mnt/{{ "usr/lib/systemd/journald.conf" if os == "fedora" else "etc/systemd/journald.conf" }}', content: Storage=persistent} - - {path: /mnt/etc/sudoers, content: Defaults logfile="/var/log/sudo.log"} - - {path: /mnt/etc/pam.d/su, content: auth required pam_wheel.so} + - { path: /mnt/etc/security/limits.conf, content: "* hard core 0" } + - { path: /mnt/etc/security/pwquality.conf, content: minlen = 14 } + - { path: /mnt/etc/security/pwquality.conf, content: dcredit = -1 } + - { path: /mnt/etc/security/pwquality.conf, content: ucredit = -1 } + - { path: /mnt/etc/security/pwquality.conf, content: ocredit = -1 } + - { path: /mnt/etc/security/pwquality.conf, content: lcredit = -1 } + - { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: umask 077 } + - { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: export TMOUT=3000 } + - { path: '/mnt/{{ "usr/lib/systemd/journald.conf" if os == "fedora" else "etc/systemd/journald.conf" }}', content: Storage=persistent } + - { path: /mnt/etc/sudoers, content: Defaults logfile="/var/log/sudo.log" } + - { path: /mnt/etc/pam.d/su, content: auth required pam_wheel.so } - path: >- /mnt/etc/{{ "pam.d/common-auth" - if os in ["debian11", "debian12", "ubuntu", "ubuntu-lts"] + if is_debian | bool else "authselect/system-auth" if os == "fedora" else "pam.d/system-auth" @@ -28,7 +28,7 @@ - path: >- /mnt/etc/{{ "pam.d/common-account" - if os in ["debian11", "debian12", "ubuntu", "ubuntu-lts"] + if is_debian | bool else "authselect/system-auth" if os == "fedora" else "pam.d/system-auth" @@ -37,10 +37,10 @@ - path: >- /mnt/etc/pam.d/{{ "common-password" - if os in ["debian11", "debian12", "ubuntu", "ubuntu-lts"] + if is_debian | bool else "passwd" }} content: >- password [success=1 default=ignore] pam_unix.so obscure sha512 remember=5 - - {path: /mnt/etc/hosts.deny, content: "ALL: ALL"} - - {path: /mnt/etc/hosts.allow, content: "sshd: ALL"} + - { path: /mnt/etc/hosts.deny, content: "ALL: ALL" } + - { path: /mnt/etc/hosts.allow, content: "sshd: ALL" } diff --git a/roles/cis/tasks/sshd.yml b/roles/cis/tasks/sshd.yml index 0010a2c..6bebcc5 100644 --- a/roles/cis/tasks/sshd.yml +++ b/roles/cis/tasks/sshd.yml @@ -5,30 +5,30 @@ regexp: ^\s*#?{{ item.option }}\s+.*$ line: "{{ item.option }} {{ item.value }}" loop: - - {option: LogLevel, value: VERBOSE} - - {option: LoginGraceTime, value: "60"} - - {option: PermitRootLogin, value: "no"} - - {option: StrictModes, value: "yes"} - - {option: MaxAuthTries, value: "4"} - - {option: MaxSessions, value: "10"} - - {option: MaxStartups, value: "10:30:60"} - - {option: PubkeyAuthentication, value: "yes"} - - {option: HostbasedAuthentication, value: "no"} - - {option: IgnoreRhosts, value: "yes"} - - {option: PasswordAuthentication, value: "no"} - - {option: PermitEmptyPasswords, value: "no"} - - {option: KerberosAuthentication, value: "no"} - - {option: GSSAPIAuthentication, value: "no"} - - {option: AllowAgentForwarding, value: "no"} - - {option: AllowTcpForwarding, value: "no"} - - {option: ChallengeResponseAuthentication, value: "no"} - - {option: GatewayPorts, value: "no"} - - {option: X11Forwarding, value: "no"} - - {option: PermitUserEnvironment, value: "no"} - - {option: ClientAliveInterval, value: "300"} - - {option: ClientAliveCountMax, value: "1"} - - {option: PermitTunnel, value: "no"} - - {option: Banner, value: /etc/issue.net} + - { option: LogLevel, value: VERBOSE } + - { option: LoginGraceTime, value: "60" } + - { option: PermitRootLogin, value: "no" } + - { option: StrictModes, value: "yes" } + - { option: MaxAuthTries, value: "4" } + - { option: MaxSessions, value: "10" } + - { option: MaxStartups, value: "10:30:60" } + - { option: PubkeyAuthentication, value: "yes" } + - { option: HostbasedAuthentication, value: "no" } + - { option: IgnoreRhosts, value: "yes" } + - { option: PasswordAuthentication, value: "no" } + - { option: PermitEmptyPasswords, value: "no" } + - { option: KerberosAuthentication, value: "no" } + - { option: GSSAPIAuthentication, value: "no" } + - { option: AllowAgentForwarding, value: "no" } + - { option: AllowTcpForwarding, value: "no" } + - { option: ChallengeResponseAuthentication, value: "no" } + - { option: GatewayPorts, value: "no" } + - { option: X11Forwarding, value: "no" } + - { option: PermitUserEnvironment, value: "no" } + - { option: ClientAliveInterval, value: "300" } + - { option: ClientAliveCountMax, value: "1" } + - { option: PermitTunnel, value: "no" } + - { option: Banner, value: /etc/issue.net } - name: Append CIS specific configurations to sshd_config ansible.builtin.blockinfile: diff --git a/roles/cleanup/defaults/main.yml b/roles/cleanup/defaults/main.yml index 9e84ae4..2145505 100644 --- a/roles/cleanup/defaults/main.yml +++ b/roles/cleanup/defaults/main.yml @@ -1,5 +1,9 @@ --- cleanup_libvirt_image_dir: >- - {{ vm_path if vm_path is defined and vm_path | length > 0 else '/var/lib/libvirt/images' }} + {{ + system_cfg.path + if system_cfg is defined and (system_cfg.path | string | length) > 0 + else '/var/lib/libvirt/images' + }} cleanup_libvirt_cloudinit_path: >- {{ [cleanup_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }} diff --git a/roles/cleanup/tasks/libvirt.yml b/roles/cleanup/tasks/libvirt.yml index dc99e00..58b4693 100644 --- a/roles/cleanup/tasks/libvirt.yml +++ b/roles/cleanup/tasks/libvirt.yml @@ -102,3 +102,5 @@ delegate_to: "{{ inventory_hostname }}" ansible.builtin.wait_for_connection: timeout: 300 + failed_when: false + changed_when: false diff --git a/roles/cleanup/tasks/proxmox.yml b/roles/cleanup/tasks/proxmox.yml index defeaad..0774e87 100644 --- a/roles/cleanup/tasks/proxmox.yml +++ b/roles/cleanup/tasks/proxmox.yml @@ -10,7 +10,7 @@ api_user: "{{ hypervisor_username }}" api_password: "{{ hypervisor_password }}" name: "{{ hostname }}" - vmid: "{{ vm_id }}" + vmid: "{{ system_cfg.id }}" disk: "{{ item }}" state: absent loop: @@ -23,5 +23,5 @@ api_user: "{{ hypervisor_username }}" api_password: "{{ hypervisor_password }}" node: "{{ hypervisor_node }}" - vmid: "{{ vm_id }}" + vmid: "{{ system_cfg.id }}" state: restarted diff --git a/roles/cleanup/tasks/virtual.yml b/roles/cleanup/tasks/virtual.yml index 917d33b..49f4843 100644 --- a/roles/cleanup/tasks/virtual.yml +++ b/roles/cleanup/tasks/virtual.yml @@ -13,3 +13,196 @@ - name: Cleanup libvirt resources ansible.builtin.include_tasks: libvirt.yml + +- name: Cleanup Xen resources + ansible.builtin.include_tasks: xen.yml + +- name: Determine post-reboot connectivity + ansible.builtin.set_fact: + cleanup_post_reboot_can_connect: >- + {{ + ( + post_reboot_can_connect + if post_reboot_can_connect is defined + else ( + (ansible_connection | default('ssh')) != 'ssh' + or ((system_cfg.ip | default('') | string | length) > 0) + or ( + install_type == 'physical' + and (ansible_host | default('') | string | length) > 0 + ) + ) + ) | bool + }} + changed_when: false + +- name: Check VM accessibility after reboot + when: + - install_type == "virtual" + - cleanup_post_reboot_can_connect | bool + block: + - name: Attempt to connect to VM + delegate_to: "{{ inventory_hostname }}" + ansible.builtin.wait_for_connection: + timeout: 300 + register: cleanup_vm_connection_check + failed_when: false + changed_when: false + + - name: VM failed to boot - initiate cleanup + when: + - cleanup_vm_connection_check is defined + - cleanup_vm_connection_check.failed | bool + block: + - name: VM boot failure detected - removing VM + ansible.builtin.debug: + msg: | + VM {{ hostname }} failed to boot after provisioning. + This VM was created in the current playbook run and will be removed + to prevent orphaned resources. + + - name: Remove VM for libvirt + when: + - hypervisor == "libvirt" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + community.libvirt.virt: + name: "{{ hostname }}" + state: destroyed + + - name: Undefine VM for libvirt + when: + - hypervisor == "libvirt" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + community.libvirt.virt: + name: "{{ hostname }}" + command: undefine + + - name: Remove VM disk for libvirt + when: + - hypervisor == "libvirt" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: "{{ virtualization_libvirt_disks | default([]) }}" + loop_control: + label: "{{ item.path }}" + + - name: Remove cloud-init disk for libvirt + when: + - hypervisor == "libvirt" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + ansible.builtin.file: + path: "{{ virtualization_libvirt_cloudinit_path }}" + state: absent + + - name: Remove VM for proxmox + when: + - hypervisor == "proxmox" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + community.proxmox.proxmox_kvm: + api_host: "{{ hypervisor_url }}" + api_user: "{{ hypervisor_username }}" + api_password: "{{ hypervisor_password }}" + node: "{{ hypervisor_node }}" + name: "{{ hostname }}" + vmid: "{{ system_cfg.id }}" + state: stopped + + - name: Delete VM for proxmox + when: + - hypervisor == "proxmox" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + community.proxmox.proxmox_kvm: + api_host: "{{ hypervisor_url }}" + api_user: "{{ hypervisor_username }}" + api_password: "{{ hypervisor_password }}" + node: "{{ hypervisor_node }}" + name: "{{ hostname }}" + vmid: "{{ system_cfg.id }}" + state: absent + unprivileged: false + + - name: Remove VM for VMware + when: + - hypervisor == "vmware" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + community.vmware.vmware_guest: + hostname: "{{ hypervisor_url }}" + username: "{{ hypervisor_username }}" + password: "{{ hypervisor_password }}" + validate_certs: "{{ hypervisor_validate_certs }}" + name: "{{ hostname }}" + folder: "{{ system_cfg.path | default('/') }}" + state: poweredoff + + - name: Delete VM for VMware + when: + - hypervisor == "vmware" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + community.vmware.vmware_guest: + hostname: "{{ hypervisor_url }}" + username: "{{ hypervisor_username }}" + password: "{{ hypervisor_password }}" + validate_certs: "{{ hypervisor_validate_certs }}" + name: "{{ hostname }}" + folder: "{{ system_cfg.path | default('/') }}" + state: absent + + - name: Destroy Xen VM if running + when: + - hypervisor == "xen" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + ansible.builtin.command: + argv: + - xl + - destroy + - "{{ hostname }}" + register: cleanup_xen_destroy + failed_when: false + changed_when: cleanup_xen_destroy.rc == 0 + + - name: Remove Xen VM disk + when: + - hypervisor == "xen" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: "{{ virtualization_xen_disks | default([]) }}" + loop_control: + label: "{{ item.path }}" + + - name: Remove Xen VM config file + when: + - hypervisor == "xen" + - virtualization_vm_created_in_run | default(false) | bool + delegate_to: localhost + become: false + ansible.builtin.file: + path: "/tmp/xen-{{ hostname }}.cfg" + state: absent + + - name: VM cleanup completed + ansible.builtin.debug: + msg: VM {{ hostname }} has been successfully removed due to boot failure. diff --git a/roles/cleanup/tasks/vmware.yml b/roles/cleanup/tasks/vmware.yml index 455f30b..d3511ae 100644 --- a/roles/cleanup/tasks/vmware.yml +++ b/roles/cleanup/tasks/vmware.yml @@ -10,7 +10,7 @@ hostname: "{{ hypervisor_url }}" username: "{{ hypervisor_username }}" password: "{{ hypervisor_password }}" - validate_certs: false + validate_certs: "{{ hypervisor_validate_certs }}" datacenter: "{{ hypervisor_datacenter }}" name: "{{ hostname }}" cdrom: @@ -34,7 +34,7 @@ hostname: "{{ hypervisor_url }}" username: "{{ hypervisor_username }}" password: "{{ hypervisor_password }}" - validate_certs: false + validate_certs: "{{ hypervisor_validate_certs }}" datacenter: "{{ hypervisor_datacenter }}" name: "{{ hostname }}" state: powered-on diff --git a/roles/cleanup/tasks/xen.yml b/roles/cleanup/tasks/xen.yml new file mode 100644 index 0000000..23bd311 --- /dev/null +++ b/roles/cleanup/tasks/xen.yml @@ -0,0 +1,58 @@ +--- +- name: Cleanup Xen installer media + when: hypervisor == "xen" + delegate_to: localhost + become: false + block: + - name: Ensure Xen disk definitions exist + when: virtualization_xen_disks is not defined + ansible.builtin.set_fact: + cleanup_xen_disks: "{{ cleanup_xen_disks | default([]) + [cleanup_xen_disk_cfg] }}" + vars: + device_letter_map: "abcdefghijklmnopqrstuvwxyz" + device_letter: "{{ device_letter_map[ansible_loop.index0] }}" + cleanup_xen_disk_cfg: >- + {{ + { + 'path': ( + virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2' + if ansible_loop.index0 == 0 + else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2' + ), + 'target': 'xvd' ~ device_letter, + 'size': (item.size | float) + } + }} + loop: "{{ system_cfg.disks }}" + loop_control: + label: "{{ item | to_json }}" + extended: true + changed_when: false + + - name: Render Xen VM configuration without installer media + vars: + xen_installer_media_enabled: false + virtualization_xen_disks: "{{ virtualization_xen_disks | default(cleanup_xen_disks | default([])) }}" + ansible.builtin.template: + src: xen.cfg.j2 + dest: /tmp/xen-{{ hostname }}.cfg + mode: "0644" + + - name: Destroy Xen VM if running + ansible.builtin.command: + argv: + - xl + - destroy + - "{{ hostname }}" + register: cleanup_xen_destroy + failed_when: false + changed_when: cleanup_xen_destroy.rc == 0 + + - name: Start Xen VM without installer media + ansible.builtin.command: + argv: + - xl + - create + - /tmp/xen-{{ hostname }}.cfg + register: cleanup_xen_start_result + changed_when: cleanup_xen_start_result.rc == 0 diff --git a/roles/configuration/defaults/main.yml b/roles/configuration/defaults/main.yml new file mode 100644 index 0000000..e45ed38 --- /dev/null +++ b/roles/configuration/defaults/main.yml @@ -0,0 +1,5 @@ +--- +configuration_motd_enabled: "{{ motd_enabled | bool }}" +configuration_sudo_banner_enabled: "{{ sudo_banner_enabled | bool }}" +configuration_firewall_enabled: "{{ firewall_enabled | bool }}" +configuration_luks_enabled: "{{ luks_enabled | bool }}" diff --git a/roles/configuration/tasks/banner.yml b/roles/configuration/tasks/banner.yml new file mode 100644 index 0000000..a2d1b6c --- /dev/null +++ b/roles/configuration/tasks/banner.yml @@ -0,0 +1,56 @@ +--- +- name: Configure MOTD + when: configuration_motd_enabled | bool + block: + - name: Create MOTD file + ansible.builtin.copy: + content: | + *************************************************************************** + * AUTHORIZED ACCESS ONLY. ALL ACTIVITIES ARE MONITORED AND LOGGED. * + * * + *************************************************************************** + dest: /mnt/etc/motd + mode: "0644" + owner: root + group: root + + - name: Remove other MOTD files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /mnt/etc/motd.d/99-motd + - /mnt/etc/motd.d/cockpit + - /mnt/etc/motd.d/insights-client + failed_when: false + +- name: Configure sudo banner + when: configuration_sudo_banner_enabled | bool + block: + - name: Create sudoers banner directory + ansible.builtin.file: + path: /mnt/etc/sudoers.d + state: directory + mode: "0755" + owner: root + group: root + + - name: Create sudo banner file + ansible.builtin.copy: + content: | + I am Groot, and I know what I'm doing. + dest: /mnt/etc/sudoers.d/banner + mode: "0644" + owner: root + group: root + + - name: Enable sudo banner in sudoers + ansible.builtin.lineinfile: + path: /mnt/etc/sudoers + line: "Defaults lecture=@/etc/sudoers.d/banner" + state: present + create: true + mode: "0440" + owner: root + group: root + validate: "visudo -cf - %s" diff --git a/roles/configuration/tasks/bootloader.yml b/roles/configuration/tasks/bootloader.yml index 6c9f7b2..803a923 100644 --- a/roles/configuration/tasks/bootloader.yml +++ b/roles/configuration/tasks/bootloader.yml @@ -8,7 +8,7 @@ configuration_bootloader_id: >- {{ "ubuntu" if os | lower in ["ubuntu", "ubuntu-lts"] else os }} configuration_efi_vendor: >- - {{ "redhat" if os | lower in ["rhel8", "rhel9", "rhel10"] else os | lower }} + {{ "redhat" if os | lower == "rhel" else os | lower }} configuration_efibootmgr_cmd: >- /usr/sbin/efibootmgr -c -L '{{ os }}' -d "{{ install_drive }}" -p 1 -l '\efi\EFI\{{ configuration_efi_vendor }}\shimx64.efi' @@ -18,7 +18,7 @@ --bootloader-id={{ configuration_bootloader_id }} configuration_bootloader_cmd: >- {{ configuration_efibootmgr_cmd if configuration_use_efibootmgr else configuration_grub_cmd }} - ansible.builtin.command: "{{ chroot_command }} /mnt {{ configuration_bootloader_cmd }}" + ansible.builtin.command: "{{ chroot_command }} {{ configuration_bootloader_cmd }}" register: configuration_bootloader_result changed_when: configuration_bootloader_result.rc == 0 @@ -27,10 +27,11 @@ ansible.builtin.lineinfile: path: /mnt/etc/mkinitcpio.conf regexp: "^(HOOKS=.*block)(?!.*lvm2)(.*)" - line: '\1 lvm2\2' + line: "\\1 lvm2\\2" backrefs: true - name: Regenerate initramfs + when: os | lower not in ["alpine", "void"] vars: configuration_initramfs_cmd: >- {{ @@ -43,14 +44,14 @@ else '/usr/bin/dracut --regenerate-all --force' ) }} - ansible.builtin.command: "{{ chroot_command }} /mnt {{ configuration_initramfs_cmd }}" + ansible.builtin.command: "{{ chroot_command }} {{ configuration_initramfs_cmd }}" register: configuration_initramfs_result changed_when: configuration_initramfs_result.rc == 0 - name: Generate grub config vars: configuration_efi_vendor: >- - {{ "redhat" if os | lower in ["rhel8", "rhel9", "rhel10"] else os | lower }} + {{ "redhat" if os | lower == "rhel" else os | lower }} configuration_grub_cfg_cmd: >- {{ '/usr/sbin/grub2-mkconfig -o ' @@ -59,6 +60,6 @@ if is_rhel | bool else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg' }} - ansible.builtin.command: "{{ chroot_command }} /mnt {{ configuration_grub_cfg_cmd }}" + ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}" register: configuration_grub_result changed_when: configuration_grub_result.rc == 0 diff --git a/roles/configuration/tasks/encryption.yml b/roles/configuration/tasks/encryption.yml index 614eeb6..0852860 100644 --- a/roles/configuration/tasks/encryption.yml +++ b/roles/configuration/tasks/encryption.yml @@ -141,8 +141,8 @@ - configuration_luks_keyfile_in_use ansible.builtin.lineinfile: path: /mnt/etc/cryptsetup-initramfs/conf-hook - regexp: '^KEYFILE_PATTERN=' - line: 'KEYFILE_PATTERN=/etc/cryptsetup-keys.d/*.key' + regexp: "^KEYFILE_PATTERN=" + line: "KEYFILE_PATTERN=/etc/cryptsetup-keys.d/*.key" create: true mode: "0644" @@ -150,7 +150,7 @@ when: os | lower == 'archlinux' ansible.builtin.lineinfile: path: /mnt/etc/mkinitcpio.conf - regexp: '^HOOKS=' + regexp: "^HOOKS=" line: >- HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt lvm2 filesystems fsck) @@ -194,7 +194,7 @@ when: os | lower == 'archlinux' ansible.builtin.lineinfile: path: /mnt/etc/mkinitcpio.conf - regexp: '^FILES=' + regexp: "^FILES=" line: >- FILES=({{ configuration_mkinitcpio_files_list_new | join(' ') @@ -271,7 +271,7 @@ - configuration_kernel_bls_entries.files | length > 0 ansible.builtin.lineinfile: path: "{{ item.path }}" - regexp: '^options ' + regexp: "^options " line: "options {{ configuration_kernel_cmdline_new }}" loop: "{{ configuration_kernel_bls_entries.files }}" loop_control: @@ -351,5 +351,5 @@ when: not is_rhel | bool ansible.builtin.lineinfile: path: /mnt/etc/default/grub - regexp: '^GRUB_CMDLINE_LINUX_DEFAULT=' + regexp: "^GRUB_CMDLINE_LINUX_DEFAULT=" line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ configuration_grub_cmdline_default_new }}"' diff --git a/roles/configuration/tasks/encryption/tpm2.yml b/roles/configuration/tasks/encryption/tpm2.yml index 0d4a873..a31aa13 100644 --- a/roles/configuration/tasks/encryption/tpm2.yml +++ b/roles/configuration/tasks/encryption/tpm2.yml @@ -36,7 +36,7 @@ + [configuration_luks_device] }} configuration_luks_enroll_chroot_cmd: >- - {{ chroot_command }} /mnt {{ configuration_luks_enroll_args | join(' ') }} + {{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }} ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}" register: configuration_luks_tpm2_enroll_chroot changed_when: configuration_luks_tpm2_enroll_chroot.rc == 0 diff --git a/roles/configuration/tasks/extras.yml b/roles/configuration/tasks/extras.yml index 89d37fa..316aefc 100644 --- a/roles/configuration/tasks/extras.yml +++ b/roles/configuration/tasks/extras.yml @@ -27,7 +27,8 @@ - name: Create zram config when: - - os | lower not in ['debian11', 'rhel8'] + - (os != "debian" or (os_version | string) != "11") and os != "rhel" + - os | lower not in ["alpine", "void"] - swap_enabled | bool ansible.builtin.copy: dest: /mnt/etc/systemd/zram-generator.conf @@ -62,7 +63,7 @@ - /mnt/etc/issue.net - name: Remove motd files - when: os | lower in ["rhel8", "rhel9", "rhel10"] + when: os == "rhel" ansible.builtin.file: path: "{{ item }}" state: absent diff --git a/roles/configuration/tasks/fstab.yml b/roles/configuration/tasks/fstab.yml index adce034..ef35b4f 100644 --- a/roles/configuration/tasks/fstab.yml +++ b/roles/configuration/tasks/fstab.yml @@ -17,14 +17,14 @@ mode: "0644" - name: Remove deprecated attr2 and disable large extent - when: os | lower in ["almalinux", "rhel8", "rhel9", "rhel10", "rocky"] and filesystem == "xfs" + when: os in ["almalinux", "rocky", "rhel"] and filesystem == "xfs" ansible.builtin.replace: path: /mnt/etc/fstab regexp: "(xfs.*?)(attr2)" - replace: '\1allocsize=64m' + replace: "\\1allocsize=64m" - name: Replace ISO UUID entry with /dev/sr0 in fstab - when: os in ["rhel8", "rhel9", "rhel10"] + when: os == "rhel" vars: configuration_fstab_dvd_line: >- {{ @@ -34,12 +34,12 @@ }} ansible.builtin.lineinfile: path: /mnt/etc/fstab - regexp: '^.*\/dvd.*$' + regexp: "^.*\\/dvd.*$" line: "{{ configuration_fstab_dvd_line }}" state: present - name: Write image from RHEL ISO to the target machine - when: os in ["rhel8", "rhel9", "rhel10"] and hypervisor == 'vmware' + when: os == "rhel" and hypervisor == 'vmware' ansible.builtin.command: argv: - dd @@ -57,9 +57,9 @@ line: "{{ fstab_entry.line }}" insertafter: EOF loop: - - {regexp: '^# TempFS$', line: '# TempFS'} - - {regexp: '^tmpfs\\s+/tmp\\s+', line: 'tmpfs /tmp tmpfs defaults,nosuid,nodev,noexec 0 0'} - - {regexp: '^tmpfs\\s+/var/tmp\\s+', line: 'tmpfs /var/tmp tmpfs defaults,nosuid,nodev,noexec 0 0'} - - {regexp: '^tmpfs\\s+/dev/shm\\s+', line: 'tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0'} + - { regexp: "^# TempFS$", line: "# TempFS" } + - { regexp: "^tmpfs\\\\s+/tmp\\\\s+", line: "tmpfs /tmp tmpfs defaults,nosuid,nodev,noexec 0 0" } + - { regexp: "^tmpfs\\\\s+/var/tmp\\\\s+", line: "tmpfs /var/tmp tmpfs defaults,nosuid,nodev,noexec 0 0" } + - { regexp: "^tmpfs\\\\s+/dev/shm\\\\s+", line: "tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0" } loop_control: loop_var: fstab_entry diff --git a/roles/configuration/tasks/grub.yml b/roles/configuration/tasks/grub.yml index 1a2977d..ed45d9b 100644 --- a/roles/configuration/tasks/grub.yml +++ b/roles/configuration/tasks/grub.yml @@ -106,7 +106,7 @@ when: configuration_grub_bls_entries.files | length > 0 ansible.builtin.lineinfile: path: "{{ item.path }}" - regexp: '^options ' + regexp: "^options " line: "options {{ configuration_kernel_cmdline_base }}" loop: "{{ configuration_grub_bls_entries.files }}" loop_control: @@ -116,5 +116,5 @@ when: partitioning_grub_enable_cryptodisk | bool ansible.builtin.lineinfile: path: /mnt/etc/default/grub - regexp: '^GRUB_ENABLE_CRYPTODISK=' + regexp: "^GRUB_ENABLE_CRYPTODISK=" line: GRUB_ENABLE_CRYPTODISK=y diff --git a/roles/configuration/tasks/locales.yml b/roles/configuration/tasks/locales.yml index 6b7b501..1122eab 100644 --- a/roles/configuration/tasks/locales.yml +++ b/roles/configuration/tasks/locales.yml @@ -1,5 +1,6 @@ --- - name: Reload systemd in installer environment + when: ansible_service_mgr == 'systemd' ansible.builtin.systemd: daemon_reload: true @@ -19,14 +20,15 @@ regexp: "{{ item.regex }}" line: "{{ item.line }}" loop: - - {regex: en_US\.UTF-8 UTF-8, line: en_US.UTF-8 UTF-8} + - { regex: en_US\.UTF-8 UTF-8, line: en_US.UTF-8 UTF-8 } - name: Generate locales when: not is_rhel | bool - ansible.builtin.command: "{{ chroot_command }} /mnt /usr/sbin/locale-gen" + ansible.builtin.command: "{{ chroot_command }} /usr/sbin/locale-gen" register: configuration_locale_result changed_when: configuration_locale_result.rc == 0 + - name: Set hostname vars: configuration_hostname_fqdn: >- @@ -34,8 +36,8 @@ hostname if '.' in hostname else ( - hostname + '.' + vm_dns_search - if vm_dns_search is defined and vm_dns_search | length + hostname + '.' + system_cfg.dns_search + if system_cfg.dns_search is defined and system_cfg.dns_search | length else hostname ) }} @@ -51,16 +53,22 @@ hostname if '.' in hostname else ( - hostname + '.' + vm_dns_search - if vm_dns_search is defined and vm_dns_search | length + hostname + '.' + system_cfg.dns_search + if system_cfg.dns_search is defined and system_cfg.dns_search | length else hostname ) }} configuration_hostname_short: "{{ hostname.split('.')[0] }}" configuration_hostname_entries: >- {{ [configuration_hostname_fqdn, configuration_hostname_short] | unique | join(' ') }} + configuration_hosts_ip: >- + {{ + system_cfg.ip + if system_cfg.ip is defined and (system_cfg.ip | string | length) > 0 + else inventory_hostname + }} configuration_hosts_line: >- - {{ (vm_ip if vm_ip is defined and vm_ip | length > 0 else inventory_hostname) }} {{ configuration_hostname_entries }} + {{ configuration_hosts_ip }} {{ configuration_hostname_entries }} ansible.builtin.lineinfile: path: /mnt/etc/hosts line: "{{ configuration_hosts_line }}" diff --git a/roles/configuration/tasks/main.yml b/roles/configuration/tasks/main.yml index 19d1c90..f862c3c 100644 --- a/roles/configuration/tasks/main.yml +++ b/roles/configuration/tasks/main.yml @@ -2,6 +2,7 @@ - name: Include configuration tasks ansible.builtin.include_tasks: "{{ configuration_task }}" loop: + - banner.yml - fstab.yml - locales.yml - services.yml diff --git a/roles/configuration/tasks/network.yml b/roles/configuration/tasks/network.yml index f3303a1..c686011 100644 --- a/roles/configuration/tasks/network.yml +++ b/roles/configuration/tasks/network.yml @@ -82,15 +82,100 @@ - configuration_net_mac | length > 0 fail_msg: Failed to detect the MAC address for network interface {{ configuration_net_inf }}. -- name: Copy NetworkManager keyfile - ansible.builtin.template: - src: network.j2 - dest: /mnt/etc/NetworkManager/system-connections/LAN.nmconnection - mode: "0600" +- name: Configure NetworkManager profile + when: os | lower not in ["alpine", "void"] + block: + - name: Copy NetworkManager keyfile + ansible.builtin.template: + src: network.j2 + dest: /mnt/etc/NetworkManager/system-connections/LAN.nmconnection + mode: "0600" -- name: Fix Ubuntu unmanaged devices - when: os | lower in ["ubuntu", "ubuntu-lts"] - ansible.builtin.file: - path: /mnt/etc/NetworkManager/conf.d/10-globally-managed-devices.conf - state: touch - mode: "0644" + - name: Fix Ubuntu unmanaged devices + when: os | lower in ["ubuntu", "ubuntu-lts"] + ansible.builtin.file: + path: /mnt/etc/NetworkManager/conf.d/10-globally-managed-devices.conf + state: touch + mode: "0644" + +- name: Configure Alpine networking + when: os | lower == "alpine" + vars: + configuration_dns_value: "{{ system_cfg.dns_servers if system_cfg.dns_servers is defined else '' }}" + configuration_dns_list_raw: >- + {{ + configuration_dns_value + if configuration_dns_value is iterable and configuration_dns_value is not string + else configuration_dns_value.split(',') + }} + configuration_dns_list: >- + {{ configuration_dns_list_raw | map('trim') | reject('equalto', '') | list }} + configuration_alpine_static: >- + {{ + system_cfg.ip is defined + and system_cfg.ip | string | length > 0 + and system_cfg.prefix is defined + and (system_cfg.prefix | string | length) > 0 + }} + block: + - name: Write Alpine network interfaces + ansible.builtin.copy: + dest: /mnt/etc/network/interfaces + mode: "0644" + content: | + auto lo + iface lo inet loopback + + auto {{ configuration_net_inf }} + iface {{ configuration_net_inf }} inet {{ 'static' if configuration_alpine_static | bool else 'dhcp' }} + {% if configuration_alpine_static | bool %} + address {{ system_cfg.ip }}/{{ system_cfg.prefix }} + {% if system_cfg.gateway is defined and system_cfg.gateway | string | length %} + gateway {{ system_cfg.gateway }} + {% endif %} + {% endif %} + + - name: Set Alpine DNS resolvers + when: configuration_dns_list | length > 0 + ansible.builtin.copy: + dest: /mnt/etc/resolv.conf + mode: "0644" + content: | + {% for resolver in configuration_dns_list %} + nameserver {{ resolver }} + {% endfor %} + +- name: Configure Void networking + when: os | lower == "void" + vars: + configuration_dns_value: "{{ system_cfg.dns_servers if system_cfg.dns_servers is defined else '' }}" + configuration_dns_list_raw: >- + {{ + configuration_dns_value + if configuration_dns_value is iterable and configuration_dns_value is not string + else configuration_dns_value.split(',') + }} + configuration_dns_list: >- + {{ configuration_dns_list_raw | map('trim') | reject('equalto', '') | list }} + configuration_void_static: >- + {{ + system_cfg.ip is defined + and system_cfg.ip | string | length > 0 + and system_cfg.prefix is defined + and (system_cfg.prefix | string | length) > 0 + }} + block: + - name: Write dhcpcd configuration for static networking + when: configuration_void_static | bool + ansible.builtin.copy: + dest: /mnt/etc/dhcpcd.conf + mode: "0644" + content: | + interface {{ configuration_net_inf }} + static ip_address={{ system_cfg.ip }}/{{ system_cfg.prefix }} + {% if system_cfg.gateway is defined and system_cfg.gateway | string | length %} + static routers={{ system_cfg.gateway }} + {% endif %} + {% if configuration_dns_list | length > 0 %} + static domain_name_servers={{ configuration_dns_list | join(' ') }} + {% endif %} diff --git a/roles/configuration/tasks/selinux.yml b/roles/configuration/tasks/selinux.yml index f57de6f..806694f 100644 --- a/roles/configuration/tasks/selinux.yml +++ b/roles/configuration/tasks/selinux.yml @@ -3,9 +3,9 @@ when: is_rhel | bool block: - name: Fix SELinux by pre-labeling the filesystem before first boot - when: os | lower in ['almalinux', 'rhel8', 'rhel9', 'rhel10', 'rocky'] and selinux | bool + when: os in ['almalinux', 'rocky', 'rhel'] and selinux | bool ansible.builtin.command: > - {{ chroot_command }} /mnt /sbin/setfiles -v -F + {{ chroot_command }} /sbin/setfiles -v -F -e /dev -e /proc -e /sys -e /run /etc/selinux/targeted/contexts/files/file_contexts / register: configuration_setfiles_result diff --git a/roles/configuration/tasks/services.yml b/roles/configuration/tasks/services.yml index c5b9fc9..2ed1e05 100644 --- a/roles/configuration/tasks/services.yml +++ b/roles/configuration/tasks/services.yml @@ -1,11 +1,12 @@ --- - name: Enable Systemd Services + when: os | lower not in ['alpine', 'void'] ansible.builtin.command: > - {{ chroot_command }} /mnt systemctl enable NetworkManager - {{ ' firewalld' if firewalld_enabled | bool else '' }} + {{ chroot_command }} systemctl enable NetworkManager + {{ ' firewalld' if firewall_backend == 'firewalld' and firewall_enabled | bool else '' }} + {{ ' ufw' if firewall_backend == 'ufw' and firewall_enabled | bool else '' }} {{ - (' ssh' if os | lower in ['ubuntu', 'ubuntu-lts'] else - (' sshd' if os | lower not in ['debian11', 'debian12', 'debian13'] else '')) + (' ssh' if is_debian | bool else ' sshd') if ssh_enabled | bool else '' }} {{ @@ -14,3 +15,65 @@ }} register: configuration_enable_services_result changed_when: configuration_enable_services_result.rc == 0 + +- name: Enable OpenRC services + when: os | lower == 'alpine' + vars: + configuration_openrc_services: >- + {{ + ['networking'] + + (['sshd'] if ssh_enabled | bool else []) + + ([firewall_backend] if firewall_enabled | bool else []) + }} + block: + - name: Ensure OpenRC runlevel directory exists + ansible.builtin.file: + path: /mnt/etc/runlevels/default + state: directory + mode: "0755" + + - name: Check OpenRC init scripts + ansible.builtin.stat: + path: "/mnt/etc/init.d/{{ item }}" + loop: "{{ configuration_openrc_services }}" + register: configuration_openrc_service_stats + changed_when: false + + - name: Enable OpenRC services + ansible.builtin.file: + src: "/mnt/etc/init.d/{{ item.item }}" + dest: "/mnt/etc/runlevels/default/{{ item.item }}" + state: link + loop: "{{ configuration_openrc_service_stats.results }}" + when: item.stat.exists + +- name: Enable runit services + when: os | lower == 'void' + vars: + configuration_runit_services: >- + {{ + ['dhcpcd'] + + (['sshd'] if ssh_enabled | bool else []) + + ([firewall_backend] if firewall_enabled | bool else []) + }} + block: + - name: Ensure runit service directory exists + ansible.builtin.file: + path: /mnt/var/service + state: directory + mode: "0755" + + - name: Check runit service definitions + ansible.builtin.stat: + path: "/mnt/etc/sv/{{ item }}" + loop: "{{ configuration_runit_services }}" + register: configuration_runit_service_stats + changed_when: false + + - name: Enable runit services + ansible.builtin.file: + src: "/mnt/etc/sv/{{ item.item }}" + dest: "/mnt/var/service/{{ item.item }}" + state: link + loop: "{{ configuration_runit_service_stats.results }}" + when: item.stat.exists diff --git a/roles/configuration/tasks/users.yml b/roles/configuration/tasks/users.yml index 43a87a5..1b290c8 100644 --- a/roles/configuration/tasks/users.yml +++ b/roles/configuration/tasks/users.yml @@ -4,11 +4,11 @@ configuration_user_group: >- {{ "sudo" if is_debian | bool else "wheel" }} configuration_useradd_cmd: >- - {{ chroot_command }} /mnt /usr/sbin/useradd --create-home --user-group + {{ chroot_command }} /usr/sbin/useradd --create-home --user-group --groups {{ configuration_user_group }} {{ user_name }} --password {{ user_password | password_hash('sha512') }} --shell /bin/bash configuration_root_cmd: >- - {{ chroot_command }} /mnt /usr/sbin/usermod --password + {{ chroot_command }} /usr/sbin/usermod --password '{{ root_password | password_hash('sha512') }}' root --shell /bin/bash ansible.builtin.command: "{{ item }}" loop: diff --git a/roles/configuration/templates/network.j2 b/roles/configuration/templates/network.j2 index 2b36ee3..4582537 100644 --- a/roles/configuration/templates/network.j2 +++ b/roles/configuration/templates/network.j2 @@ -4,14 +4,14 @@ uuid={{ configuration_net_uuid }} type=ethernet [ipv4] -{% set dns_value = vm_dns if vm_dns is defined else '' %} +{% set dns_value = system_cfg.dns_servers if system_cfg.dns_servers is defined else '' %} {% set dns_list_raw = dns_value if dns_value is iterable and dns_value is not string else dns_value.split(',') %} {% set dns_list = dns_list_raw | map('trim') | reject('equalto', '') | list %} -{% set search_value = vm_dns_search if vm_dns_search is defined else '' %} +{% set search_value = system_cfg.dns_search if system_cfg.dns_search is defined else '' %} {% set search_list_raw = search_value if search_value is iterable and search_value is not string else search_value.split(',') %} {% set search_list = search_list_raw | map('trim') | reject('equalto', '') | list %} -{% if vm_ip is defined and vm_ip | length %} -address1={{ vm_ip }}/{{ vm_nms }}{{ (',' ~ vm_gw) if (vm_gw is defined and vm_gw | length) else '' }} +{% if system_cfg.ip is defined and system_cfg.ip | string | length %} +address1={{ system_cfg.ip }}/{{ system_cfg.prefix }}{{ (',' ~ system_cfg.gateway) if (system_cfg.gateway is defined and system_cfg.gateway | string | length) else '' }} method=manual {% else %} method=auto diff --git a/roles/environment/tasks/main.yml b/roles/environment/tasks/main.yml index 6e2131b..4cbe2d6 100644 --- a/roles/environment/tasks/main.yml +++ b/roles/environment/tasks/main.yml @@ -10,15 +10,73 @@ - name: Gather facts ansible.builtin.setup: - - name: Check if host is booted from the Arch install media + - name: Check for live environment markers ansible.builtin.stat: - path: /run/archiso - register: environment_archiso_stat + path: "{{ item }}" + loop: + - /run/archiso + - /run/live + - /run/initramfs + - /run/initramfs/live + register: environment_live_marker_stat + changed_when: false + + - name: Determine root filesystem type + ansible.builtin.set_fact: + environment_root_fstype: >- + {{ + ansible_mounts + | selectattr('mount', 'equalto', '/') + | map(attribute='fstype') + | list + | first + | default('') + | lower + }} + environment_archiso_present: >- + {{ + ( + environment_live_marker_stat.results + | selectattr('item', 'equalto', '/run/archiso') + | selectattr('stat.exists') + | list + | length + ) > 0 + }} + changed_when: false + + - name: Identify live environment indicators + ansible.builtin.set_fact: + environment_is_live_environment: >- + {{ + ( + environment_live_marker_stat.results + | selectattr('stat.exists') + | list + | length + ) > 0 + or environment_root_fstype in ['overlay', 'overlayfs', 'squashfs', 'aufs'] + or (ansible_hostname | default('') | lower is search('live')) + }} + changed_when: false + + - name: Abort if target is not a live environment + ansible.builtin.assert: + that: + - environment_is_live_environment | bool + fail_msg: | + PRODUCTION SYSTEM DETECTED - ABORTING + + The target system does not appear to be a live installer environment. + This playbook must run from a live ISO to avoid wiping production data. + + Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry. + quiet: true - name: Abort if the host is not booted from the Arch install media when: - not (custom_iso | bool) - - not environment_archiso_stat.stat.exists + - not environment_archiso_present | bool ansible.builtin.fail: msg: This host is not booted from the Arch install media! @@ -40,9 +98,9 @@ - name: Set IP-Address when: - hypervisor == "vmware" - - vm_ip is defined and vm_ip | length > 0 + - system_cfg.ip is defined and system_cfg.ip | string | length > 0 ansible.builtin.command: >- - ip addr replace {{ vm_ip }}/{{ vm_nms }} + ip addr replace {{ system_cfg.ip }}/{{ system_cfg.prefix }} dev {{ environment_interface_name }} register: environment_ip_result changed_when: environment_ip_result.rc == 0 @@ -50,9 +108,9 @@ - name: Set Default Gateway when: - hypervisor == "vmware" - - vm_gw is defined and vm_gw | length > 0 - - vm_ip is defined and vm_ip | length > 0 - ansible.builtin.command: "ip route replace default via {{ vm_gw }}" + - system_cfg.gateway is defined and system_cfg.gateway | string | length > 0 + - system_cfg.ip is defined and system_cfg.ip | string | length > 0 + ansible.builtin.command: "ip route replace default via {{ system_cfg.gateway }}" register: environment_gateway_result changed_when: environment_gateway_result.rc == 0 @@ -105,23 +163,23 @@ - name: Setup Pacman when: - not (custom_iso | bool) - - "'os' not in item or os in item.os" + - item.os is not defined or (os_resolved | default(os)) in item.os community.general.pacman: update_cache: true force: true name: "{{ item.name }}" state: latest loop: - - {name: glibc} - - {name: dnf, os: [almalinux, fedora, rhel8, rhel9, rhel10, rocky]} - - {name: debootstrap, os: [debian11, debian12, debian13, ubuntu, ubuntu-lts]} - - {name: debian-archive-keyring, os: [debian11, debian12, debian13]} - - {name: ubuntu-keyring, os: [ubuntu, ubuntu-lts]} + - { name: glibc } + - { name: dnf, os: [almalinux8, almalinux9, almalinux10, fedora40, fedora41, fedora42, fedora43, rhel8, rhel9, rhel10, rocky8, rocky9, rocky10] } + - { name: debootstrap, os: [debian10, debian11, debian12, debian13, debianunstable, ubuntu, ubuntu-lts] } + - { name: debian-archive-keyring, os: [debian10, debian11, debian12, debian13, debianunstable] } + - { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] } retries: 4 delay: 15 - name: Prepare /iso mount and repository for RHEL-based systems - when: os | lower in ["rhel8", "rhel9", "rhel10"] + when: os == "rhel" block: - name: Create /iso directory ansible.builtin.file: @@ -129,9 +187,19 @@ state: directory mode: "0755" + - name: Select RHEL ISO device + ansible.builtin.set_fact: + environment_rhel_iso_device: >- + {{ + '/dev/sr2' + if hypervisor == 'libvirt' + else '/dev/sr1' + }} + changed_when: false + - name: Mount RHEL ISO ansible.posix.mount: - src: "{{ '/dev/sr1' if hypervisor == 'vmware' else '/dev/sr2' }}" + src: "{{ environment_rhel_iso_device }}" path: /usr/local/install/redhat/dvd fstype: iso9660 opts: "ro,loop" @@ -140,6 +208,16 @@ - name: Configure RHEL Repos for installation when: is_rhel | bool block: + - name: Select repository template + ansible.builtin.set_fact: + environment_repo_template: >- + {{ + (os_resolved | default(os)) | lower + if os == 'rhel' + else os | lower + }} + changed_when: false + - name: Create directories for repository files and RPM GPG keys ansible.builtin.file: path: /etc/yum.repos.d @@ -148,8 +226,8 @@ - name: Create RHEL repository file ansible.builtin.template: - src: "{{ os | lower }}.repo.j2" - dest: /etc/yum.repos.d/{{ os | lower }}.repo + src: "{{ environment_repo_template }}.repo.j2" + dest: /etc/yum.repos.d/{{ environment_repo_template }}.repo mode: "0644" - name: Check for third-party preparation tasks diff --git a/roles/global_defaults/defaults/main.yml b/roles/global_defaults/defaults/main.yml index c827bac..d34e5aa 100644 --- a/roles/global_defaults/defaults/main.yml +++ b/roles/global_defaults/defaults/main.yml @@ -1,18 +1,49 @@ --- hypervisor: "none" +hypervisor_defaults: + type: "none" + url: "" + username: "" + password: "" + node: "" + storage: "" + datacenter: "" + cluster: "" + validate_certs: false custom_iso: false cis: false selinux: true vmware_ssh: false -firewalld_enabled: true +firewall_enabled: true +firewall_backend: "firewalld" +firewall_toolkit: "nftables" ssh_enabled: true zstd_enabled: true swap_enabled: true -chroot_command: "arch-chroot" +chroot_tool: "arch-chroot" +os_version: "" +motd_enabled: true +sudo_banner_enabled: true thirdparty_preparation_tasks_path: "dropins/preparation.yml" cis_enabled: "{{ cis | bool }}" +system_defaults: + name: "" + id: "" + cpus: 0 + memory_mb: 0 + balloon_mb: 0 + network: "" + vlan: "" + ip: "" + prefix: "" + gateway: "" + dns_servers: [] + dns_search: [] + path: "" + disks: [] + luks_enabled: false luks_mapper_name: "SYSTEM_DECRYPTED" luks_auto_decrypt: true diff --git a/roles/global_defaults/tasks/hypervisor.yml b/roles/global_defaults/tasks/hypervisor.yml new file mode 100644 index 0000000..3cde0f9 --- /dev/null +++ b/roles/global_defaults/tasks/hypervisor.yml @@ -0,0 +1,42 @@ +--- +- name: Validate hypervisor dict input + when: hypervisor is mapping + ansible.builtin.assert: + that: + - hypervisor.type is defined + - hypervisor.type | string | length > 0 + fail_msg: "hypervisor.type is required when hypervisor is a dictionary" + quiet: true + +- name: Normalize hypervisor configuration + vars: + hypervisor_input: "{{ hypervisor if hypervisor is mapping else {} }}" + hypervisor_type_legacy: "{{ (hypervisor | default('none')) if hypervisor is string else '' }}" + hypervisor_legacy_cfg: + type: "{{ hypervisor_type_legacy }}" + url: "{{ hypervisor_url | default('') }}" + username: "{{ hypervisor_username | default('') }}" + password: "{{ hypervisor_password | default('') }}" + node: "{{ hypervisor_node | default('') }}" + storage: "{{ hypervisor_storage | default('') }}" + datacenter: "{{ hypervisor_datacenter | default('') }}" + cluster: "{{ hypervisor_cluster | default('') }}" + validate_certs: "{{ hypervisor_validate_certs | default(false) | bool }}" + hypervisor_cfg_effective: >- + {{ + hypervisor_defaults + | combine(hypervisor_legacy_cfg, recursive=True) + | combine(hypervisor_input, recursive=True) + }} + ansible.builtin.set_fact: + hypervisor_cfg: "{{ hypervisor_cfg_effective }}" + hypervisor: "{{ hypervisor_cfg_effective.type | string | lower }}" + hypervisor_url: "{{ hypervisor_cfg_effective.url }}" + hypervisor_username: "{{ hypervisor_cfg_effective.username }}" + hypervisor_password: "{{ hypervisor_cfg_effective.password }}" + hypervisor_node: "{{ hypervisor_cfg_effective.node }}" + hypervisor_storage: "{{ hypervisor_cfg_effective.storage }}" + hypervisor_datacenter: "{{ hypervisor_cfg_effective.datacenter }}" + hypervisor_cluster: "{{ hypervisor_cfg_effective.cluster }}" + hypervisor_validate_certs: "{{ hypervisor_cfg_effective.validate_certs | bool }}" + changed_when: false diff --git a/roles/global_defaults/tasks/main.yml b/roles/global_defaults/tasks/main.yml index 3bf99a8..5bd224f 100644 --- a/roles/global_defaults/tasks/main.yml +++ b/roles/global_defaults/tasks/main.yml @@ -4,96 +4,59 @@ msg: Global defaults loaded. changed_when: false +- name: Normalize hypervisor inputs + ansible.builtin.include_tasks: hypervisor.yml + +- name: Normalize system inputs + ansible.builtin.include_tasks: system.yml + - name: Validate variables - ansible.builtin.assert: - that: - - install_type is defined and install_type in ["virtual", "physical"] - - hypervisor in ["libvirt", "proxmox", "vmware", "none"] - - >- - install_type is defined and ( - install_type == "physical" - or hypervisor in ["libvirt", "proxmox", "vmware"] - ) - - filesystem is defined and filesystem in ["btrfs", "ext4", "xfs"] - - install_drive is defined and install_drive | length > 0 - - hostname is defined and hostname | length > 0 - - >- - os is defined and os in [ - "archlinux", "almalinux", "debian11", "debian12", "debian13", "fedora", - "rhel8", "rhel9", "rhel10", "rocky", "ubuntu", "ubuntu-lts" - ] - - >- - os is defined and ( - os not in ["rhel8", "rhel9", "rhel10"] - or (rhel_iso is defined and rhel_iso | length > 0) - ) - - >- - install_type is defined and ( - install_type == "physical" - or (boot_iso is defined and boot_iso | length > 0) - ) - - >- - install_type is defined and ( - install_type == "physical" - or (vm_cpus is defined and (vm_cpus | int) > 0) - ) - - >- - install_type is defined and ( - install_type == "physical" - or (vm_size is defined and (vm_size | float) > 0) - ) - - >- - install_type is defined and ( - install_type == "physical" - or (vm_memory is defined and (vm_memory | float) > 0) - ) - - >- - install_type is defined and filesystem is defined and ( - install_type == "physical" - or ( - vm_size is defined - and (vm_size | int) >= 20 - ) - ) - - >- - install_type is defined and ( - install_type == "physical" - or ( - vm_size is defined - and vm_memory is defined - and filesystem is defined - and ( - filesystem != "btrfs" - or ( - (vm_size | float) - >= ( - (vm_memory | float / 1024 >= 16.0) - | ternary( - (vm_memory | float / 2048), - [vm_memory | float / 1024, 4.0] | max - ) - + 5.5 - ) - ) - ) - ) - ) - - >- - vm_ip is not defined - or vm_ip | length == 0 - or (vm_nms is defined and (vm_nms | int) > 0) - fail_msg: Invalid input specified, please try again. + ansible.builtin.include_tasks: validation.yml - name: Set OS family flags ansible.builtin.set_fact: - is_rhel: "{{ os | lower in ['almalinux', 'fedora', 'rhel8', 'rhel9', 'rhel10', 'rocky'] }}" - is_debian: "{{ os | lower in ['debian11', 'debian12', 'debian13', 'ubuntu', 'ubuntu-lts'] }}" + is_rhel: "{{ os | lower in ['almalinux', 'fedora', 'rhel', 'rocky'] }}" + is_debian: "{{ os | lower in ['debian', 'ubuntu', 'ubuntu-lts'] }}" + changed_when: false + +- name: Normalize OS version for keying + when: + - os_version is defined + - (os_version | string | length) > 0 + ansible.builtin.set_fact: + os_version_major: "{{ (os_version | string).split('.')[0] }}" + changed_when: false + +- name: Resolve final OS key with version + when: + - os_version is defined + - (os_version | string | length) > 0 + ansible.builtin.set_fact: + os_resolved: >- + {{ + 'debian' + os_version | string if os == 'debian' + else 'fedora' + os_version | string if os == 'fedora' + else 'rocky' + os_version_major if os == 'rocky' + else 'almalinux' + os_version_major if os == 'almalinux' + else 'rhel' + os_version_major if os == 'rhel' + else os + }} + changed_when: false + +- name: Set chroot command wrapper + ansible.builtin.set_fact: + chroot_command: >- + {{ + 'systemd-nspawn -D /mnt' + if chroot_tool == 'systemd-nspawn' + else chroot_tool ~ ' /mnt' + }} changed_when: false - name: Set Python interpreter for RHEL-based installers when: - ansible_python_interpreter is not defined - - os | lower in ["almalinux", "rhel8", "rhel9", "rhel10", "rocky"] + - is_rhel | bool ansible.builtin.set_fact: ansible_python_interpreter: /usr/bin/python3 changed_when: false @@ -107,8 +70,10 @@ ansible_password: "{{ user_password }}" ansible_become_password: "{{ user_password }}" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" + changed_when: false - name: Set connection for VMware when: hypervisor == "vmware" ansible.builtin.set_fact: ansible_connection: vmware_tools + changed_when: false diff --git a/roles/global_defaults/tasks/system.yml b/roles/global_defaults/tasks/system.yml new file mode 100644 index 0000000..cb835a9 --- /dev/null +++ b/roles/global_defaults/tasks/system.yml @@ -0,0 +1,291 @@ +--- +- name: Ensure system input is a dictionary + ansible.builtin.set_fact: + system: "{{ system | default({}) }}" + changed_when: false + +- name: Validate system input type + ansible.builtin.assert: + that: + - system is mapping + fail_msg: "system must be a dictionary" + quiet: true + +- name: Normalize base system fields + vars: + system_name_effective: >- + {{ + system.name + if system.name is defined and (system.name | string | length) > 0 + else ( + hostname + if hostname is defined and (hostname | string | length) > 0 + else inventory_hostname + ) + }} + system_id_effective: >- + {{ + system.id + if system.id is defined and (system.id | string | length) > 0 + else (vm_id | default('')) + }} + system_cpus_effective: >- + {{ + system.cpus + if system.cpus is defined and (system.cpus | int) > 0 + else (vm_cpus | default(0)) + }} + system_memory_mb_effective: >- + {{ + system.memory_mb + if system.memory_mb is defined and (system.memory_mb | int) > 0 + else (vm_memory | default(0)) + }} + system_balloon_mb_effective: >- + {{ + system.balloon_mb + if system.balloon_mb is defined and (system.balloon_mb | int) > 0 + else (vm_ballo | default('')) + }} + system_network_effective: >- + {{ + system.network + if system.network is defined and (system.network | string | length) > 0 + else (vm_nif | default('')) + }} + system_vlan_effective: >- + {{ + system.vlan + if system.vlan is defined and (system.vlan | string | length) > 0 + else (vlan_name | default('')) + }} + system_ip_effective: >- + {{ + system.ip + if system.ip is defined and (system.ip | string | length) > 0 + else (vm_ip | default('')) + }} + system_prefix_effective: >- + {{ + system.prefix + if system.prefix is defined and (system.prefix | int) > 0 + else (vm_nms | default('')) + }} + system_gateway_effective: >- + {{ + system.gateway + if system.gateway is defined and (system.gateway | string | length) > 0 + else (vm_gw | default('')) + }} + system_dns_servers_effective: >- + {{ + system.dns_servers + if system.dns_servers is defined + else (vm_dns | default([])) + }} + system_dns_search_effective: >- + {{ + system.dns_search + if system.dns_search is defined + else (vm_dns_search | default([])) + }} + system_path_effective: >- + {{ + system.path + if system.path is defined and (system.path | string | length) > 0 + else ( + system.hypervisor_path + if system.hypervisor_path is defined and (system.hypervisor_path | string | length) > 0 + else (vm_path | default('')) + ) + }} + ansible.builtin.set_fact: + hostname: "{{ system_name_effective }}" + system_cfg: >- + {{ + system_defaults + | combine(system, recursive=True) + | combine( + { + 'name': system_name_effective, + 'id': system_id_effective, + 'cpus': system_cpus_effective, + 'memory_mb': system_memory_mb_effective, + 'balloon_mb': system_balloon_mb_effective, + 'network': system_network_effective, + 'vlan': system_vlan_effective, + 'ip': system_ip_effective, + 'prefix': system_prefix_effective, + 'gateway': system_gateway_effective, + 'dns_servers': system_dns_servers_effective, + 'dns_search': system_dns_search_effective, + 'path': system_path_effective + }, + recursive=True + ) + }} + changed_when: false + +- name: Normalize system disks input + vars: + system_disk_defaults: + size: 0 + device: "" + mount: "" + fstype: "" + label: "" + opts: "defaults" + system_disks_raw: >- + {{ + system_cfg.disks + if system_cfg.disks is defined + else [] + }} + system_disks_legacy: >- + {{ + [ {'size': vm_size} ] + if (system_disks_raw | length) == 0 and (vm_size is defined and (vm_size | float) > 0) + else [] + }} + system_disks_effective: >- + {{ + system_disks_raw + if (system_disks_raw | length) > 0 + else system_disks_legacy + }} + system_disk_device_prefix: >- + {{ + '/dev/vd' + if (install_type | default('')) == 'virtual' and (hypervisor | default('')) == 'libvirt' + else ( + '/dev/xvd' + if (install_type | default('')) == 'virtual' and (hypervisor | default('')) == 'xen' + else ( + '/dev/sd' + if (install_type | default('')) == 'virtual' + and (hypervisor | default('')) in ['proxmox', 'vmware'] + else '' + ) + ) + }} + system_disk_letter_map: "abcdefghijklmnopqrstuvwxyz" + block: + - name: Validate system disks type + ansible.builtin.assert: + that: + - system_disks_effective is sequence + fail_msg: "system.disks must be a list" + quiet: true + + - name: Validate system disk items + ansible.builtin.assert: + that: + - item is mapping + fail_msg: "Each system disk entry must be a dictionary" + quiet: true + loop: "{{ system_disks_effective }}" + loop_control: + label: "{{ item | to_json }}" + + - name: Validate system disk count + ansible.builtin.assert: + that: + - (system_disks_effective | length) <= 26 + fail_msg: "system.disks supports at most 26 entries." + quiet: true + + - name: Build normalized system disk configuration + ansible.builtin.set_fact: + system_disks_cfg: "{{ system_disks_cfg | default([]) + [system_disk_cfg] }}" + vars: + disk_idx: "{{ ansible_loop.index0 }}" + disk_letter: "{{ system_disk_letter_map[disk_idx] }}" + disk_device_default: >- + {{ + ( + install_drive + if disk_idx == 0 and install_drive is defined and (install_drive | string | length) > 0 + else (system_disk_device_prefix ~ disk_letter) + ) + if (install_type | default('')) == 'virtual' + else ( + install_drive + if disk_idx == 0 and install_drive is defined and (install_drive | string | length) > 0 + else '' + ) + }} + system_disk_cfg_base: "{{ system_disk_defaults | combine(item, recursive=True) }}" + system_disk_cfg_tmp: >- + {{ + system_disk_cfg_base + | combine( + { + 'device': ( + system_disk_cfg_base.device + if system_disk_cfg_base.device | string | length > 0 + else disk_device_default + ), + 'fstype': ( + system_disk_cfg_base.fstype + if system_disk_cfg_base.fstype | string | length > 0 + else ( + 'ext4' + if system_disk_cfg_base.mount | string | length > 0 + else '' + ) + ) + }, + recursive=True + ) + }} + system_disk_partition_device: >- + {{ + system_disk_cfg_tmp.device + ~ ('p1' if (system_disk_cfg_tmp.device | regex_search('\\d$')) else '1') + }} + system_disk_cfg: >- + {{ + system_disk_cfg_tmp + | combine( + { + 'partition': system_disk_partition_device + }, + recursive=True + ) + }} + loop: "{{ system_disks_effective }}" + loop_control: + loop_var: item + extended: true + label: "{{ item | to_json }}" + + - name: Update system configuration with normalized disks + ansible.builtin.set_fact: + system_cfg: "{{ system_cfg | combine({'disks': system_disks_cfg | default([])}, recursive=True) }}" + changed_when: false + + - name: Set install_drive from system disk definition (if needed) + when: + - (install_drive is not defined) or (install_drive | string | length) == 0 + - system_disks_cfg | length > 0 + - system_disks_cfg[0].device | string | length > 0 + ansible.builtin.set_fact: + install_drive: "{{ system_disks_cfg[0].device }}" + changed_when: false + + - name: Set legacy vm_* aliases (compat) + ansible.builtin.set_fact: + vm_id: "{{ system_cfg.id }}" + vm_cpus: "{{ system_cfg.cpus }}" + vm_memory: "{{ system_cfg.memory_mb }}" + vm_ballo: "{{ system_cfg.balloon_mb }}" + vm_nif: "{{ system_cfg.network }}" + vlan_name: "{{ system_cfg.vlan }}" + vm_ip: "{{ system_cfg.ip }}" + vm_nms: "{{ system_cfg.prefix }}" + vm_gw: "{{ system_cfg.gateway }}" + vm_dns: "{{ system_cfg.dns_servers }}" + vm_dns_search: "{{ system_cfg.dns_search }}" + vm_path: "{{ system_cfg.path }}" + vm_size: "{{ (system_cfg.disks | default([]) | first | default({})).size | default(0) }}" + changed_when: false diff --git a/roles/global_defaults/tasks/validation.yml b/roles/global_defaults/tasks/validation.yml new file mode 100644 index 0000000..be0de80 --- /dev/null +++ b/roles/global_defaults/tasks/validation.yml @@ -0,0 +1,234 @@ +--- +- name: Validate core variables + ansible.builtin.assert: + that: + - install_type is defined + - install_type in ["virtual", "physical"] + - hypervisor is defined + - hypervisor in ["libvirt", "proxmox", "vmware", "xen", "none"] + - filesystem is defined + - filesystem in ["btrfs", "ext4", "xfs"] + - install_drive is defined + - install_drive | string | length > 0 + - hostname is defined + - hostname | string | length > 0 + fail_msg: Invalid core variables were specified, please check your inventory/vars. + quiet: true + +- name: Validate install_type/hypervisor relationship + ansible.builtin.assert: + that: + - install_type == "physical" or hypervisor in ["libvirt", "proxmox", "vmware", "xen"] + fail_msg: "hypervisor must be one of: libvirt, proxmox, vmware, xen when install_type=virtual." + quiet: true + +- name: Validate OS and version inputs + ansible.builtin.assert: + that: + - os is defined + - os in ["almalinux", "alpine", "archlinux", "debian", "fedora", "opensuse", "rhel", "rocky", "ubuntu", "ubuntu-lts", "void"] + - >- + os not in ["debian", "fedora", "rocky", "almalinux", "rhel"] + or (os_version is defined and (os_version | string | length) > 0) + - >- + os_version is not defined or (os_version | string | length) == 0 + or ( + os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"] + ) or ( + os == "fedora" and (os_version | string) in ["40", "41", "42", "43"] + ) or ( + os in ["rocky", "almalinux"] + and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$") + ) or ( + os == "rhel" + and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$") + ) or ( + os in ["alpine", "archlinux", "opensuse", "ubuntu", "ubuntu-lts", "void"] + ) + fail_msg: "Invalid os/os_version specified. Please check README.md for supported values." + quiet: true + +- name: Validate RHEL ISO requirement + ansible.builtin.assert: + that: + - os != "rhel" or (rhel_iso is defined and (rhel_iso | string | length) > 0) + fail_msg: "rhel_iso is required when os=rhel." + quiet: true + +- name: Validate Proxmox hypervisor inputs + when: + - install_type == "virtual" + - hypervisor == "proxmox" + ansible.builtin.assert: + that: + - hypervisor_cfg.url | string | length > 0 + - hypervisor_cfg.username | string | length > 0 + - hypervisor_cfg.password | string | length > 0 + - hypervisor_cfg.node | string | length > 0 + - hypervisor_cfg.storage | string | length > 0 + - system_cfg.id | string | length > 0 + - system_cfg.network | string | length > 0 + fail_msg: "Missing required Proxmox inputs. Define hypervisor.(url,username,password,node,storage) and system.(id,network)." + quiet: true + +- name: Validate VMware hypervisor inputs + when: + - install_type == "virtual" + - hypervisor == "vmware" + ansible.builtin.assert: + that: + - hypervisor_cfg.url | string | length > 0 + - hypervisor_cfg.username | string | length > 0 + - hypervisor_cfg.password | string | length > 0 + - hypervisor_cfg.datacenter | string | length > 0 + - hypervisor_cfg.cluster | string | length > 0 + - hypervisor_cfg.storage | string | length > 0 + - system_cfg.network | string | length > 0 + fail_msg: "Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage) and system.network." + quiet: true + +- name: Validate Xen hypervisor inputs + when: + - install_type == "virtual" + - hypervisor == "xen" + ansible.builtin.assert: + that: + - system_cfg.network | string | length > 0 + fail_msg: "Missing required Xen inputs. Define system.network." + quiet: true + +- name: Validate virtual installer ISO requirement + ansible.builtin.assert: + that: + - install_type == "physical" or (boot_iso is defined and (boot_iso | string | length) > 0) + fail_msg: "boot_iso is required when install_type=virtual." + quiet: true + +- name: Validate firewall and feature flags + ansible.builtin.assert: + that: + - firewall_backend is defined + - firewall_backend in ["firewalld", "ufw"] + - firewall_toolkit is defined + - firewall_toolkit in ["iptables", "nftables"] + - firewall_enabled is defined + - motd_enabled is defined + - sudo_banner_enabled is defined + - luks_enabled is defined + - chroot_tool is defined + - chroot_tool in ["arch-chroot", "chroot", "systemd-nspawn"] + fail_msg: Invalid feature flags were specified, please check your inventory/vars. + quiet: true + +- name: Validate system configuration exists + ansible.builtin.assert: + that: + - system_cfg is defined + - system_cfg is mapping + fail_msg: "system configuration is missing. Define system: {...} or legacy vm_* variables." + quiet: true + +- name: Validate virtual system sizing + when: install_type == "virtual" + ansible.builtin.assert: + that: + - system_cfg.cpus is defined and (system_cfg.cpus | int) > 0 + - system_cfg.memory_mb is defined and (system_cfg.memory_mb | int) > 0 + - system_cfg.disks is defined and (system_cfg.disks | length) > 0 + - (system_cfg.disks[0].size | float) > 0 + - (system_cfg.disks[0].size | float) >= 20 + - >- + filesystem != "btrfs" + or ( + (system_cfg.disks[0].size | float) + >= ( + ( + (system_cfg.memory_mb | float / 1024 >= 16.0) + | ternary( + (system_cfg.memory_mb | float / 2048), + [system_cfg.memory_mb | float / 1024, 4.0] | max + ) + ) + + 5.5 + ) + ) + fail_msg: "Invalid system sizing. Check system.cpus, system.memory_mb, and system.disks[0].size." + quiet: true + +- name: Validate all virtual disks have a positive size + when: install_type == "virtual" + ansible.builtin.assert: + that: + - item.size is defined + - (item.size | float) > 0 + fail_msg: "Each system disk must have a positive size when install_type=virtual: {{ item | to_json }}" + quiet: true + loop: "{{ system_cfg.disks | default([]) }}" + loop_control: + label: "{{ item | to_json }}" + +- name: Validate primary disk mount is not used + when: + - system_cfg.disks is defined + - system_cfg.disks | length > 0 + ansible.builtin.assert: + that: + - (system_cfg.disks[0].mount | default('') | string | trim) == '' + fail_msg: "system.disks[0].mount must be empty; use system.disks[1:] for additional mounts." + quiet: true + +- name: Validate disk mountpoint inputs + vars: + system_disk_mounts: >- + {{ + (system_cfg.disks | default([])) + | map(attribute='mount') + | map('string') + | map('trim') + | reject('equalto', '') + | list + }} + ansible.builtin.assert: + that: + - system_disk_mounts | length == (system_disk_mounts | unique | list | length) + fail_msg: "Duplicate disk mountpoints found in system.disks." + quiet: true + +- name: Validate disk mount definitions + when: system_cfg.disks is defined + vars: + reserved_mounts: + - /boot + - /boot/efi + - /home + - /swap + - /var + - /var/cache/pacman/pkg + - /var/log + - /var/log/audit + disk_mount: "{{ (item.mount | default('') | string) | trim }}" + disk_fstype: "{{ (item.fstype | default('') | string) | trim }}" + disk_device: "{{ (item.device | default('') | string) | trim }}" + disk_size: "{{ item.size | default(0) }}" + ansible.builtin.assert: + that: + - disk_mount == "" or disk_mount.startswith("/") + - disk_mount == "" or disk_mount != "/" + - disk_mount == "" or disk_mount not in reserved_mounts + - disk_mount == "" or disk_fstype in ["btrfs", "ext4", "xfs"] + - disk_mount == "" or install_type == "virtual" or (disk_device | length) > 0 + - disk_mount == "" or install_type != "virtual" or (disk_size | float) > 0 + fail_msg: "Invalid system disk entry: {{ item | to_json }}" + quiet: true + loop: "{{ system_cfg.disks }}" + loop_control: + label: "{{ item | to_json }}" + +- name: Validate static IP requirements + when: system_cfg.ip is defined and (system_cfg.ip | string | length) > 0 + ansible.builtin.assert: + that: + - system_cfg.prefix is defined + - (system_cfg.prefix | int) > 0 + fail_msg: "system.prefix is required when system.ip is set." + quiet: true diff --git a/roles/partitioning/defaults/main.yml b/roles/partitioning/defaults/main.yml index 0e5fb9b..6abda16 100644 --- a/roles/partitioning/defaults/main.yml +++ b/roles/partitioning/defaults/main.yml @@ -51,7 +51,7 @@ partitioning_efi_mountpoint: >- if (partitioning_separate_boot | bool) else ( '/boot/efi' - if is_rhel or (os | lower in ['debian11', 'debian12', 'debian13', 'ubuntu', 'ubuntu-lts']) + if is_rhel or (os in ['ubuntu', 'ubuntu-lts'] or (os == 'debian' and (os_version | string) in ['11', '12', '13'])) else '/boot' ) }} diff --git a/roles/partitioning/tasks/btrfs.yml b/roles/partitioning/tasks/btrfs.yml index b1ae080..1b8d9b0 100644 --- a/roles/partitioning/tasks/btrfs.yml +++ b/roles/partitioning/tasks/btrfs.yml @@ -47,20 +47,20 @@ args: creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }} loop: - - {subvol: root} - - {subvol: swap} - - {subvol: home} - - {subvol: var} - - {subvol: pkg} - - {subvol: var_log} - - {subvol: var_log_audit} + - { subvol: root } + - { subvol: swap } + - { subvol: home } + - { subvol: var } + - { subvol: pkg } + - { subvol: var_log } + - { subvol: var_log_audit } register: partitioning_btrfs_subvol_result - name: Set quotas for subvolumes when: cis_enabled ansible.builtin.command: btrfs qgroup limit {{ item.quota }} /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }} loop: - - {subvol: home, quota: 2G} + - { subvol: home, quota: 2G } register: partitioning_btrfs_qgroup_result changed_when: false diff --git a/roles/partitioning/tasks/ext4.yml b/roles/partitioning/tasks/ext4.yml index 1225200..a5abbfe 100644 --- a/roles/partitioning/tasks/ext4.yml +++ b/roles/partitioning/tasks/ext4.yml @@ -6,20 +6,22 @@ fstype: ext4 force: true loop: - - {lv: root} - - {lv: home} - - {lv: var} - - {lv: var_log} - - {lv: var_log_audit} + - { lv: root } + - { lv: home } + - { lv: var } + - { lv: var_log } + - { lv: var_log_audit } - name: Remove Unsupported features for older Systems - when: (os | lower in ['almalinux', 'debian11', 'rhel8', 'rhel9', 'rocky']) and (cis_enabled or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']) + when: > + (os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11')) + and (cis_enabled or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']) ansible.builtin.command: tune2fs -O "^orphan_file,^metadata_csum_seed" "/dev/sys/{{ item.lv }}" loop: - - {lv: root} - - {lv: home} - - {lv: var} - - {lv: var_log} - - {lv: var_log_audit} + - { lv: root } + - { lv: home } + - { lv: var } + - { lv: var_log } + - { lv: var_log_audit } register: partitioning_ext4_tune_result changed_when: partitioning_ext4_tune_result.rc == 0 diff --git a/roles/partitioning/tasks/extra_disks.yml b/roles/partitioning/tasks/extra_disks.yml new file mode 100644 index 0000000..f57d23b --- /dev/null +++ b/roles/partitioning/tasks/extra_disks.yml @@ -0,0 +1,85 @@ +--- +- name: Determine additional disks to auto-mount + ansible.builtin.set_fact: + partitioning_extra_disks: >- + {{ + (system_cfg.disks | default([]))[1:] + | selectattr('mount') + | list + }} + changed_when: false + +- name: Validate additional disks do not target install_drive + when: partitioning_extra_disks | length > 0 + ansible.builtin.assert: + that: + - item.device is defined + - item.device | string | length > 0 + - item.device != install_drive + - item.partition is defined + - item.partition | string | length > 0 + - item.fstype is defined + - item.fstype in ['btrfs', 'ext4', 'xfs'] + - item.mount is defined + - item.mount | string | length > 0 + - item.mount.startswith('/') + - item.mount != '/' + fail_msg: "Invalid additional disk definition: {{ item | to_json }}" + quiet: true + loop: "{{ partitioning_extra_disks }}" + loop_control: + label: "{{ item | to_json }}" + +- name: Partition additional disks + when: partitioning_extra_disks | length > 0 + community.general.parted: + device: "{{ item.device }}" + label: gpt + number: 1 + part_start: "1MiB" + part_end: "100%" + name: "{{ (item.label | default('') | string | length > 0) | ternary(item.label, 'data') }}" + state: present + loop: "{{ partitioning_extra_disks }}" + loop_control: + label: "{{ item.device }}" + +- name: Settle partition tables for additional disks + when: partitioning_extra_disks | length > 0 + ansible.builtin.command: udevadm settle + changed_when: false + +- name: Create filesystems on additional disks + when: partitioning_extra_disks | length > 0 + community.general.filesystem: + dev: "{{ item.partition }}" + fstype: "{{ item.fstype }}" + opts: "{{ ('-L ' ~ item.label) if (item.label | default('') | string | length) > 0 else omit }}" + force: true + loop: "{{ partitioning_extra_disks }}" + loop_control: + label: "{{ item.partition }}" + +- name: Ensure mount directories exist for additional disks + when: partitioning_extra_disks | length > 0 + ansible.builtin.file: + path: "/mnt{{ item.mount }}" + state: directory + owner: root + group: root + mode: "0755" + loop: "{{ partitioning_extra_disks }}" + loop_control: + label: "{{ item.mount }}" + +- name: Mount additional disks for fstab generation + when: partitioning_extra_disks | length > 0 + ansible.posix.mount: + path: "/mnt{{ item.mount }}" + src: "{{ item.partition }}" + fstype: "{{ item.fstype }}" + opts: "{{ item.opts | default('defaults') }}" + state: mounted + loop: "{{ partitioning_extra_disks }}" + loop_control: + label: "{{ item.mount }}" diff --git a/roles/partitioning/tasks/main.yml b/roles/partitioning/tasks/main.yml index 428f4fa..baf3509 100644 --- a/roles/partitioning/tasks/main.yml +++ b/roles/partitioning/tasks/main.yml @@ -403,9 +403,9 @@ size: "{{ partitioning_lvm_swap_gb | string + 'G' }}" - lv: home size: "{{ partitioning_lvm_home_gb | string + 'G' }}" - - {lv: var, size: "2G"} - - {lv: var_log, size: "2G"} - - {lv: var_log_audit, size: "1.5G"} + - { lv: var, size: "2G" } + - { lv: var_log, size: "2G" } + - { lv: var_log_audit, size: "1.5G" } - name: Create filesystems block: @@ -427,7 +427,7 @@ when: - partitioning_separate_boot | bool - partitioning_boot_fs_fstype == 'ext4' - - os | lower in ['almalinux', 'debian11', 'rhel8', 'rhel9', 'rocky'] + - os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11') ansible.builtin.command: >- tune2fs -O "^orphan_file,^metadata_csum_seed" "{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}" @@ -548,14 +548,14 @@ - name: Mount filesystems and subvolumes when: - >- - cis_enabled or ( - not cis_enabled and ( - (filesystem == 'btrfs' and item.path in ['/home', '/var/log', '/var/cache/pacman/pkg']) - or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg']) - ) + cis_enabled or ( + not cis_enabled and ( + (filesystem == 'btrfs' and item.path in ['/home', '/var/log', '/var/cache/pacman/pkg']) + or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg']) ) + ) - >- - not (item.path in ['/swap', '/var/cache/pacman/pkg'] and filesystem != 'btrfs') + not (item.path in ['/swap', '/var/cache/pacman/pkg'] and filesystem != 'btrfs') - swap_enabled | bool or item.path != '/swap' ansible.posix.mount: path: /mnt{{ item.path }} @@ -663,3 +663,6 @@ ansible.builtin.command: "{{ partitioning_swap_cmd }}" register: partitioning_swap_activate_result changed_when: partitioning_swap_activate_result.rc == 0 + +- name: Mount additional disks + ansible.builtin.include_tasks: extra_disks.yml diff --git a/roles/partitioning/tasks/xfs.yml b/roles/partitioning/tasks/xfs.yml index 32e6b7a..8fc903f 100644 --- a/roles/partitioning/tasks/xfs.yml +++ b/roles/partitioning/tasks/xfs.yml @@ -6,8 +6,8 @@ fstype: xfs force: true loop: - - {lv: root} - - {lv: home} - - {lv: var} - - {lv: var_log} - - {lv: var_log_audit} + - { lv: root } + - { lv: home } + - { lv: var } + - { lv: var_log } + - { lv: var_log_audit } diff --git a/roles/system_check/tasks/main.yml b/roles/system_check/tasks/main.yml new file mode 100644 index 0000000..1b9d538 --- /dev/null +++ b/roles/system_check/tasks/main.yml @@ -0,0 +1,188 @@ +--- +- name: Gather minimal facts for safety checks + ansible.builtin.setup: + gather_subset: + - "!all" + - "min" + - "mounts" + changed_when: false + +- name: Production system protection check + block: + - name: Check for OS release information + ansible.builtin.stat: + path: /etc/os-release + register: system_check_os_release_exists + + - name: Check for live environment markers + ansible.builtin.stat: + path: "{{ item }}" + loop: + - /run/archiso + - /run/live + - /run/initramfs + - /run/initramfs/live + register: system_check_live_markers + changed_when: false + + - name: Determine root filesystem type + ansible.builtin.set_fact: + system_check_root_fstype: >- + {{ + ansible_mounts + | selectattr('mount', 'equalto', '/') + | map(attribute='fstype') + | list + | first + | default('') + | lower + }} + changed_when: false + + - name: Identify live environment indicators + ansible.builtin.set_fact: + system_check_is_live_environment: >- + {{ + (not system_check_os_release_exists.stat.exists) + or ( + system_check_live_markers.results + | selectattr('stat.exists') + | list + | length + > 0 + ) + or system_check_root_fstype in ['overlay', 'overlayfs', 'squashfs', 'aufs'] + or (ansible_hostname | default('') | lower is search('live')) + }} + changed_when: false + + - name: Assert target is not a production system + ansible.builtin.assert: + that: + - system_check_is_live_environment | bool + fail_msg: | + PRODUCTION SYSTEM DETECTED - ABORTING + + The target system appears to be a production environment with an already + installed operating system. This playbook is designed to run ONLY on + live installer environments (e.g., ArchLinux ISO, Debian netinst). + + DO NOT proceed on production systems. This could result in data loss. + + To use this playbook: + 1. Boot from a live installer ISO (ArchLinux, Debian, Ubuntu, etc.) + 2. Run playbook against live environment + 3. Target an empty disk for installation + + If you are certain you want to proceed, you must verify you are running + from a live environment, not an installed system. + quiet: true + +- name: VM existence protection check + when: install_type == "virtual" + block: + - name: Check if VM already exists on libvirt + when: hypervisor == "libvirt" + delegate_to: localhost + become: false + community.libvirt.virt: + command: list_vms + register: system_check_libvirt_existing_vms + changed_when: false + failed_when: false + + - name: Abort if VM already exists on libvirt + when: hypervisor == "libvirt" + ansible.builtin.assert: + that: + - hostname not in system_check_libvirt_existing_vms.domains | default([]) + fail_msg: | + VM {{ hostname }} already exists on libvirt hypervisor. + To avoid data loss, the playbook will not overwrite or delete existing VMs. + Please choose a different hostname or remove the existing VM manually before proceeding. + quiet: true + + - name: Check if VM already exists on Proxmox + when: hypervisor == "proxmox" + delegate_to: localhost + become: false + community.proxmox.proxmox_vm_info: + api_host: "{{ hypervisor_url }}" + api_user: "{{ hypervisor_username }}" + api_password: "{{ hypervisor_password }}" + node: "{{ hypervisor_node }}" + vmid: "{{ system_cfg.id }}" + name: "{{ hostname }}" + type: qemu + register: system_check_proxmox_check_result + changed_when: false + + - name: Abort if VM already exists on Proxmox + when: hypervisor == "proxmox" + ansible.builtin.assert: + that: + - system_check_proxmox_check_result.proxmox_vms | default([]) | length == 0 + fail_msg: | + VM {{ hostname }} (ID: {{ system_cfg.id }}) already exists on Proxmox hypervisor. + To avoid data loss, the playbook will not overwrite or delete existing VMs. + Please choose a different hostname or VM ID, or remove the existing VM manually before proceeding. + quiet: true + + - name: Check if VM already exists in vCenter + when: hypervisor == "vmware" + delegate_to: localhost + community.vmware.vmware_guest_info: + hostname: "{{ hypervisor_url }}" + username: "{{ hypervisor_username }}" + password: "{{ hypervisor_password }}" + validate_certs: "{{ hypervisor_validate_certs }}" + datacenter: "{{ hypervisor_datacenter }}" + name: "{{ hostname }}" + folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}" + register: system_check_vmware_check_result + failed_when: false + changed_when: false + + - name: Fail if vCenter lookup failed unexpectedly + when: hypervisor == "vmware" + ansible.builtin.assert: + that: + - not system_check_vmware_check_result.failed + or (system_check_vmware_check_result.msg is search('non-existing VM')) + fail_msg: | + Unable to verify VM existence in vCenter. + {{ system_check_vmware_check_result.msg | default('Unknown error') }} + quiet: true + + - name: Abort if VM already exists in vCenter + when: hypervisor == "vmware" + ansible.builtin.assert: + that: + - system_check_vmware_check_result.instance is not defined + fail_msg: | + VM {{ hostname }} already exists in vCenter. + To avoid data loss, the playbook will not overwrite or delete existing VMs. + Please choose a different hostname or remove the existing VM manually before proceeding. + quiet: true + + - name: Check if VM already exists on Xen + when: hypervisor == "xen" + delegate_to: localhost + ansible.builtin.command: + argv: + - xl + - list + register: system_check_xen_existing_vms + changed_when: false + failed_when: false + + - name: Abort if VM already exists on Xen + when: hypervisor == "xen" + ansible.builtin.assert: + that: + - hostname not in system_check_xen_existing_vms.stdout | default('') + fail_msg: | + VM {{ hostname }} already exists on Xen hypervisor. + To avoid data loss, the playbook will not overwrite or delete existing VMs. + Please choose a different hostname or remove the existing VM manually before proceeding. + quiet: true diff --git a/roles/virtualization/defaults/main.yml b/roles/virtualization/defaults/main.yml index f1e02d8..eb05274 100644 --- a/roles/virtualization/defaults/main.yml +++ b/roles/virtualization/defaults/main.yml @@ -1,12 +1,15 @@ --- virtualization_libvirt_image_dir: >- - {{ vm_path if vm_path is defined and vm_path | length > 0 else '/var/lib/libvirt/images' }} + {{ + system_cfg.path + if system_cfg is defined and (system_cfg.path | string | length) > 0 + else '/var/lib/libvirt/images' + }} virtualization_libvirt_disk_path: >- {{ [virtualization_libvirt_image_dir, hostname ~ '.qcow2'] | ansible.builtin.path_join }} virtualization_libvirt_cloudinit_path: >- {{ [virtualization_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }} -virtualization_mac_address: >- - {{ '52:54:00' | community.general.random_mac(seed=hostname) }} +virtualization_xen_disk_path: /var/lib/xen/images virtualization_tpm2_enabled: >- {{ diff --git a/roles/virtualization/tasks/libvirt.yml b/roles/virtualization/tasks/libvirt.yml index c08a662..9615690 100644 --- a/roles/virtualization/tasks/libvirt.yml +++ b/roles/virtualization/tasks/libvirt.yml @@ -1,5 +1,31 @@ --- -- name: Create VM disk +- name: Build disk definitions + ansible.builtin.set_fact: + virtualization_libvirt_disks: "{{ virtualization_libvirt_disks | default([]) + [virtualization_libvirt_disk_cfg] }}" + vars: + device_letter_map: "abcdefghijklmnopqrstuvwxyz" + device_letter: "{{ device_letter_map[ansible_loop.index0] }}" + virtualization_libvirt_disk_cfg: >- + {{ + { + 'path': ( + virtualization_libvirt_disk_path + if ansible_loop.index0 == 0 + else ([virtualization_libvirt_image_dir, hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'] | ansible.builtin.path_join) + ), + 'target': 'vd' ~ device_letter, + 'bus': 'virtio', + 'format': 'qcow2', + 'size': (item.size | float) + } + }} + loop: "{{ system_cfg.disks }}" + loop_control: + label: "{{ item | to_json }}" + extended: true + changed_when: false + +- name: Create VM disks delegate_to: localhost ansible.builtin.command: argv: @@ -7,19 +33,22 @@ - create - -f - qcow2 - - "{{ virtualization_libvirt_disk_path }}" - - "{{ vm_size }}G" - creates: "{{ virtualization_libvirt_disk_path }}" + - "{{ item.path }}" + - "{{ item.size }}G" + creates: "{{ item.path }}" + loop: "{{ virtualization_libvirt_disks }}" + loop_control: + label: "{{ item.path }}" - name: Render cloud config templates delegate_to: localhost ansible.builtin.template: src: "{{ item.src }}" dest: /tmp/{{ item.dest_prefix }}-{{ hostname }}.yml - mode: '0644' + mode: "0644" loop: - - {src: cloud-user-data.yml.j2, dest_prefix: cloud-user-data} - - {src: cloud-network-config.yml.j2, dest_prefix: cloud-network-config} + - { src: cloud-user-data.yml.j2, dest_prefix: cloud-user-data } + - { src: cloud-network-config.yml.j2, dest_prefix: cloud-network-config } - name: Create cloud-init disk delegate_to: localhost @@ -43,3 +72,9 @@ community.libvirt.virt: name: "{{ hostname }}" state: running + register: virtualization_libvirt_start_result + +- name: Set VM created fact + ansible.builtin.set_fact: + virtualization_vm_created_in_run: true + when: virtualization_libvirt_start_result is defined and virtualization_libvirt_start_result.changed | bool diff --git a/roles/virtualization/tasks/proxmox.yml b/roles/virtualization/tasks/proxmox.yml index 74708c0..f840f2e 100644 --- a/roles/virtualization/tasks/proxmox.yml +++ b/roles/virtualization/tasks/proxmox.yml @@ -2,7 +2,7 @@ - name: Deploy VM on Proxmox delegate_to: localhost vars: - virtualization_dns_value: "{{ vm_dns if vm_dns is defined else '' }}" + virtualization_dns_value: "{{ system_cfg.dns_servers if system_cfg.dns_servers is defined else '' }}" virtualization_dns_list_raw: >- {{ virtualization_dns_value @@ -11,7 +11,7 @@ }} virtualization_dns_list: >- {{ virtualization_dns_list_raw | map('trim') | reject('equalto', '') | list }} - virtualization_search_value: "{{ vm_dns_search if vm_dns_search is defined else '' }}" + virtualization_search_value: "{{ system_cfg.dns_search if system_cfg.dns_search is defined else '' }}" virtualization_search_list_raw: >- {{ virtualization_search_value @@ -20,6 +20,12 @@ }} virtualization_search_list: >- {{ virtualization_search_list_raw | map('trim') | reject('equalto', '') | list }} + virtualization_proxmox_scsi: >- + {%- set out = {} -%} + {%- for disk in system_cfg.disks -%} + {%- set _ = out.update({ 'scsi' ~ loop.index0: hypervisor_storage ~ ':' ~ (disk.size | int) }) -%} + {%- endfor -%} + {{ out }} community.proxmox.proxmox_kvm: api_host: "{{ hypervisor_url }}" api_user: "{{ hypervisor_username }}" @@ -28,12 +34,12 @@ cipassword: "{{ user_password }}" ciupgrade: false node: "{{ hypervisor_node }}" - vmid: "{{ vm_id }}" + vmid: "{{ system_cfg.id }}" name: "{{ hostname }}" cpu: host - cores: "{{ vm_cpus }}" - memory: "{{ vm_memory }}" - balloon: "{{ vm_ballo if vm_ballo is defined and vm_ballo | int > 0 else omit }}" + cores: "{{ system_cfg.cpus }}" + memory: "{{ system_cfg.memory_mb }}" + balloon: "{{ system_cfg.balloon_mb if system_cfg.balloon_mb is defined and system_cfg.balloon_mb | int > 0 else omit }}" numa_enabled: true hotplug: network,disk update: "{{ virtualization_tpm2_enabled | bool }}" @@ -42,8 +48,7 @@ machine: "{{ 'q35' if virtualization_tpm2_enabled | bool else omit }}" boot: ac scsihw: virtio-scsi-single - scsi: - scsi0: "{{ hypervisor_storage }}:{{ vm_size }}" + scsi: "{{ virtualization_proxmox_scsi }}" efidisk0: efitype: 4m format: raw @@ -60,13 +65,14 @@ ide1: "{{ rhel_iso + ',media=cdrom' if rhel_iso is defined and rhel_iso | length > 0 else omit }}" ide2: "{{ hypervisor_storage }}:cloudinit" net: - net0: virtio,bridge={{ vm_nif }}{% if vlan_name is defined and vlan_name | length > 0 %},tag={{ vlan_name }}{% endif %} + net0: >- + virtio,bridge={{ system_cfg.network }}{% if system_cfg.vlan is defined and system_cfg.vlan | string | length > 0 %},tag={{ system_cfg.vlan }}{% endif %} ipconfig: ipconfig0: >- {{ - 'ip=' ~ vm_ip ~ '/' ~ vm_nms - ~ (',gw=' ~ vm_gw if vm_gw is defined and vm_gw | length else '') - if vm_ip is defined and vm_ip | length + 'ip=' ~ system_cfg.ip ~ '/' ~ system_cfg.prefix + ~ (',gw=' ~ system_cfg.gateway if system_cfg.gateway is defined and system_cfg.gateway | length else '') + if system_cfg.ip is defined and system_cfg.ip | string | length else 'ip=dhcp' }} nameservers: "{{ virtualization_dns_list if virtualization_dns_list | length else omit }}" @@ -82,5 +88,11 @@ api_password: "{{ hypervisor_password }}" node: "{{ hypervisor_node }}" name: "{{ hostname }}" - vmid: "{{ vm_id }}" + vmid: "{{ system_cfg.id }}" state: started + register: virtualization_proxmox_start_result + +- name: Set VM created fact + ansible.builtin.set_fact: + virtualization_vm_created_in_run: true + when: virtualization_proxmox_start_result is defined and virtualization_proxmox_start_result.changed | bool diff --git a/roles/virtualization/tasks/vmware.yml b/roles/virtualization/tasks/vmware.yml index 3356915..53255eb 100644 --- a/roles/virtualization/tasks/vmware.yml +++ b/roles/virtualization/tasks/vmware.yml @@ -1,26 +1,36 @@ --- +- name: Build vCenter disk list + ansible.builtin.set_fact: + virtualization_vmware_disks: "{{ virtualization_vmware_disks | default([]) + [virtualization_vmware_disk_cfg] }}" + vars: + virtualization_vmware_disk_cfg: + size_gb: "{{ item.size | int }}" + type: thin + datastore: "{{ hypervisor_storage }}" + loop: "{{ system_cfg.disks }}" + loop_control: + label: "{{ item | to_json }}" + changed_when: false + - name: Create VM in vCenter delegate_to: localhost community.vmware.vmware_guest: hostname: "{{ hypervisor_url }}" username: "{{ hypervisor_username }}" password: "{{ hypervisor_password }}" - validate_certs: false + validate_certs: "{{ hypervisor_validate_certs }}" datacenter: "{{ hypervisor_datacenter }}" cluster: "{{ hypervisor_cluster }}" - folder: "{{ vm_path if vm_path is defined and vm_path | length > 0 else omit }}" + folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}" name: "{{ hostname }}" guest_id: otherLinux64Guest annotation: | {{ note if note is defined else '' }} state: "{{ 'poweredoff' if virtualization_tpm2_enabled | bool else 'poweredon' }}" - disk: - - size_gb: "{{ vm_size }}" - type: thin - datastore: "{{ hypervisor_storage }}" + disk: "{{ virtualization_vmware_disks }}" hardware: - memory_mb: "{{ vm_memory }}" - num_cpus: "{{ vm_cpus }}" + memory_mb: "{{ system_cfg.memory_mb }}" + num_cpus: "{{ system_cfg.cpus }}" boot_firmware: efi secure_boot: false cdrom: >- @@ -44,9 +54,18 @@ } ] if rhel_iso is defined and rhel_iso | length > 0 else [] ) }} networks: - - name: "{{ vm_nif }}" + - name: "{{ system_cfg.network }}" type: dhcp - vlan: "{{ vlan_name if vlan_name is defined and vlan_name | length > 0 else omit }}" + vlan: "{{ system_cfg.vlan if system_cfg.vlan is defined and system_cfg.vlan | string | length > 0 else omit }}" + register: virtualization_vmware_create_result + +- name: Set VM created fact when VM was powered on during creation + ansible.builtin.set_fact: + virtualization_vm_created_in_run: true + when: + - virtualization_vmware_create_result is defined + - not virtualization_tpm2_enabled | bool + - virtualization_vmware_create_result.changed | bool - name: Ensure vTPM2 is enabled when required when: virtualization_tpm2_enabled | bool @@ -55,9 +74,9 @@ hostname: "{{ hypervisor_url }}" username: "{{ hypervisor_username }}" password: "{{ hypervisor_password }}" - validate_certs: false + validate_certs: "{{ hypervisor_validate_certs }}" datacenter: "{{ hypervisor_datacenter }}" - folder: "{{ vm_path if vm_path is defined and vm_path | length > 0 else omit }}" + folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}" name: "{{ hostname }}" state: present @@ -68,7 +87,16 @@ hostname: "{{ hypervisor_url }}" username: "{{ hypervisor_username }}" password: "{{ hypervisor_password }}" - validate_certs: false + validate_certs: "{{ hypervisor_validate_certs }}" datacenter: "{{ hypervisor_datacenter }}" name: "{{ hostname }}" state: powered-on + register: virtualization_vmware_start_result + +- name: Set VM created fact when VM was started separately (TPM2 case) + ansible.builtin.set_fact: + virtualization_vm_created_in_run: true + when: + - virtualization_tpm2_enabled | bool + - virtualization_vmware_start_result is defined + - virtualization_vmware_start_result.changed | bool diff --git a/roles/virtualization/tasks/xen.yml b/roles/virtualization/tasks/xen.yml new file mode 100644 index 0000000..701002e --- /dev/null +++ b/roles/virtualization/tasks/xen.yml @@ -0,0 +1,77 @@ +--- +- name: Deploy VM on Xen + block: + - name: Build disk definitions + ansible.builtin.set_fact: + virtualization_xen_disks: "{{ virtualization_xen_disks | default([]) + [virtualization_xen_disk_cfg] }}" + vars: + device_letter_map: "abcdefghijklmnopqrstuvwxyz" + device_letter: "{{ device_letter_map[ansible_loop.index0] }}" + virtualization_xen_disk_cfg: >- + {{ + { + 'path': ( + virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2' + if ansible_loop.index0 == 0 + else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2' + ), + 'target': 'xvd' ~ device_letter, + 'size': (item.size | float) + } + }} + loop: "{{ system_cfg.disks }}" + loop_control: + label: "{{ item | to_json }}" + extended: true + changed_when: false + + - name: Create VM disks for Xen + delegate_to: localhost + ansible.builtin.command: + argv: + - qemu-img + - create + - -f + - qcow2 + - "{{ item.path }}" + - "{{ item.size }}G" + creates: "{{ item.path }}" + loop: "{{ virtualization_xen_disks }}" + loop_control: + label: "{{ item.path }}" + + - name: Render Xen VM configuration + delegate_to: localhost + vars: + xen_installer_media_enabled: true + ansible.builtin.template: + src: xen.cfg.j2 + dest: /tmp/xen-{{ hostname }}.cfg + mode: "0644" + + - name: Create Xen VM + delegate_to: localhost + ansible.builtin.command: + argv: + - xl + - create + - /tmp/xen-{{ hostname }}.cfg + register: virtualization_xen_create_result + changed_when: virtualization_xen_create_result.rc == 0 + + - name: Ensure VM is running + delegate_to: localhost + ansible.builtin.command: + argv: + - xl + - list + register: virtualization_xen_list_result + changed_when: false + failed_when: false + + - name: Set VM created fact + ansible.builtin.set_fact: + virtualization_vm_created_in_run: true + when: + - virtualization_xen_list_result is defined + - hostname in virtualization_xen_list_result.stdout diff --git a/roles/virtualization/templates/cloud-network-config.yml.j2 b/roles/virtualization/templates/cloud-network-config.yml.j2 index dbcefa3..584eb3f 100644 --- a/roles/virtualization/templates/cloud-network-config.yml.j2 +++ b/roles/virtualization/templates/cloud-network-config.yml.j2 @@ -4,27 +4,27 @@ network: id0: match: macaddress: "{{ virtualization_mac_address }}" -{% set has_static = vm_ip is defined and vm_ip | length %} -{% set dns_value = vm_dns if vm_dns is defined else '' %} +{% set has_static = system_cfg.ip is defined and system_cfg.ip | string | length %} +{% set dns_value = system_cfg.dns_servers if system_cfg.dns_servers is defined else '' %} {% set dns_list_raw = dns_value if dns_value is iterable and dns_value is not string else dns_value.split(',') %} {% set dns_list = dns_list_raw | map('trim') | reject('equalto', '') | list %} -{% set search_value = vm_dns_search if vm_dns_search is defined else '' %} +{% set search_value = system_cfg.dns_search if system_cfg.dns_search is defined else '' %} {% set search_list_raw = search_value if search_value is iterable and search_value is not string else search_value.split(',') %} {% set search_list = search_list_raw | map('trim') | reject('equalto', '') | list %} {% if has_static %} addresses: - - "{{ vm_ip }}/{{ vm_nms }}" -{% if vm_gw is defined and vm_gw | length %} - gateway4: "{{ vm_gw }}" + - "{{ system_cfg.ip }}/{{ system_cfg.prefix }}" +{% if system_cfg.gateway is defined and system_cfg.gateway | string | length %} + gateway4: "{{ system_cfg.gateway }}" {% endif %} {% else %} dhcp4: true -{% if (vm_dns is defined and vm_dns | length) or (vm_dns_search is defined and vm_dns_search | length) %} +{% if (system_cfg.dns_servers is defined and system_cfg.dns_servers | length) or (system_cfg.dns_search is defined and system_cfg.dns_search | length) %} dhcp4-overrides: -{% if vm_dns is defined and vm_dns | length %} +{% if system_cfg.dns_servers is defined and system_cfg.dns_servers | length %} use-dns: false {% endif %} -{% if vm_dns_search is defined and vm_dns_search | length %} +{% if system_cfg.dns_search is defined and system_cfg.dns_search | length %} use-domains: false {% endif %} {% endif %} diff --git a/roles/virtualization/templates/vm.xml.j2 b/roles/virtualization/templates/vm.xml.j2 index edf934e..de2953f 100644 --- a/roles/virtualization/templates/vm.xml.j2 +++ b/roles/virtualization/templates/vm.xml.j2 @@ -1,8 +1,8 @@ {{ hostname }} - {{ vm_memory | int * 1024 }} - {% if vm_ballo is defined and vm_ballo | int > 0 %}{{ vm_ballo | int * 1024 }}{% endif %} - {{ vm_cpus }} + {{ system_cfg.memory_mb | int * 1024 }} + {% if system_cfg.balloon_mb is defined and system_cfg.balloon_mb | int > 0 %}{{ system_cfg.balloon_mb | int * 1024 }}{% endif %} + {{ system_cfg.cpus }} hvm @@ -22,11 +22,13 @@ restart destroy + {% for disk in virtualization_libvirt_disks | default([]) %} - - - + + + + {% endfor %} diff --git a/templates/xen.cfg.j2 b/templates/xen.cfg.j2 new file mode 100644 index 0000000..1931b37 --- /dev/null +++ b/templates/xen.cfg.j2 @@ -0,0 +1,17 @@ +builder = "hvm" +name = "{{ hostname }}" +memory = "{{ system_cfg.memory_mb }}" +vcpus = "{{ system_cfg.cpus }}" +disk = [ +{%- for disk in virtualization_xen_disks | default([]) -%} + 'file:{{ disk.path }},{{ disk.target }},w'{% if not loop.last or xen_installer_media_enabled | bool %}, {% endif %} +{%- endfor -%} +{%- if xen_installer_media_enabled | bool -%} + '{{ boot_iso }},,hdc,cdrom'{% if rhel_iso is defined and rhel_iso | length > 0 %}, '{{ rhel_iso }},,hdd,cdrom'{% endif %} +{%- endif -%} +] +vif = [ 'bridge={{ system_cfg.network }},model=e1000' ] +boot = "{{ 'dc' if xen_installer_media_enabled | bool else 'c' }}" +on_crash = "preserve" +on_poweroff = "destroy" +serial = "pty" diff --git a/vars_baremetal_example.yml b/vars_baremetal_example.yml index 57d7ec2..76cc32a 100644 --- a/vars_baremetal_example.yml +++ b/vars_baremetal_example.yml @@ -1,5 +1,6 @@ --- -hypervisor: "none" +hypervisor: + type: "none" install_type: "physical" install_drive: "/dev/sda" @@ -8,7 +9,7 @@ filesystem: "btrfs" cis: false selinux: true -firewalld_enabled: true +firewall_enabled: true luks_enabled: true luks_passphrase: "1234" diff --git a/vars_example.yml b/vars_example.yml index 7f39e5b..20cf82a 100644 --- a/vars_example.yml +++ b/vars_example.yml @@ -1,24 +1,32 @@ --- -# Set vm_ip for static addressing. Remove vm_ip to use DHCP. -vm_ip: "{{ inventory_hostname }}" +# Set system.ip for static addressing. Remove system.ip to use DHCP. +system: + ip: "{{ inventory_hostname }}" install_type: "virtual" install_drive: "/dev/sda" # Use /dev/vda for virtio/libvirt. custom_iso: false # Set true to skip ArchISO-specific validation and pacman setup. cis: false # Set true to enable CIS hardening. selinux: true # Toggle SELinux where supported. -firewalld_enabled: true # Toggle firewalld package and service. +firewall_enabled: true # Toggle firewall package and service. -hypervisor_url: "pve01.example.com" -hypervisor_username: "root@pam" -hypervisor_password: "CHANGE_ME" -hypervisor_node: "pve01" -hypervisor_storage: "local-lvm" -hypervisor_datacenter: "dc01" -hypervisor_cluster: "cluster01" +hypervisor: + type: "proxmox" # libvirt|proxmox|vmware|xen|none + url: "pve01.example.com" + username: "root@pam" + password: "CHANGE_ME" + node: "pve01" + storage: "local-lvm" + datacenter: "dc01" + cluster: "cluster01" + validate_certs: false + +# Legacy (still supported): hypervisor_url, hypervisor_username, hypervisor_password, hypervisor_node, hypervisor_storage, +# hypervisor_datacenter, hypervisor_cluster, hypervisor_validate_certs. # VMware (only needed when hypervisor: vmware) -# vm_path: "/Folder" # Optional folder path segment in vCenter. +# system: +# path: "/Folder" # Optional folder path segment in vCenter. vmware_ssh: true # LUKS disk encryption (optional)