Compare commits

...

9 Commits

Author SHA1 Message Date
09b3ed44ba fix(bootstrap): RHEL 9 bootstrap from Arch ISO compatibility
- Generate resolv.conf from inventory DNS settings instead of copying
  host file (Arch ISO has systemd-resolved stub 127.0.0.53)
- Add XFS compat options for GRUB 2.06 and kernel 5.14 across LVM
  volumes, /boot partition, and data disks
- Mount API filesystems (proc, sys, dev) into chroot for RPM scriptlets
- Bypass GPG Sequoia validation with _pkgverify_level none
- Tolerate grub2-common scriptlet warnings
- Handle libvirt VM destroy gracefully during cleanup
2026-02-20 16:58:59 +01:00
603abe63cb refactor: make bootstrap host target configurable 2026-02-20 16:58:59 +01:00
1c0e6533ae fix(ubuntu): add initramfs-tools to debootstrap base packages 2026-02-20 16:58:59 +01:00
00aa614cfd fix(bootstrap): use explicit keyring for debootstrap and copy resolv.conf 2026-02-20 16:58:59 +01:00
4905d10bc0 fix(cloud-init): handle boolean sudo values in user-data template 2026-02-20 16:58:59 +01:00
b4e8ccb77f fix: re-gather facts after reboot to detect target OS package manager
The live ISO (Arch) caches ansible_pkg_mgr=pacman. After rebooting
into the target OS (e.g. Debian), package module fails because pacman
is not available. Re-gather minimal facts including pkg_mgr.
2026-02-20 16:58:59 +01:00
2a82ee4d5c fix: resolve Jinja2 .keys ambiguity, fastfetch availability, and python interpreter
- Use bracket notation item['keys'] instead of item.keys to avoid
  conflict with Python dict .keys() method
- Remove fastfetch from Debian 12 package list (only available in 13+)
- Set explicit python interpreter path for post-reboot tasks
2026-02-20 16:58:58 +01:00
7b213e7456 fix(partitioning): create separate /boot for LVM-based filesystems
VMware EFI firmware may not initialize all SCSI devices before GRUB
runs, preventing LVM assembly when the root LV spans multiple disks.
A separate /boot partition (the standard RHEL Anaconda layout) lets
GRUB load kernels without LVM; the kernel initramfs handles LVM
activation with proper device waiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 04:50:32 +01:00
cfc261878a fix(bootloader): run efibootmgr on host for universal chroot compatibility
The previous approach ran efibootmgr inside the chroot, which only works
with arch-chroot (auto-mounts efivars) but fails silently with
systemd-nspawn or plain chroot. Move EFI boot entry creation to the host
where efivars is always available.

Also fixes wrong EFI loader path (\efi\EFI\... -> \EFI\...) and uses
the correct vendor label (e.g. "redhat" instead of raw os variable).

For non-RHEL distros, grub-install now uses --no-nvram to avoid
redundant NVRAM writes; the host efibootmgr handles entry creation
for all distros uniformly with idempotent pre-check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 03:36:20 +01:00
15 changed files with 112 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
---
- name: Create and configure VMs
hosts: all
hosts: "{{ bootstrap_target | default('all') }}"
strategy: free # noqa: run-once[play]
gather_facts: false
become: true
@@ -152,6 +152,16 @@
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter: /usr/bin/python3
- name: Re-gather facts for target OS after reboot
when:
- post_reboot_can_connect | bool
ansible.builtin.setup:
gather_subset:
- "!all"
- min
- pkg_mgr
- name: Install post-reboot packages
when:

View File

@@ -1,4 +1,28 @@
---
- name: Create API filesystem mountpoints in installroot
when: is_rhel | bool
ansible.builtin.file:
path: "/mnt/{{ item }}"
state: directory
mode: "0755"
loop:
- dev
- proc
- sys
- name: Mount API filesystems into installroot
when: is_rhel | bool
ansible.posix.mount:
src: "{{ item.src }}"
path: "/mnt/{{ item.path }}"
fstype: "{{ item.fstype }}"
opts: "{{ item.opts | default(omit) }}"
state: ephemeral
loop:
- { src: proc, path: proc, fstype: proc }
- { src: sysfs, path: sys, fstype: sysfs }
- { src: /dev, path: dev, fstype: none, opts: bind }
- name: Run OS-specific bootstrap process
vars:
bootstrap_os_task_map:

View File

