2023-02-19 20:21:20 +01:00
#!/bin/bash
2023-02-19 21:55:47 +01:00
declare this_script_url
this_script_url = " ${ SCRIPT_URL : ? } "
declare zpool_name zfs_arch_dataset_name
2023-02-19 20:21:20 +01:00
zpool_name = 'zpool'
zfs_arch_dataset_name = 'archlinux'
function set_ntp ( ) {
timedatectl set-ntp true
}
function we_are_changerooted ( ) {
if [ " $( stat -c '%d:%i' '/' ) " != " $( stat -c '%d:%i' '/proc/1/root/.' ) " ] ; then
return 0
else
return 1
fi
}
2023-02-19 20:38:42 +01:00
function update_pacman_db ( ) {
2023-02-20 00:39:31 +01:00
printf -- '%s\n' 'Refreshing mirror list ...'
2023-02-19 21:13:11 +01:00
systemctl start reflector
2023-02-19 20:46:51 +01:00
# In an ISO and for the minimal number of packages we need we do not
# care about partial upgrades
2023-02-19 20:38:42 +01:00
pacman -Sy
}
2023-02-19 20:21:20 +01:00
function install_pkgs ( ) {
printf -- '%s\n' 'Installing packages ...'
pacman -S --needed --noconfirm " ${ @ } "
}
function install_zfs ( ) {
2023-02-19 20:52:20 +01:00
declare reset_colors = '\033[0m'
2023-02-19 20:21:20 +01:00
curl -s 'https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init' | bash
2023-02-19 21:01:36 +01:00
printf -- " ${ reset_colors } "
2023-02-19 20:21:20 +01:00
}
function get_partitions ( ) {
declare partitions_json
partitions_json = " $( lsblk --output PATH,PARTTYPE --json) " || return 1
printf -- '%s' " ${ partitions_json } "
return 0
}
function get_parts ( ) {
declare parttype parts
parttype = " ${ 1 : ? } "
case " ${ parttype } " in
zfs)
parts = " $( get_partitions | jq --raw-output '.[][] | select(.parttype=="6a85cf4d-1dd2-11b2-99a6-080020736631") | .path' ) " || exit 1
; ;
efi)
parts = " $( get_partitions | jq --raw-output '.[][] | select(.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b") | .path' ) " || exit 1
; ;
*)
printf -- '%s\n' 'Unknown partition type ' "'" '"${parttype}"' "'" ', exiting 1 ...'
exit 1
; ;
esac
printf -- '%s' " ${ parts } "
return 0
}
function we_have_exactly_one_part ( ) {
2023-02-21 02:56:44 +01:00
local parttype parts_list parts_count
2023-02-19 20:21:20 +01:00
parttype = " ${ 1 : ? } "
parts_list = " ${ 2 : ? } "
parts_count = " $( wc -l <<< " ${ parts_list } " ) "
if [ [ " $( wc -c <<< " ${ parts_list } " ) " -gt '1' ] ] ; then
case " ${ parts_count } " in
0)
2023-02-21 02:57:13 +01:00
printf -- '%s\n' 'No ' " ${ parttype ^^ } " ' partition found. Exiting 1 ...'
2023-02-19 20:21:20 +01:00
exit 1
; ;
1)
return 0
; ;
*)
printf -- '%s\n' 'Multiple ' " ${ parttype } " ' partitions found. We expect exactly one. Cowardly exiting 1 ...'
exit 1
; ;
esac
printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting 1 ...'
exit 1
fi
}
function no_zpool_exists ( ) {
declare zpool_list
zpool_list = " $( zpool list -H) "
2023-02-19 21:00:03 +01:00
[ [ " $( wc -l <<< " ${ zpool_list } " ) " -le '1' ] ] && [ [ " $( wc -c <<< " ${ zpool_list } " ) " -le '1' ] ] && return 0
2023-02-19 20:21:20 +01:00
return 1
}
function zpool_drive_id ( ) {
declare drive_id
drive_id = " $( find -L /dev/disk/by-id -samefile " ${ 1 : ? } " ) "
if [ [ " $( wc -l <<< " ${ drive_id } " ) " -eq '1' ] ] ; then
printf -- '%s' " ${ drive_id } "
return 0
fi
printf -- '%s\n' 'More than zpool partition entry in /dev/disk/by-id, exiting 1 ...'
exit 1
}
function set_zpool_password ( ) {
2023-02-21 00:10:03 +01:00
# May or may not have a newline at the end, ZFS doesn't care
2023-02-20 21:26:59 +01:00
printf -- '%s' 'password' > '/etc/zfs/' " ${ zpool_name } " '.key'
2023-02-20 21:27:27 +01:00
chmod '600' '/etc/zfs/' " ${ zpool_name } " '.key'
2023-02-19 20:21:20 +01:00
}
function import_pool ( ) {
zpool import -d '/dev/disk/by-id' -R '/mnt' " ${ zpool_name } " -N -f
zfs load-key " ${ zpool_name } "
}
function create_pool ( ) {
# Create a temporary pool that is not cached
zpool create -f \
-o 'ashift=12' \
-o 'autotrim=on' \
-O 'acltype=posix' \
-O 'compression=on' \
-O 'relatime=on' \
-O 'xattr=sa' \
-O 'encryption=on' \
-O 'keyformat=passphrase' \
-O 'keylocation=file:///etc/zfs/' " ${ zpool_name } " '.key' \
-O 'normalization=formD' \
-O 'mountpoint=none' \
-O 'canmount=off' \
-O 'devices=off' \
-R '/mnt' \
" ${ zpool_name } " " ${ 1 : ? } "
}
function create_root_dataset ( ) {
zfs create -o mountpoint = none " ${ zpool_name } " '/root'
# zfs set org.zfsbootmenu:commandline="ro quiet" "${zpool_name}"'/root'
zfs set org.zfsbootmenu:commandline= "ro" " ${ zpool_name } " '/root'
}
function create_system_dataset ( ) {
zfs create -o 'mountpoint=/' -o 'canmount=noauto' " ${ zpool_name } " '/root/' " ${ zfs_arch_dataset_name } "
zgenhostid
zpool set bootfs = " ${ zpool_name } " '/root/' " ${ zfs_arch_dataset_name } " " ${ zpool_name } "
zfs mount " ${ zpool_name } " '/root/' " ${ zfs_arch_dataset_name } "
}
function create_home_dataset ( ) {
zfs create -o 'mountpoint=/' -o 'canmount=off' " ${ zpool_name } " '/data'
zfs create " ${ zpool_name } " '/data/home'
}
function export_pool ( ) {
zpool export " ${ zpool_name } "
}
function setup_zpool ( ) {
declare zpool_drive drive_by_id
zpool_drive = " ${ 1 : ? } "
drive_by_id = " $( zpool_drive_id " ${ zpool_drive } " ) "
set_zpool_password
if no_zpool_exists; then
create_pool " ${ drive_by_id } "
create_root_dataset
create_system_dataset
create_home_dataset
export_pool
import_pool
2023-02-19 21:00:03 +01:00
else
printf -- '%s\n' 'A zpool already exists, that is unexpected. Cowardly exiting 1 ...'
exit 1
2023-02-19 20:21:20 +01:00
fi
}
function mount_system ( ) {
zfs mount " ${ zpool_name } " '/root/' " ${ zfs_arch_dataset_name } "
zfs mount -a
declare efi_part
efi_part = " $( get_parts 'efi' ) "
if we_have_exactly_one_part 'efi' " ${ efi_part } " ; then
mkdir -p '/mnt/efi'
mount " ${ efi_part } " '/mnt/efi'
fi
}
function copy_zpool_cache ( ) {
mkdir -p '/mnt/etc/zfs'
zpool set 'cachefile=/etc/zfs/' " ${ zpool_name } " '.cache' " ${ zpool_name } "
}
function install_archlinux ( ) {
sed -ri -e 's' $'\x1' '^.*?(ParallelDownloads)[^\r\n\f]*' $'\x1' '\1 = 5' $'\x1' 'g' '/etc/pacman.conf'
pacstrap /mnt \
base \
base-devel \
linux \
linux-headers \
linux-firmware \
amd-ucode \
efibootmgr \
vim \
git \
iwd \
networkmanager \
network-manager-applet \
dialog \
os-prober \
reflector \
bluez \
bluez-utils \
man-db \
xdg-utils \
xdg-user-dirs
}
function gen_fstab ( ) {
genfstab -U /mnt | grep -v " ${ zpool_name } " | tr -s '\n' | sed -r -e 's/\/mnt//' -e '/./,$!d' > '/mnt/etc/fstab'
}
function configure_hosts_file ( ) {
cat > '/mnt/etc/hosts' <<EOF
#<ip-address> <hostname.domain.org> <hostname>
127.0.0.1 localhost ${ 1 }
::1 localhost ${ 1 }
EOF
}
function set_hostname ( ) {
declare new_hostname
install_pkgs 'pwgen'
new_hostname = " $( pwgen --no-numerals --no-capitalize --ambiguous 8) "
printf -- '%s\n' " ${ new_hostname } " > '/mnt/etc/hostname'
configure_hosts_file " ${ new_hostname } "
}
function set_locale ( ) {
2023-02-20 02:49:21 +01:00
printf -- '%s\n' \
'KEYMAP=de-latin1' \
'FONT=Lat2-Terminus16' \
'FONT_MAP=8859-1' \
> '/mnt/etc/vconsole.conf'
2023-02-19 20:21:20 +01:00
sed -ri -e 's' $'\x1' '^(#)(en_US.UTF-8[^\r\n\f]*)' $'\x1' '\2' $'\x1' 'g' '/mnt/etc/locale.gen'
printf -- '%s\n' 'LANG=en_US.UTF-8' > '/mnt/etc/locale.conf'
}
function add_zfs_hook_to_initramfs ( ) {
2023-02-20 23:58:23 +01:00
# Add zfs hook, remove fsck hook from initramfs. Also add plain text key
# file into initramfs since it's living inside an encrypted pool anyway.
2023-02-20 02:49:21 +01:00
sed -ri \
2023-02-20 23:58:23 +01:00
-e 's' $'\x1' '^(FILES=)[^\r\n\f]*' $'\x1' '\1(/etc/zfs/' " ${ zpool_name } " '.key)' $'\x1' 'g' \
2023-02-20 02:49:21 +01:00
-e 's' $'\x1' '(HOOKS=)(.*?[\(| ])(filesystems)([\)| ][^\r\n\f]*)' $'\x1' '\1\2zfs \3\4' $'\x1' 'g' \
-e 's' $'\x1' '((\()(fsck)(\)))' $'\x1' '\2\4' $'\x1' 'g' \
-e 's' $'\x1' '(([[:space:]]+)(fsck)|(fsck)([[:space:]]+))' $'\x1' '' $'\x1' 'g' \
'/mnt/etc/mkinitcpio.conf'
2023-02-19 20:21:20 +01:00
}
2023-02-20 04:14:44 +01:00
function set_initramfs_build_list ( ) {
# No need to build fallback initramfs, our new fallback is ZFS snapshots
sed -ri \
-e '/^#/d' \
-e '/^$/d' \
-e '/^fallback/d' \
-e 's' $'\x1' '^(PRESETS=)[^\r\n\f]*' $'\x1' '\1(' "'" 'default' "'" ')' $'\x1' 'g' \
'/mnt/etc/mkinitcpio.d/linux.preset'
2023-02-21 00:38:55 +01:00
# Remove any existing fallback initramfs files
find '/mnt/boot' -type f -regextype posix-extended -iregex '^/mnt/boot/initramfs-.*?-fallback.img' -delete
2023-02-20 04:14:44 +01:00
}
2023-02-19 20:21:20 +01:00
function add_zfs_files_to_new_os ( ) {
2023-02-20 23:59:13 +01:00
for zfs_file in '/etc/hostid' '/etc/zfs/zpool.cache' '/etc/zfs/' " ${ zpool_name } " '.key' ; do
2023-02-19 20:21:20 +01:00
rsync -av --itemize-changes { '' ,'/mnt' } " ${ zfs_file } "
done
}
function enter_chroot ( ) {
arch-chroot /mnt /bin/bash -xe <<EOF
2023-02-20 00:47:53 +01:00
curl --silent '${this_script_url}' | bash
2023-02-19 20:21:20 +01:00
EOF
}
function get_pkg_info ( ) {
declare from_where local_pkg_info local_pkg_version version_search
from_where = " ${ 1 } "
version_search = '/Version/{print $3}'
pkg_info = " $( paru -$( [ [ " ${ from_where } " = = 'local' ] ] && printf -- '%s' 'Q' || printf -- '%s' 'S' ) i " ${ 2 } " 2>& 1) "
if [ [ " ${ from_where } " = = 'local' ] ] && grep -Piq -- '^error: package .*? was not found' <<< " ${ pkg_info } " ; then
return 1
else
local_pkg_version = " $( awk " ${ version_search } " <<< " ${ pkg_info } " ) "
fi
printf -- '%s' " ${ local_pkg_version } "
return 0
}
function paru_with_zfs_first ( ) {
if [ [ " ${# } " -eq '0' ] ] ; then
declare -A local_pkg_info
/usr/bin/paru -Sy
if local_pkg_info[ 'zfs-dkms' ] = " $( get_pkg_info 'local' 'zfs-dkms' ) " && local_pkg_info[ 'zfs-utils' ] = " $( get_pkg_info 'local' 'zfs-utils' ) " ; then
local_pkg_info[ 'zfs-dkms' ] = " $( get_pkg_info 'local' 'zfs-dkms' ) "
local_pkg_info[ 'zfs-utils' ] = " $( get_pkg_info 'local' 'zfs-utils' ) "
declare -A remote_pkg_info
remote_pkg_info[ 'zfs-dkms' ] = " $( get_pkg_info 'remote' 'zfs-dkms' ) "
remote_pkg_info[ 'zfs-utils' ] = " $( get_pkg_info 'remote' 'zfs-utils' ) "
/usr/bin/paru -S --needed archlinux-keyring
if [ [ " ${ local_pkg_info [ 'zfs-dkms' ] } " = = " ${ remote_pkg_info [ 'zfs-dkms' ] } " ] ] && \
[ [ " ${ local_pkg_info [ 'zfs-utils' ] } " = = " ${ remote_pkg_info [ 'zfs-utils' ] } " ] ] ; then
/usr/bin/paru -Su
else
/usr/bin/paru -Sy 'zfs-dkms' 'zfs-utils' \
--assume-installed zfs-dkms= " ${ local_pkg_info [ 'zfs-dkms' ] } " \
--assume-installed zfs-dkms= " ${ remote_pkg_info [ 'zfs-dkms' ] } " \
--assume-installed zfs-utils= " ${ local_pkg_info [ 'zfs-utils' ] } " \
--assume-installed zfs-utils= " ${ remote_pkg_info [ 'zfs-utils' ] } "
/usr/bin/paru -Su
fi
else
/usr/bin/paru -S --needed archlinux-keyring
/usr/bin/paru -Su
fi
else
/usr/bin/paru " ${ @ } "
fi
}
function create_unpriv_user ( ) {
account_name = " ${ 1 : ? } "
full_name = " ${ 2 :- ${ account_name } } "
authorized_keys_abs_path = '/home/' " ${ account_name } " '/.ssh/authorized_keys'
useradd --create-home --shell '/bin/bash' --comment 'Personal account of ' " ${ full_name } " --user-group " ${ account_name } "
chfn --full-name " ${ full_name } " " ${ account_name } "
mkdir -p " $( dirname " ${ authorized_keys_abs_path } " ) "
touch " ${ authorized_keys_abs_path } "
chown -R " ${ account_name } " ':' '/home/' " ${ account_name } " ; chmod -R 'u=rwX,go=' " $( dirname " ${ authorized_keys_abs_path } " ) "
}
function get_aur_helper ( ) {
create_unpriv_user 'build'
usermod --append --groups 'wheel' 'build'
printf -- '%s\n' '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' > '/etc/sudoers.d/10-wheel-group-no-passwd-prompt'
pushd /tmp
git clone 'https://aur.archlinux.org/paru.git'
2023-02-19 21:28:42 +01:00
chown -R 'build:' 'paru'
2023-02-19 20:21:20 +01:00
pushd 'paru'
sudo --user 'build' makepkg -si --noconfirm
popd
rm -rf 'paru'
popd
alias paru = 'paru_with_zfs_first'
}
function paru_install ( ) {
sudo --user build paru -S --noconfirm " ${ @ } "
}
2023-02-21 00:36:06 +01:00
function keep_initiramfs_root_only_rw ( ) {
declare systemd_local_admin_override_path unit_name
systemd_local_admin_override_path = '/etc/systemd/system'
unit_name = 'chmod-initramfs'
path_unit = " ${ systemd_local_admin_override_path %/ } " '/' " ${ unit_name } " '.path'
service_unit = " ${ systemd_local_admin_override_path %/ } " '/' " ${ unit_name } " '.service'
cat > " ${ path_unit } " <<"EOF"
[ Unit]
Description = chmod initramfs to be root-read-writable only
[ Path]
PathChanged = /boot/initramfs-linux.img
[ Install]
WantedBy = multi-user.target
WantedBy = system-update.target
EOF
cat > " ${ service_unit } " <<"EOF"
[ Unit]
Description = chmod initramfs to be root-read-writable only
[ Service]
Type = oneshot
ExecStart = /usr/bin/chmod 600 /boot/initramfs-linux.img
EOF
systemctl enable --now " ${ path_unit } "
}
2023-02-19 20:21:20 +01:00
function install_os_in_chroot ( ) {
### Reinit keyring
# As keyring is initialized at boot, and copied to the install dir with pacstrap, and ntp is running
# Time changed after keyring initialization, it leads to malfunction
# Keyring needs to be reinitialised properly to be able to sign keys.
rm -rf '/etc/pacman.d/gnupg'
pacman-key --init
pacman-key --populate archlinux
pacman -S archlinux-keyring --noconfirm
2023-02-21 00:40:16 +01:00
keep_initiramfs_root_only_rw
2023-02-19 20:21:20 +01:00
get_aur_helper
paru_install 'zfs-dkms' 'zfs-utils'
hwclock --systohc
locale-gen
source /etc/locale.conf
mkinitcpio -P
# Install ZFSBootMenu and deps
git clone --depth= 1 https://github.com/zbm-dev/zfsbootmenu/ '/tmp/zfsbootmenu'
paru_install 'cpanminus' 'kexec-tools' 'fzf' 'util-linux'
pushd '/tmp/zfsbootmenu'
make
make install
cpanm --notest --installdeps .
popd
rm -rf '/tmp/zfsbootmenu'
}
function set_root_pw ( ) {
printf -- '%s\n' 'root:password' | chpasswd --root '/mnt'
}
function configure_networking ( ) {
cat > '/mnt/etc/systemd/network/50-wired.network' <<"EOF"
[ Match]
Name = en*
[ Network]
DHCP = ipv4
IPForward = yes
2023-02-19 22:50:10 +01:00
[ DHCP]
2023-02-19 20:21:20 +01:00
UseDNS = no
RouteMetric = 10
EOF
systemctl enable 'systemd-networkd' --root= '/mnt'
systemctl disable 'systemd-networkd-wait-online' --root= '/mnt'
}
function configure_dns ( ) {
rm '/mnt/etc/resolv.conf'
ln -s '/run/systemd/resolve/stub-resolv.conf' '/mnt/etc/resolv.conf'
# sed -i 's/^#DNS=.*/DNS=1.1.1.1/' /mnt/etc/systemd/resolved.conf
systemctl enable 'systemd-resolved' --root= '/mnt'
}
function configure_zfs ( ) {
systemctl enable 'zfs-import-cache' 'zfs-mount' 'zfs-import.target' 'zfs.target' --root= '/mnt'
}
function configure_zfs_mount_gen ( ) {
mkdir -p '/mnt/etc/zfs/zfs-list.cache'
touch '/mnt/etc/zfs/zfs-list.cache/' " ${ zpool_name } "
zfs list -H -o name,mountpoint,canmount,atime,relatime,devices,exec,readonly,setuid,nbmand | sed 's/\/mnt//' > '/mnt/etc/zfs/zfs-list.cache/' " ${ zpool_name } "
systemctl enable 'zfs-zed.service' --root= '/mnt'
}
function configure_zfsbootmenu ( ) {
mkdir -p '/mnt/efi/EFI/ZBM'
curl -s 'https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master/etc/zfsbootmenu/mkinitcpio.conf' | sed -r -e '/^#/d' -e '/^$/d' > '/mnt/etc/zfsbootmenu/mkinitcpio.conf'
cat > '/mnt/etc/zfsbootmenu/config.yaml' <<EOF
Global:
ManageImages: true
BootMountPoint: /efi
InitCPIO: true
Components:
Enabled: false
EFI:
ImageDir: /efi/EFI/ZBM
Versions: false
Enabled: true
Kernel:
CommandLine: ro loglevel = 0 zbm.import_policy= hostid
Prefix: vmlinuz
EOF
# Up here maybe 'ro quiet' instead of 'ro'
zfs set org.zfsbootmenu:commandline= 'rw nowatchdog rd.vconsole.keymap=de-latin1' " ${ zpool_name } " '/root/' " ${ zfs_arch_dataset_name } "
}
function gen_zfsbootmenu ( ) {
arch-chroot /mnt /bin/bash -xe <<"EOF"
source /etc/locale.conf
find /efi/EFI/ZBM -type f -delete
generate-zbm
EOF
}
function get_disk_with_efipart ( ) {
declare disks_with_efipart
# Find disks that have exactly one EFI partition and where that EFI
# partition is partition number 1. We expect exactly one disk to meet
# these criteria. Anything else and we bail.
disks_with_efipart = " $( lsblk --output PATH,PARTTYPE --json --tree | jq --raw-output '.[][] | select(.children | length > 0) | select( any (.children[]; (.path | test("^[^[:digit:]]+1")) and (.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")) and ([select(.children[].parttype=="ebd0a0a2-b9e5-4433-87c0-68b6b72699c7")] | length == 1) ) | .path' ) "
if [ [ " $( wc -l <<< " ${ disks_with_efipart } " ) " -eq '1' ] ] && [ [ " $( wc -c <<< " ${ disks_with_efipart } " ) " -gt '1' ] ] ; then
printf -- '%s' " ${ disks_with_efipart } "
return 0
fi
return 1
}
function add_zbm_to_efi ( ) {
if ! efibootmgr | grep -Pi -- 'ZFSBootMenu' ; then
declare efi_disk
efi_disk = " $( get_disk_with_efipart) "
efibootmgr --disk " ${ efi_disk } " \
--part 1 \
--create \
--label "ZFSBootMenu" \
--loader "\EFI\ZBM\vmlinuz.efi" \
--verbose
fi
}
function umount_all ( ) {
umount '/mnt/efi'
zfs umount -a
zpool export " ${ zpool_name } "
}
function finalize_os_setup ( ) {
set_root_pw
configure_networking
configure_dns
configure_zfs
configure_zfs_mount_gen
configure_zfsbootmenu
gen_zfsbootmenu
add_zbm_to_efi
umount_all
}
function main ( ) {
if we_are_changerooted; then
install_os_in_chroot
else
set_ntp
2023-02-19 20:38:42 +01:00
update_pacman_db
2023-02-19 20:21:20 +01:00
install_pkgs 'jq'
declare zfs_part
zfs_part = " $( get_parts 'zfs' ) "
if we_have_exactly_one_part 'zfs' " ${ zfs_part } " ; then
printf -- 'Creating zpool on partition ' "'" '%s' "'" ' ...\n' " ${ zfs_part } "
install_zfs
setup_zpool " ${ zfs_part } "
mount_system
copy_zpool_cache
install_archlinux
gen_fstab
set_hostname
set_locale
add_zfs_hook_to_initramfs
2023-02-20 04:14:44 +01:00
set_initramfs_build_list
2023-02-20 01:19:13 +01:00
add_zfs_files_to_new_os
2023-02-19 20:21:20 +01:00
enter_chroot
# We're done in chroot
finalize_os_setup
fi
fi
}
main