commit 1444f05d9c376f4cb85ee1eeccebc17092fd0560 Author: Mark Riedesel Date: Wed Nov 20 09:12:19 2024 -0600 arch installation mostly works with zfsbootmgr diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..347aadf --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory=inventory +hostfile=inventory +roles_path=roles +retry_files_enabled = False + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=10s diff --git a/inventory/host_vars/vmguest.yaml b/inventory/host_vars/vmguest.yaml new file mode 100644 index 0000000..2542c8f --- /dev/null +++ b/inventory/host_vars/vmguest.yaml @@ -0,0 +1,10 @@ +--- +ansible_user: root +ansible_host: 10.4.0.53 + +base_root_disks: + - /dev/disk/by-id/virtio-fingletoobp + - /dev/disk/by-id/virtio-portshartle +base_root_efi_mib: 512 +base_root_swap_mib: 256 +base_root_free_percent: 0 diff --git a/inventory/hosts b/inventory/hosts new file mode 100644 index 0000000..40c46e2 --- /dev/null +++ b/inventory/hosts @@ -0,0 +1,2 @@ +[desktop] +vmguest diff --git a/local.yaml b/local.yaml new file mode 100644 index 0000000..43d1fea --- /dev/null +++ b/local.yaml @@ -0,0 +1,2 @@ +--- +- hosts: localhost diff --git a/playbooks/base-partition.yaml b/playbooks/base-partition.yaml new file mode 100644 index 0000000..1c0820e --- /dev/null +++ b/playbooks/base-partition.yaml @@ -0,0 +1,4 @@ +- name: Partition base filesystem + shell: echo poop + when: base_partition_devices is defined + diff --git a/playbooks/info.yaml b/playbooks/info.yaml new file mode 100644 index 0000000..35a2d99 --- /dev/null +++ b/playbooks/info.yaml @@ -0,0 +1,9 @@ +- hosts: all + become: true + tasks: + - name: is this a livecd? + debug: + msg: "{{ ansible_nodename }} {{ ansible_nodename == 'archiso' }}" + + + diff --git a/playbooks/install.yaml b/playbooks/install.yaml new file mode 100644 index 0000000..82af648 --- /dev/null +++ b/playbooks/install.yaml @@ -0,0 +1,6 @@ +--- +- name: Provision new workstation from scratch + tags: base + hosts: all + roles: + - base diff --git a/roles/base/defaults/main.yaml b/roles/base/defaults/main.yaml new file mode 100644 index 0000000..e69de29 diff --git a/roles/base/handlers/main.yaml b/roles/base/handlers/main.yaml new file mode 100644 index 0000000..e69de29 diff --git a/roles/base/tasks/archinstall/initialize_efi.yaml b/roles/base/tasks/archinstall/initialize_efi.yaml new file mode 100644 index 0000000..9f4e836 --- /dev/null +++ b/roles/base/tasks/archinstall/initialize_efi.yaml @@ -0,0 +1,11 @@ +--- +- name: Inspect EFI boot partitions + become: true + command: "blkid {{ item }}" + loop: "{{ base_efi_partitions | default([]) }}" + register: blkid + +- name: Format EFI partitions fat32 + command: "mkfs.vfat -v -F 32 -n EFI {{ item.item }}" + loop: "{{ blkid.results | rejectattr('stdout', 'contains', 'TYPE=\"vfat\"') }}" + register: mkfs_vfat_efi diff --git a/roles/base/tasks/archinstall/initialize_root_zvol.yaml b/roles/base/tasks/archinstall/initialize_root_zvol.yaml new file mode 100644 index 0000000..1081de1 --- /dev/null +++ b/roles/base/tasks/archinstall/initialize_root_zvol.yaml @@ -0,0 +1,67 @@ +--- +- name: Check for existing zroot zfs volume + command: zpool list -Ho name zroot + register: zroot_check + ignore_errors: true + +- block: + - name: Initialize disk device to id table + set_fact: + partition_device_to_ids: {} + + - name: Create disk device to id table + set_fact: + partition_device_to_ids: >- + {{ + partition_device_to_ids + | combine({ item.value: (partition_device_to_ids[item.value] | default([])) + [item.key] }) + }} + with_items: "{{ base_partitions_by_id | dict2items }}" + + - name: Create zroot volume + become: true + command: >- + zpool create -f -o ashift=12 + -o autotrim=on + -O devices=off + -O relatime=on + -O xattr=sa + -O acltype=posixacl + -O normalization=formD + -O compression=lz4 + -O canmount=off + -O mountpoint=none + -R /mnt + zroot {{ mirror }} {{ base_root_partitions | map('extract', partition_device_to_ids) | flatten | list | join(' ') }} + vars: + mirror: "{{ 'mirror' if base_root_partitions | length > 1 else '' }}" + when: zroot_check.rc != 0 + +- name: Create zroot/ROOT and zroot/DATA volumes + community.general.zfs: + name: "zroot/{{ item }}" + state: present + register: zfs_zroot_root_volume + with_items: [ROOT, DATA] + +- name: Create zroot/ROOT/arch + community.general.zfs: + name: zroot/ROOT/arch + state: present + extra_zfs_properties: + canmount: noauto + mountpoint: / + when: zfs_zroot_root_volume.changed + +- name: Create zroot/home + community.general.zfs: + name: zroot/DATA/home + state: present + extra_zfs_properties: + mountpoint: /home + +- name: Export zroot pool + command: zpool export zroot + +- name: Import zroot pool (-R /mnt) + command: zpool import -R /mnt zroot -N diff --git a/roles/base/tasks/archinstall/initialize_swap.yaml b/roles/base/tasks/archinstall/initialize_swap.yaml new file mode 100644 index 0000000..83a0a72 --- /dev/null +++ b/roles/base/tasks/archinstall/initialize_swap.yaml @@ -0,0 +1,12 @@ +--- +- name: Initialize swap space + become: true + command: "blkid {{item}}" + loop: "{{ base_swap_partitions | default([]) }}" + register: blkid + +- name: Swap devices without swap filesystems present + become: true + command: "mkswap --verbose {{item.item}}" + loop: "{{ blkid.results | rejectattr('stdout', 'contains', 'TYPE=\"swap\"') }}" + register: mkswap diff --git a/roles/base/tasks/archinstall/install_os.yaml b/roles/base/tasks/archinstall/install_os.yaml new file mode 100644 index 0000000..16a542b --- /dev/null +++ b/roles/base/tasks/archinstall/install_os.yaml @@ -0,0 +1,80 @@ +--- +- name: archinstall | install os | check for presence of previously pacstrapped /mnt + stat: + path: /mnt/usr/lib + register: existing_pacstrap + +- debug: + var: existing_pacstrap + +- name: archinstall | install os | pacstrap + shell: "pacstrap /mnt {{ packages | join(' ') }}" + vars: + packages: + - base + - base-devel + - efibootmgr + - git + - iwd + - linux-firmware + - linux-lts + - linux-lts-headers + - openssh + when: not existing_pacstrap.stat.exists + +- name: archinstall | install os | copy pacman mirrorlist + copy: + remote_src: true + src: /etc/pacman.d/mirrorlist + dest: /mnt/etc/pacman.d/mirrorlist + +- name: archinstall | install os | propagate root authorized keys + copy: + remote_src: true + src: /root/.ssh/authorized_keys + dest: /mnt/root/.ssh/authorized_keys + +- name: archinstall | install os | passwordless sudo for group wheel + copy: + content: "%wheel ALL=(ALL) NOPASSWD: ALL" + dest: /mnt/etc/sudoers.d/wheel-group-nopasswd + +- name: archinstall | install os | set timezone + file: + src: /usr/share/zoneinfo/US/Central + dest: /mnt/etc/localtime + state: link + +- name: archinstall | install os | enable en_US locales + command: sed -i 's/^#en_US/en_US/' /mnt/etc/locale.gen + +- name: archinstall | install os | generate locales + command: arch-chroot /mnt locale-gen + +- name: archinstall | install os | generate template for arch-chroot installation + template: + src: arch_chroot_install.sh + dest: /mnt/arch_chroot_install.sh + mode: "0755" + +- name: archinstall | install os | set hostname + copy: + dest: /mnt/etc/hostname + content: | + {{ inventory_hostname }} + +- name: archinstall | install os | run installation script in arch-chroot + command: arch-chroot /mnt /arch_chroot_install.sh + register: chroot + +- name: archinstall | install os | arch-chroot install output + debug: + msg: "{{ chroot.stdout_lines }}" + +- name: archinstall | install os | remove arch-chroot installation script + file: + path: /mnt/arch_chroot_install.sh + state: absent + +# TODO +# - name: archinstall | install os | create fstab diff --git a/roles/base/tasks/archinstall/mirrorlist.yaml b/roles/base/tasks/archinstall/mirrorlist.yaml new file mode 100644 index 0000000..08af6ee --- /dev/null +++ b/roles/base/tasks/archinstall/mirrorlist.yaml @@ -0,0 +1,4 @@ +--- +- name: Select fastest Arch repository mirrors + command: + cmd: reflector --country US --latest 5 --sort rate --save /etc/pacman.d/mirrorlist diff --git a/roles/base/tasks/archinstall/partition.yaml b/roles/base/tasks/archinstall/partition.yaml new file mode 100644 index 0000000..a008aa8 --- /dev/null +++ b/roles/base/tasks/archinstall/partition.yaml @@ -0,0 +1,138 @@ +--- +- name: Get details about rootfs disks + community.general.parted: + device: "{{ item }}" + unit: MiB + register: base_root_disks_info + loop: "{{ base_root_disks | list }}" + +- name: Calculate maximum usable disk space + ansible.builtin.set_fact: + base_root_usable_mib: "{{ (base_root_disks_info.results | map(attribute='disk.size') | min | int) - 1 }}" + +- debug: var=base_root_usable_mib + +- name: Calculate disk utilization percentage + ansible.builtin.set_fact: + base_root_usable_mib: "{{ base_root_usable_mib|int - ((base_root_usable_mib|float) * (0.01 * (base_root_free_percent|float))) | round(method='floor') | int }}" + +- name: Calculate zfs volume size + set_fact: + base_root_zpool_mib: "{{ base_root_usable_mib|int - fixed_size_partitions|int }}" + vars: + fixed_size_partitions: "{{ base_root_swap_mib|int + base_root_efi_mib|int }}" +# +- name: Calculate partition layouts + set_fact: + partition_ranges: >- + {{ + (partition_ranges|d([])) + [{ + 'begin': base_root_partitions[:(loop_index|int)] | map(attribute='size_mib') | map('int') | sum(), + 'end': base_root_partitions[:(loop_index|int+1)] | map(attribute='size_mib') | map('int') | sum(), + }] + }} + vars: + loop_index: "{{ item }}" + base_root_partitions: + - name: efi + size_mib: "{{ base_root_efi_mib }}" + - name: root + size_mib: "{{ base_root_zpool_mib }}" + - name: swap + size_mib: "{{ base_root_swap_mib }}" + with_sequence: start=0 end="{{ base_root_partitions | length - 1}}" + +- debug: + var: partition_ranges + +- name: Initialize partition facts + set_fact: + base_efi_partitions: [] + base_root_partitions: [] + base_swap_partitions: [] + +- name: Create EFI partition + become: true + vars: + part_index: 0 + parted: + label: gpt + unit: MiB + name: EFI Boot + device: "{{ item.disk.dev }}" + number: "{{ part_index + 1 }}" + flags: [ boot, esp ] + part_start: "{{ [partition_ranges[part_index].begin, 1]|max }}MiB" + part_end: "{{ partition_ranges[part_index].end }}MiB" + state: present + fs_type: fat32 + loop: "{{ base_root_disks_info.results }}" + register: parted_create + +- name: Store EFI partition details + set_fact: + base_efi_partitions: "{{ parted_create.results | map(attribute='disk.dev') | product([1]) | map('join') | list }}" + +- name: Create root zvol partition + become: true + vars: + part_index: 1 + parted: + label: gpt + unit: MiB + name: ArchLinux ZFS Root + device: "{{ item.disk.dev }}" + number: "{{ part_index + 1 }}" + part_start: "{{ partition_ranges[part_index].begin }}MiB" + part_end: "{{ partition_ranges[part_index].end }}MiB" + state: present + loop: "{{ parted_create.results }}" + register: parted_create + +- name: Store root zvol partition details + set_fact: + base_root_partitions: "{{ parted_create.results | map(attribute='disk.dev') | product([2]) | map('join') | list }}" + +- name: Create swap partition + become: true + vars: + part_index: 2 + parted: + label: gpt + unit: MiB + device: "{{ item.disk.dev }}" + number: "{{ part_index + 1 }}" + part_start: "{{ partition_ranges[part_index].begin }}MiB" + part_end: "{{ partition_ranges[part_index].end }}MiB" + state: present + fs_type: linux-swap + loop: "{{ parted_create.results }}" + register: parted_create + when: base_root_swap_mib is defined and base_root_swap_mib > 0 + +- name: Store swap partition details + set_fact: + base_swap_partitions: "{{ parted_create.results | map(attribute='disk.dev') | product([3]) | map('join') | list }}" + when: base_root_swap_mib is defined and base_root_swap_mib > 0 + +- name: Analyze resulting partition layouts + parted: + unit: MiB + device: "{{ item }}" + register: base_root_disks_info + loop: "{{ base_root_disks | list }}" + +- name: Collect disk device identifiers + shell: "for x in /dev/disk/by-id/*; do echo $x $(realpath $x); done" + register: disk_realpaths + +- name: Collect disk device identifiers into a base_partitions_by_id dictionary + set_fact: + base_partitions_by_id: >- + {{ + dict( + disk_realpaths.stdout_lines + | map('split', ' ') + | map('list') + ) + }} diff --git a/roles/base/tasks/archinstall/postinstall_snapshot.yaml b/roles/base/tasks/archinstall/postinstall_snapshot.yaml new file mode 100644 index 0000000..d25cb28 --- /dev/null +++ b/roles/base/tasks/archinstall/postinstall_snapshot.yaml @@ -0,0 +1,11 @@ +- name: archinstall | re-create post-installation snapshot of zroot/ROOT/arch + community.general.zfs: + name: zroot/ROOT/arch@post-installation + state: "{{ item }}" + with_items: [absent, present] + +- name: archinstall | re-create post-installation snapshot of zroot/DATA/home + community.general.zfs: + name: zroot/DATA/home@post-installation + state: "{{ item }}" + with_items: [absent, present] diff --git a/roles/base/tasks/archinstall/prepare_chroot.yaml b/roles/base/tasks/archinstall/prepare_chroot.yaml new file mode 100644 index 0000000..7e971be --- /dev/null +++ b/roles/base/tasks/archinstall/prepare_chroot.yaml @@ -0,0 +1,30 @@ +--- +- name: Mount arch zroot + command: zfs mount zroot/ROOT/arch + +- name: Mount all other zroot mountpoints + command: zfs mount -a + +- name: Create zroot destination directories + ansible.builtin.file: + path: "/mnt{{ item }}" + state: directory + loop: + - /etc/zfs + - /boot/efi + +- name: Mount EFI + ansible.posix.mount: + path: /mnt/boot/efi + src: "{{ base_efi_partitions | first }}" + fstype: vfat + state: mounted + +- name: zfs | set zroot bootfs to arch + command: zpool set bootfs=zroot/ROOT/arch zroot + +- name: zfs | set cachefile + command: zpool set cachefile=/etc/zfs/zpool.cache zroot + +- name: zfs | copy cache file to chroot + command: cp /etc/zfs/zpool.cache /mnt/etc/zfs diff --git a/roles/base/tasks/main.yaml b/roles/base/tasks/main.yaml new file mode 100644 index 0000000..c659a6c --- /dev/null +++ b/roles/base/tasks/main.yaml @@ -0,0 +1,12 @@ +--- +- block: + # - import_tasks: archinstall/mirrorlist.yaml + - import_tasks: archinstall/partition.yaml + - import_tasks: archinstall/initialize_root_zvol.yaml + - import_tasks: archinstall/initialize_swap.yaml + - import_tasks: archinstall/initialize_efi.yaml + - import_tasks: archinstall/prepare_chroot.yaml + - import_tasks: archinstall/install_os.yaml + - import_tasks: archinstall/postinstall_snapshot.yaml + when: base_root_disks is defined and ansible_hostname == 'archiso' + diff --git a/roles/base/templates/arch_chroot_install.sh b/roles/base/templates/arch_chroot_install.sh new file mode 100644 index 0000000..6fcbc57 --- /dev/null +++ b/roles/base/templates/arch_chroot_install.sh @@ -0,0 +1,89 @@ +#!/bin/bash +set -e + +function add_admin_user { + username="$1" + id -u $username || useradd -m -G wheel $username + id $username +} + +function install_paru_as { + username="$1" + if ! command -v paru 2>&1 >/dev/null; then + sudo -u $username bash -c "cd ~; [ -d paru-bin ] || (git clone https://aur.archlinux.org/paru-bin.git && cd paru-bin && makepkg -si --noconfirm && paru -Sy)" + sudo -u $username bash -c "rm -rf ~/paru-bin ~/.cache" + fi +} + +function install_archzfs_pacman_repository { + if grep -q "[archzfs]" /etc/pacman.conf; then + echo "need to install archzfs pacman" + fi +} + +function install_zfs_packages_as { + username="$1" + if ! pacman -Q zfs-dkms; then + sudo -u $username -i bash -c "paru -Sy zfs-dkms --noconfirm" + fi +} + +function configure_hostid { + [ -f /etc/hostid ] || zgenhostid -f -o /etc/hostid + hostid +} + +function install_zfsbootmenu { + command -v wget || pacman -S --noconfirm wget + if [ ! -f /boot/efi/EFI/zbm/zfsbootmenu.EFI ]; then + mkdir -p /boot/efi/EFI/zbm/ + wget https://get.zfsbootmenu.org/latest.EFI -O /boot/efi/EFI/zbm/zfsbootmenu.EFI + efibootmgr --disk {{ base_root_disks | first }} --part 1 --create \ + --label "ZFSBootMenu" \ + --loader '\EFI\zbm\zfsbootmenu.EFI' \ + --unicode "spl_hostid=$(hostid) zbm.timeout=3 zbm.prefer=zroot zbm.import_policy=hostid" \ + --verbose + else + echo "skipping zfsbootmenu installation" + fi + + zfs set org.zfsbootmenu:commandline="noresume init_on_alloc=0 rw spl.spl_hostid=$(hostid)" zroot/ROOT +} + +function configure_zfs_initrd { + # Add 'zfs' to HOOKS=() before 'filesystem' + sed -i '/^HOOKS=(/!b;/zfs/!s/filesystem/zfs filesystem/' /etc/mkinitcpio.conf + # Add 'zfs' to MODULES=() + sed -i '/^MODULES=/!b;/zfs/!s/)/zfs)/' /etc/mkinitcpio.conf + initrd=/boot/initramfs-linux-lts.img + [[ ! -e $initrd || $initrd -ot /etc/mkinitcpio.conf ]] || mkinitcpio -P + # ^ only rebuild if initrd image is older than mkinitpio.conf +} + +function configure_dhcp_ethernet { + cat <<- EOF > /etc/systemd/network/20-wired-dhcp.network + [Match] + Name=en* + + [Link] + RequiredForOnline=routable + + [Network] + DHCP=yes + EOF + systemctl enable systemd-networkd.service +} + +function enable_systemd_services { + systemctl enable sshd.service +} + +# do the stuff +add_admin_user mark +install_paru_as mark +install_zfs_packages_as mark +configure_hostid +install_zfsbootmenu +configure_zfs_initrd +configure_dhcp_ethernet +enable_systemd_services