@@ -9,12 +9,21 @@
groupinstall -y core base standard
register: bootstrap_result
changed_when: bootstrap_result.rc == 0
failed_when:
- bootstrap_result.rc != 0
- "'grub2-common' not in (bootstrap_result.stderr | default(''))"
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
- name: Write resolv.conf into chroot
ansible.builtin.copy:
dest: /mnt/etc/resolv.conf
state: link
mode: "0644"
content: |
{% for dns in system_cfg.network.dns.servers %}
nameserver {{ dns }}
{% endfor %}
{% if system_cfg.network.dns.search | default([]) | length > 0 %}
search {{ system_cfg.network.dns.search | join(' ') }}
{% endif %}
- name: Ensure chroot RHEL DVD directory exists
ansible.builtin.file:

View File

@@ -39,17 +39,20 @@
- name: Install Ubuntu base system
ansible.builtin.command: >-
debootstrap --include={{ bootstrap_ubuntu_base_csv }}
debootstrap
--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg
--include={{ bootstrap_ubuntu_base_csv }}
{{ 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
ansible.builtin.copy:
src: /etc/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
remote_src: true
mode: "0644"
- name: Enable universe repository
ansible.builtin.command: "{{ chroot_command }} sed -i '1s|$| universe|' /etc/apt/sources.list"

View File

@@ -104,7 +104,7 @@ bootstrap_debian_extra_common:
bootstrap_debian_extra_versioned:
- linux-image-amd64
- "{{ 'duf' if (os_version | string) not in ['10', '11'] else '' }}"
- "{{ 'fastfetch' if (os_version | string) in ['12', '13', 'unstable'] else '' }}"
- "{{ 'fastfetch' if (os_version | string) in ['13', 'unstable'] else '' }}"
- "{{ 'neofetch' if (os_version | string) == '12' else '' }}"
- "{{ 'software-properties-common' if (os_version | string) not in ['13', 'unstable'] else '' }}"
- "{{ 'systemd-zram-generator' if (os_version | string) not in ['10', '11'] else '' }}"
@@ -121,6 +121,7 @@ bootstrap_debian:
bootstrap_ubuntu:
base:
- initramfs-tools
- linux-image-generic
extra: >-
{{

View File

@@ -92,6 +92,7 @@
community.libvirt.virt:
name: "{{ hostname }}"
state: destroyed
failed_when: false
- name: Start the VM
community.libvirt.virt:

View File

@@ -1,27 +1,40 @@
---
- name: Configure Bootloader
vars:
_efi_vendor: >-
{{
"redhat" if os == "rhel"
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
}}
_efi_loader: >-
{{ "shimx64.efi" if is_rhel | bool else "grubx64.efi" }}
block:
- name: Install Bootloader
vars:
configuration_use_efibootmgr: "{{ is_rhel | bool }}"
configuration_efi_dir: "{{ partitioning_efi_mountpoint }}"
configuration_bootloader_id: >-
{{ "ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os }}
configuration_efi_vendor: >-
{{ "redhat" if os == "rhel" else os }}
configuration_efibootmgr_cmd: >-
/usr/sbin/efibootmgr -c -L '{{ os }}' -d "{{ install_drive }}" -p 1
-l '\efi\EFI\{{ configuration_efi_vendor }}\shimx64.efi'
configuration_grub_cmd: >-
/usr/sbin/grub-install --target=x86_64-efi
--efi-directory={{ configuration_efi_dir }}
--bootloader-id={{ configuration_bootloader_id }}
configuration_bootloader_cmd: >-
{{ configuration_efibootmgr_cmd if configuration_use_efibootmgr else configuration_grub_cmd }}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_bootloader_cmd }}"
- name: Install GRUB EFI binary
when: not (is_rhel | bool)
ansible.builtin.command: >-
{{ chroot_command }} /usr/sbin/grub-install --target=x86_64-efi
--efi-directory={{ partitioning_efi_mountpoint }}
--bootloader-id={{ _efi_vendor }}
--no-nvram
register: configuration_bootloader_result
changed_when: configuration_bootloader_result.rc == 0
- name: Check existing EFI boot entries
ansible.builtin.command: efibootmgr
register: _efi_entries
changed_when: false
- name: Ensure EFI boot entry exists
when: ('* ' + _efi_vendor) not in _efi_entries.stdout
ansible.builtin.command: >-
efibootmgr -c
-L '{{ _efi_vendor }}'
-d '{{ install_drive }}'
-p 1
-l '\EFI\{{ _efi_vendor }}\{{ _efi_loader }}'
register: _efi_entry_result
changed_when: _efi_entry_result.rc == 0
- name: Ensure lvm2 for non btrfs filesystems
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
ansible.builtin.lineinfile:
@@ -50,13 +63,11 @@
- name: Generate grub config
vars:
configuration_efi_vendor: >-
{{ "redhat" if os == "rhel" else os }}
configuration_grub_cfg_cmd: >-
{{
'/usr/sbin/grub2-mkconfig -o '
+ partitioning_efi_mountpoint
+ '/EFI/' + configuration_efi_vendor + '/grub.cfg'
+ '/EFI/' + _efi_vendor + '/grub.cfg'
if is_rhel | bool
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
}}

View File

@@ -26,7 +26,7 @@
changed_when: configuration_user_result.rc == 0
- name: Ensure .ssh directory exists
when: item.keys | default([]) | length > 0
when: item['keys'] | default([]) | length > 0
ansible.builtin.file:
path: "/mnt/home/{{ item.name }}/.ssh"
state: directory

View File

@@ -205,6 +205,13 @@
opts: "ro,loop"
state: mounted
- name: Relax RPM Sequoia signature policy for RHEL bootstrap
when: is_rhel | bool
ansible.builtin.copy:
dest: /etc/rpm/macros
content: "%_pkgverify_level none\n"
mode: "0644"
- name: Configure RHEL Repos for installation
when: is_rhel | bool
block:

View File

@@ -30,7 +30,7 @@
that:
- item is mapping
- item.name is defined and (item.name | string | length) > 0
- item.keys is not defined or (item.keys is iterable and item.keys is not string)
- item['keys'] is not defined or (item['keys'] is iterable and item['keys'] is not string)
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
quiet: true
loop: "{{ system.users }}"

View File

@@ -9,7 +9,10 @@ partitioning_boot_size_mib: 1024
partitioning_use_full_disk: true
partitioning_separate_boot: >-
{{
(system_cfg.luks.enabled | bool)
(
(system_cfg.luks.enabled | bool)
or (system_cfg.filesystem != 'btrfs')
)
and (os not in ['archlinux'])
}}
partitioning_boot_fs_fstype: >-

View File

@@ -51,10 +51,14 @@
- name: Create filesystems on additional disks
when: partitioning_extra_disks | length > 0
vars:
_label_opt: "{{ ('-L ' ~ item.mount.label) if (item.mount.label | default('') | string | length) > 0 else '' }}"
_compat_opt: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if (is_rhel | bool and item.mount.fstype == 'xfs') else '' }}"
_all_opts: "{{ ([_label_opt, _compat_opt] | select | join(' ')) or omit }}"
community.general.filesystem:
dev: "{{ item.partition }}"
fstype: "{{ item.mount.fstype }}"
opts: "{{ ('-L ' ~ item.mount.label) if (item.mount.label | default('') | string | length) > 0 else omit }}"
opts: "{{ _all_opts }}"
force: true
loop: "{{ partitioning_extra_disks }}"
loop_control:

View File

@@ -418,6 +418,7 @@
community.general.filesystem:
dev: "{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}"
fstype: "{{ partitioning_boot_fs_fstype }}"
opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if (is_rhel | bool and partitioning_boot_fs_fstype == 'xfs') else omit }}"
force: true
- name: Remove unsupported ext4 features from /boot

View File

@@ -4,6 +4,7 @@
community.general.filesystem:
dev: /dev/sys/{{ item.lv }}
fstype: xfs
opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if is_rhel | bool else omit }}"
force: true
loop:
- { lv: root }

View File

@@ -8,10 +8,10 @@ users:
- name: "{{ user.name }}"
primary_group: "{{ user.name }}"
groups: users
sudo: "{{ user.sudo | default('ALL=(ALL) NOPASSWD:ALL') }}"
sudo: "{{ 'ALL=(ALL) NOPASSWD:ALL' if (user.sudo is defined and user.sudo is sameas true) else user.sudo | default('ALL=(ALL) NOPASSWD:ALL') }}"
passwd: "{{ user.password | password_hash('sha512') }}"
lock_passwd: false
{% set ssh_keys = user.keys | default([]) %}
{% set ssh_keys = user['keys'] | default([]) %}
{% if ssh_keys | length > 0 %}
ssh_authorized_keys:
{% for key in ssh_keys %}