arch-zbm/setup.sh
hygienic-books 64c66cbd0f fix(zfs): Install ZFS only when needed (#26)
In situations where this script runs on alternative Arch Linux live
CD ISOs such as github.com/stevleibelt/arch-linux-live-cd-iso-with
-zfs we may not have to insall ZFS kernel modules. Test if the 'zfs'
module is loaded and skip installation if yes.
2025-01-17 21:09:19 +01:00

1591 lines
56 KiB
Bash

#!/bin/bash
# Whatever comes in on file descriptor (FD) 3 gets redirected to where file
# descriptor 1 is pointing. File descriptor 1 points to stdout so when we
# output-redirect something into FD 3 it shows up on stdout. We can use this
# to produce arbitrary logging output inside a subshell like so:
#
# function my_func () {
# some_command "${1:?}"
# >&3 echo 'A log message'
# }
#
# var="$(my_func arg_1)"
#
# Here "${var}" will only capture the output of some_command "${1:?}". It
# will not capture 'echo' which will instead show up on our stdout/FD 1.
exec 3>&1
declare this_script_url
this_script_url="${SCRIPT_URL:?}"
declare zpool_name zfs_arch_dataset_name settings_file
zpool_name='zpool'
zfs_arch_dataset_name='archlinux'
settings_file='archzbm_settings.env'
declare -A partition_types
partition_types[gpt_zfs]='6a85cf4d-1dd2-11b2-99a6-080020736631'
partition_types[gpt_efi]='c12a7328-f81f-11d2-ba4b-00a0c93ec93b'
partition_types[mbr_zfs]='0xbf'
partition_types[mbr_boot]='0x83'
# https://unix.stackexchange.com/a/48550
set -E
trap '[ "$?" -ne 77 ] || exit 77' ERR
declare zpool_drive efi_drive boot_drive part_schema
function calculate_prefix_from_netmask () {
# https://stackoverflow.com/a/50419919
c='0'
x='0'"$(printf -- '%o' ${1//./ })"
while [ "${x}" -gt '0' ]; do
# Modulo then bitwise-shift x and store as new x
let c+="$(( x % 2 ))" 'x>>=1'
done
printf -- '%s' '/'"${c}";
}
function setup_env_vars () {
printf -- '%s\n' \
'We will go over a series of questions to create an answer file with' \
'options you want the script to use.' \
'' \
'Current working directory is:'\
"$(pwd)" \
'' \
'After we'"'"'re done answer file will be written to current working dir:' \
'./'"${settings_file}" \
'' \
'Press <Ctrl>+C to abort this process. No answer file will' \
'be written to ./'"${settings_file}"' if you abort the script.' \
'' \
'When done rerun the same command you just did without '"'"'setup'"'"' argument.' \
''
read -u3 -n 1 -s -r -p "Press any key to begin questionnaire"
echo
echo '----------------------------------------'
echo
read -u3 -p 'Please type kernel version to use, leave empty for latest, confirm with <Enter>: ' ARCHZBM_KERNEL_VER
echo
echo 'Do you want compressed datasets?'
select arg_compressed in 'Compressed' 'Uncompressed'; do
case "${arg_compressed}" in
'Compressed')
break
;;
'Uncompressed')
ARCHZBM_ZFSPROPS_NO_COMPRESSION='true'
break
;;
esac
done <&3 && echo
echo 'Do you want encrypted datasets?'
select arg_encrypted in 'Encrypted' 'Unencrypted'; do
case "${arg_encrypted}" in
'Encrypted')
break
;;
'Unencrypted')
ARCHZBM_ZFSPROPS_NO_ENCRYPTION='true'
break
;;
esac
done <&3 && echo
if [[ "${arg_encrypted}" = 'Encrypted' ]]; then
echo 'Do you want a custom dataset decryption password?'
select arg_custom_dataset_pw in 'Yes' 'No (use '"'"'password'"'"')'; do
case "${arg_custom_dataset_pw}" in
'Yes')
want_custom_dataset_pw='true'
break
;;
'No (use '"'"'password'"'"')')
break
;;
esac
done <&3 && echo
if [[ "${want_custom_dataset_pw}" ]]; then
read -u3 -p 'Please type password, confirm with <Enter>: ' -s ARCHZBM_ZPOOL_PASSWORD
echo
echo
fi
fi
echo 'Do you want a custom '"'"'root'"'"' user password?'
select arg_custom_root_pw in 'Yes' 'No (use '"'"'password'"'"')'; do
case "${arg_custom_root_pw}" in
'Yes')
want_custom_root_pw='true'
break
;;
'No (use '"'"'password'"'"')')
break
;;
esac
done <&3 && echo
if [[ "${want_custom_root_pw}" ]]; then
read -u3 -p 'Please type password, confirm with <Enter>: ' -s ARCHZBM_ROOT_PASSWORD
echo
echo
fi
echo 'Do you want an SSH daemon in ZFSBootMenu?'
select arg_ssh_in_zbm in "Yes" "No"; do
case "${arg_ssh_in_zbm}" in
Yes)
want_ssh_in_zbm='true'
break
;;
No)
break
;;
esac
done <&3 && echo
if [[ "${want_ssh_in_zbm}" ]]; then
echo 'How do you want to assign an IP address in ZFSBootMenu?'
select arg_ip_autoconf_method in 'Statically' 'Dynamically, DHCP' 'Dynamically, BOOTP' 'Dynamically, RARP'; do
case "${arg_ip_autoconf_method}" in
'Statically')
ARCHZBM_NET_AUTOCONF='none'
break
;;
'Dynamically, DHCP')
ARCHZBM_NET_AUTOCONF='dhcp'
break
;;
'Dynamically, BOOTP')
ARCHZBM_NET_AUTOCONF='bootp'
break
;;
'Dynamically, RARP')
ARCHZBM_NET_AUTOCONF='rarp'
break
;;
esac
done <&3 && echo
read -u3 -p 'Which device to configure (eth0 is a good guess): ' ARCHZBM_NET_DEVICE
echo
if [[ "${arg_ip_autoconf_method}" = 'Statically' ]]; then
read -u3 -p 'Interface IP address: ' ARCHZBM_NET_CLIENT_IP
echo
read -u3 -p 'Netmask: ' ARCHZBM_NET_NETMASK
echo
read -u3 -p 'Gateway IP address: ' ARCHZBM_NET_GATEWAY_IP
echo
fi
echo 'Do you want a custom SSH listening port?'
select arg_custom_ssh_port in 'Yes (let me specify)' 'No (keep port 22)'; do
case "${arg_custom_ssh_port}" in
'Yes (let me specify)')
want_custom_ssh_port='true'
break
;;
'No (keep port 22)')
break
;;
esac
done <&3 && echo
if [[ "${want_custom_ssh_port}" ]]; then
read -u3 -p 'Please type SSH port, confirm with <Enter>: ' ARCHZBM_SSH_PORT
echo
fi
echo 'Do you want the SSH daemon to use a custom keepalive send interval?'
select arg_custom_ssh_keepalive_intvl in 'Yes (let me specify)' 'No (keep 1)'; do
case "${arg_custom_ssh_keepalive_intvl}" in
'Yes (let me specify)')
want_custom_keepalive_intvl='true'
break
;;
'No (keep 1)')
break
;;
esac
done <&3 && echo
if [[ "${want_custom_keepalive_intvl}" ]]; then
read -u3 -p 'Please type server keepalive send interval, confirm with <Enter>: ' ARCHZBM_SSH_KEEPALIVE_INTVL
echo
fi
read -u3 -p 'Please type SSH pub keys on one line separated by double-commas (,,) and confirm with <Enter>: ' ARCHZBM_SSH_AUTH_KEYS
echo
fi
if [[ "${want_ssh_in_zbm}" ]]; then
echo 'Do you want to define operating system'"'"'s IP address?'
select arg_os_ip in 'Yes (let me specify)' 'Yes (use ZBM addresses)' 'No (DHCP is fine)'; do
case "${arg_os_ip}" in
'Yes (let me specify)')
want_custom_ip_in_os='true'
want_dns_and_ntp='true'
break
;;
'Yes (use ZBM addresses)')
ARCHZBM_OS_CLIENT_IP="${ARCHZBM_NET_CLIENT_IP}"
ARCHZBM_NET_CLIENT_IP_PREFIX="$(calculate_prefix_from_netmask "${ARCHZBM_NET_NETMASK}")"
ARCHZBM_OS_CLIENT_IP+="${ARCHZBM_NET_CLIENT_IP_PREFIX}"
ARCHZBM_OS_GATEWAY_IP="${ARCHZBM_NET_GATEWAY_IP}"
want_dns_and_ntp='true'
break
;;
'No (DHCP is fine)')
break
;;
esac
done <&3 && echo
if [[ "${want_custom_ip_in_os}" ]]; then
read -u3 -p 'Interface IP address with CIDR prefix (a.b.c.d/nn): ' ARCHZBM_OS_CLIENT_IP
echo
read -u3 -p 'Gateway IP address: ' ARCHZBM_OS_GATEWAY_IP
echo
fi
else
echo 'Do you want to define operating system'"'"'s IP address?'
select arg_os_ip in 'Yes (let me specify)' 'No (DHCP is fine)'; do
case "${arg_os_ip}" in
'Yes (let me specify)')
want_own_ip_in_os='true'
want_dns_and_ntp='true'
break
;;
'No (DHCP is fine)')
break
;;
esac
done <&3 && echo
if [[ "${want_own_ip_in_os}" ]]; then
read -u3 -p 'Interface IP address with CIDR prefix (a.b.c.d/nn): ' ARCHZBM_OS_CLIENT_IP
echo
read -u3 -p 'Gateway IP address: ' ARCHZBM_OS_GATEWAY_IP
echo
fi
fi
if [[ "${want_ssh_in_zbm}" ]]; then
echo 'Do you want to define OS '"'"'root'"'"' user'"'"'s SSH pub key?'
select arg_root_pub_keys in 'Yes (let me specify)' 'Yes (use ZBM pub keys)' 'No (don'"'"'t enable sshd.service)'; do
case "${arg_root_pub_keys}" in
'Yes (let me specify)')
want_custom_pub_keys_in_os='true'
break
;;
'Yes (use ZBM pub keys)')
ARCHZBM_OS_SSH_AUTH_KEYS="${ARCHZBM_SSH_AUTH_KEYS}"
break
;;
'No (don'"'"'t enable sshd.service)')
break
;;
esac
done <&3 && echo
if [[ "${want_custom_pub_keys_in_os}" ]]; then
read -u3 -p 'Please type SSH pub keys on one line separated by double-commas (,,) and confirm with <Enter>: ' ARCHZBM_OS_SSH_AUTH_KEYS
echo
fi
else
echo 'Do you want to define OS root user'"'"'s SSH pub key?'
select arg_root_pub_keys in 'Yes (let me specify)' 'No (don'"'"'t enable sshd.service)'; do
case "${arg_root_pub_keys}" in
'Yes (let me specify)')
want_own_pub_key_in_os='true'
break
;;
'No (don'"'"'t enable sshd.service)')
break
;;
esac
done <&3 && echo
if [[ "${want_own_pub_key_in_os}" ]]; then
read -u3 -p 'Please type SSH pub keys on one line separated by double-commas (,,) and confirm with <Enter>: ' ARCHZBM_OS_SSH_AUTH_KEYS
echo
fi
fi
if [[ "${want_dns_and_ntp}" ]]; then
read -u3 -p 'Specify one or more comma-separated DNS IPs: ' ARCHZBM_OS_DNS_IP
echo
echo 'Do you want to override Arch Linux'"'"' NTP servers?'
select arg_custom_ntp in 'Yes' 'No'; do
case "${arg_custom_ntp}" in
'Yes')
want_own_ntp='true'
break
;;
'No')
break
;;
esac
done <&3 && echo
if [[ "${want_own_ntp}" ]]; then
read -u3 -p 'Specify one or more comma-separated NTP hostnames or IPs: ' ARCHZBM_OS_NTP_IP
echo
fi
fi
for env_var in 'ARCHZBM_KERNEL_VER' 'ARCHZBM_ZFSPROPS_NO_COMPRESSION' 'ARCHZBM_ZFSPROPS_NO_ENCRYPTION' 'ARCHZBM_ZPOOL_PASSWORD' 'ARCHZBM_ROOT_PASSWORD' 'ARCHZBM_NET_AUTOCONF' 'ARCHZBM_NET_DEVICE' 'ARCHZBM_NET_CLIENT_IP' 'ARCHZBM_NET_NETMASK' 'ARCHZBM_NET_GATEWAY_IP' 'ARCHZBM_SSH_PORT' 'ARCHZBM_SSH_KEEPALIVE_INTVL' 'ARCHZBM_SSH_AUTH_KEYS' 'ARCHZBM_OS_CLIENT_IP' 'ARCHZBM_OS_GATEWAY_IP' 'ARCHZBM_OS_SSH_AUTH_KEYS' 'ARCHZBM_OS_DNS_IP' 'ARCHZBM_OS_NTP_IP'; do
if [[ "${!env_var}" ]]; then
printf -- '%s='"'"'%s'"'"'\n' \
"${env_var}" "${!env_var}" \
>> "${settings_file}"
fi
done
printf -- '%s\n' \
'Done, please rerun script now with just' \
'... | bash' \
'so without the '"'"'setup'"'"' argument'
exit 77
}
function arg_parse () {
[[ "${1}" ]] && while :; do
case "${1}" in
-[[:alnum:]]*)
>&3 printf -- '%s\n' \
'Short options '"'${1}'"' detected. Only known option is' \
'literal string '"'"'setup'"'"'. No idea what '"'${1}'"' is.' \
'Exiting ...'
exit 77
;;
--*)
>&3 printf -- '%s\n' \
'Long-form option '"'${1}'"' detected. Only known option is' \
'literal string '"'"'setup'"'"'. No idea what '"'${1}'"' is.' \
'Exiting ...'
exit 77
;;
setup)
setup_env_vars
return
;;
*)
>&3 printf -- '%s\n' \
'Argument '"'${1}'"' detected. Only known option is literal' \
'string '"'"'setup'"'"'. No idea what '"'${1}'"' is.' \
'Exiting ...'
exit 77
;;
esac
done
}
function we_are_changerooted () {
if [ "$(stat -c '%d:%i' '/')" != "$(stat -c '%d:%i' '/proc/1/root/.')" ]; then
return 0
else
return 1
fi
}
function no_kernel_update_in_iso () {
#1.1
sed -ri -e 's'$'\x1''#(IgnorePkg)[^\r\n\f]+'$'\x1''\1 = linux linux-headers'$'\x1''g' /etc/pacman.conf
}
function set_ntp () {
#1.2
timedatectl set-ntp true
}
function resize_cow_space () {
#1.3
mount -o remount,size='50%' /run/archiso/cowspace
}
function update_pacman_db () {
#1.4
pacman_dl_parallel
printf -- '%s\n' 'Refreshing mirror list ...'
printf -- '%s\n' \
'--save /etc/pacman.d/mirrorlist' \
'--protocol https' \
'--latest 5' \
'--sort age' \
> '/etc/xdg/reflector/reflector.conf'
systemctl start reflector
# In an ISO and for the minimal number of packages we need we do not
# care about partial upgrades
#
# Are we better off not attempting an upgrade inside the ISO?
# Let's try and find out.
# while ! pacman -Syyuu --needed --noconfirm --downloadonly; do
# sleep 5
# done
# pacman -Syyuu --needed --noconfirm
pacman -Syy
}
function install_pkgs () {
#1.5
printf -- '%s\n' 'Installing packages ...'
while ! pacman -S --needed --noconfirm --downloadonly "${@}"; do
sleep 5
done
pacman -S --needed --noconfirm "${@}"
}
function install_zfs () {
#1.6
declare reset_colors='\033[0m'
if modinfo 'zfs' &>/dev/null; then
>&3 printf -- '%s\n' \
'ZFS kernel module is loaded, no need to install ...'
else
curl -s 'https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init' | bash
fi
printf -- "${reset_colors}"
}
function uefi_or_bios () {
#1.7
local part_count_linux part_count_efi
# Select disks with at least one partition. Among them count how many
# with the given partition type code we have.
part_count_linux="$(get_partitions | jq --raw-output '[.[][] | select(.children | length > 0) | .children[] | select(.parttype=="'"${partition_types[mbr_boot]}"'") | .path] | length')"
part_count_efi="$(get_partitions | jq --raw-output '[.[][] | select(.children | length > 0) | .children[] | select(.parttype=="'"${partition_types[gpt_efi]}"'") | .path] | length')"
if [[ "${part_count_linux}" -eq '1' ]] && [[ "${part_count_efi}" -eq '0' ]]; then
part_schema='mbr'
>&3 printf -- '%s\n' \
'Treating this as an MBR/legacy BIOS machine ...'
elif [[ "${part_count_linux}" -eq '0' ]] && [[ "${part_count_efi}" -eq '1' ]]; then
part_schema='gpt'
>&3 printf -- '%s\n' \
'Treating this as a GPT/UEFI machine ...'
else
>&3 printf -- '%s\n' \
'We are seeing partitions as follows:' \
'- Partition type code '"${partition_types[mbr_boot]}"': '"${part_count_linux}" \
'- Partition type code '"${partition_types[gpt_efi]}"': '"${part_count_efi}" \
'' \
'We are instead expecting either 1 and 0 indicating a legacy' \
'BIOS setup or 0 and 1 indicating UEFI. '"${part_count_linux}"' and '"${part_count_efi}"' is' \
'neither. Cowardly exiting ...'
exit 77
fi
export part_schema="${part_schema}"
}
function get_partitions () {
declare partitions_json
partitions_json="$(lsblk --output PATH,PARTTYPE --json --tree)" || return 1
printf -- '%s' "${partitions_json}"
return 0
}
function get_part_parent () {
local child_partition parent_partition
child_partition="${1:?}"
parent_partition="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.children[].path=="'"${child_partition:?}"'") | .path')"
printf -- '%s' "${parent_partition}"
return 0
}
function get_parts () {
local zfs_install_drive part_type_code
declare parttype parts
parttype="${1:?}"
zfs_install_drive="${2:-}"
case "${parttype}" in
zfs)
if [[ "${part_schema}" = 'mbr' ]]; then
part_type_code="${partition_types[mbr_zfs]}"
else
part_type_code="${partition_types[gpt_zfs]}"
fi
parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | .children[] | select(.parttype=="'"${part_type_code}"'") | .path')" || exit 1
;;
boot)
parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="'"${partition_types[mbr_boot]}"'") | .path')" || exit 1
;;
efi)
parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="'"${partition_types[gpt_efi]}"'") | .path')" || exit 1
;;
*)
>&3 printf -- '%s\n' 'Unknown partition type '"'"'"${parttype}"'"'"', exiting ...'
exit 77
;;
esac
printf -- '%s' "${parts}"
return 0
}
function we_have_exactly_one_part () {
local parttype parts_list parts_count
parttype="${1:?}"
parts_list="${2:?}"
parts_count="$(wc -l <<<"${parts_list}")"
if [[ "$(wc -c <<<"${parts_list}")" -gt '1' ]]; then
case "${parts_count}" in
0)
>&3 printf -- '%s\n' 'No '"${parttype^^}"' partition found. Exiting ...'
exit 77
;;
1)
return 0
;;
*)
return 1
;;
esac
>&3 printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting ...'
exit 77
fi
}
function get_drive_id () {
local drive_id_list drive_id_single
drive_id_list="$(find -L /dev/disk/by-partuuid -samefile "${1:?}" | sort)"
drive_id_single="$(head -n1 <<<"${drive_id_list}")"
if [[ "$(wc -l <<<"${drive_id_single}")" -eq '1' ]] && [[ "$(wc -c <<<"${drive_id_single}")" -gt '1' ]]; then
printf -- '%s' "${drive_id_single}"
return 0
fi
>&3 printf -- '%s\n' 'No '"'${1:?}'"' partition entry in /dev/disk/by-partuuid, exiting ...'
exit 77
}
function select_part () {
local parts enriched_parts enriched_parts_count part_number part_type zfs_install_drive part_type_code specific_part_type
declare part
part_type="${1:?}" # 'boot' or 'zfs'
zfs_install_drive="${2:-}"
if [[ "${part_type}" = 'boot' ]]; then
if [[ "${part_schema}" = 'mbr' ]]; then
specific_part_type='boot'
else
specific_part_type='efi'
fi
else
specific_part_type="${part_type}"
fi
if [[ "${zfs_install_drive}" ]]; then
# This is intended to find correct boot/EFI partition
parts="$(get_parts "${specific_part_type}" "${zfs_install_drive}")"
else
parts="$(get_parts "${specific_part_type}")"
fi
if [[ ! "${parts}" ]]; then
case "${part_type}" in
efi)
part_type_human_readable='EFI System Partition (ESP) with partition type code EF00'
;;
zfs)
if [[ "${part_schema}" = 'mbr' ]]; then
part_type_code='bf'
else
part_type_code='BF00'
fi
part_type_human_readable='ZFS zpool partition with partition type code '"${part_type_code}"
;;
esac
>&3 printf -- '%s\n' \
'It looks as if there is no '"${part_type_human_readable}" \
'on any of the disks. Did you correctly partition a disk before starting?' \
'Check https://quico.space/quico-os-setup/arch-zbm#prep. Exiting ...'
exit 77
fi
if we_have_exactly_one_part "${part_type}" "${parts}"; then
part="${parts}"
else
>&3 printf -- '%s\n' 'More than one '"${part_type^^}"' partition to pick for installation. Cowardly exiting ...'
exit 77
fi
printf -- '%s' "${part}"
return 0
}
function no_zpool_exists () {
declare zpool_list
zpool_list="$(zpool list -H)"
[[ "$(wc -l <<<"${zpool_list}")" -le '1' ]] && [[ "$(wc -c <<<"${zpool_list}")" -le '1' ]] && return 0
return 1
}
function set_zpool_password () {
local zpool_password
if [[ "${ARCHZBM_ZPOOL_PASSWORD}" ]]; then
zpool_password="${ARCHZBM_ZPOOL_PASSWORD}"
else
zpool_password='password'
fi
# May or may not have a newline at the end, ZFS doesn't care
printf -- '%s' "${zpool_password}" > '/etc/zfs/'"${zpool_name}"'.key'
chmod '000' '/etc/zfs/'"${zpool_name}"'.key'
}
function import_pool () {
zpool import -d '/dev/disk/by-partuuid' -R '/mnt' "${zpool_name}" -N -f
[[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && zfs load-key "${zpool_name}"
}
function create_pool () {
# Create a temporary pool that is not cached
#
# Add zfsprops 'compression' unless environment variable
# ARCHZBM_ZFSPROPS_NO_COMPRESSION is set to any value.
#
# Add zfsprops 'encryption' along with 'keyformat' and a 'keylocation'
# unless environment variable ARCHZBM_ZFSPROPS_NO_ENCRYPTION is set to
# any value.
zpool create -f \
-o 'ashift=12' \
-o 'autotrim=on' \
-O 'acltype=posix' \
$([[ ! "${ARCHZBM_ZFSPROPS_NO_COMPRESSION}" ]] && \
printf -- '%s ' \
'-O compression=on') \
-O 'relatime=on' \
-O 'xattr=sa' \
$([[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && \
printf -- '%s ' \
'-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' -o 'canmount=off' "${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 load_settings_file () {
#1.8
local working_dir settings_abs
working_dir="$(pwd)"
settings_abs="${working_dir}"'/'"${settings_file}"
if [[ -r "${settings_abs}" ]]; then
set -a
source "${settings_abs}"
set +a
fi
}
function setup_zpool () {
#1.9
local drive_by_id
zpool_drive="$(select_part 'zfs')"
drive_by_id="$(get_drive_id "${zpool_drive}")"
[[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && 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
else
>&3 printf -- '%s\n' 'A zpool already exists, that is unexpected. Cowardly exiting 1 ...'
exit 1
fi
}
function mount_system () {
#1.10
zfs mount "${zpool_name}"'/root/'"${zfs_arch_dataset_name}"
zfs mount -a
local zfs_parent efi_part boot_part
zfs_parent="$(get_part_parent "${zpool_drive:?}")"
if [[ "${part_schema}" = 'mbr' ]]; then
boot_drive="${zfs_parent}"
export boot_drive="${boot_drive}"
boot_part="$(select_part 'boot' "${zfs_parent:?}")"
mkdir -p '/mnt/boot/syslinux'
mount "${boot_part}" '/mnt/boot/syslinux'
else
efi_drive="${zfs_parent}"
export efi_drive="${efi_drive}"
efi_part="$(select_part 'boot' "${zfs_parent:?}")"
mkdir -p '/mnt/efi'
mount "${efi_part}" '/mnt/efi'
fi
}
function copy_zpool_cache () {
#1.11
mkdir -p '/mnt/etc/zfs'
zpool set 'cachefile=/etc/zfs/'"${zpool_name}"'.cache' "${zpool_name}"
}
function pacman_dont_check_space () {
# See pacman bug comment
# https://bugs.archlinux.org/task/45070#comment142712
#
# When we pacstrap onto ZFS pacman incorrectly calculates and
# overestimates required disk space. We instead assume an installation
# gets done with at least a 10 GiB drive which is plenty. Skip pacman's
# size check.
#
# We're setting this in Arch Linux ISO CD while we install proper Arch.
# No need to revert this later as it is ephemeral anyway.
sed -ri -e 's'$'\x1''^.*?(CheckSpace)([^\r\n\f]*)'$'\x1''#\1\2'$'\x1''g' '/etc/pacman.conf'
}
function install_archlinux () {
#1.12
pacman_dl_parallel
pacman_dont_check_space
while ! 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; do
sleep 5
done
}
function gen_fstab () {
#1.13
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 () {
#1.14
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 () {
#1.15
printf -- '%s\n' \
'KEYMAP=de-latin1' \
'FONT=Lat2-Terminus16' \
'FONT_MAP=8859-1' \
> '/mnt/etc/vconsole.conf'
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 in_file_in_array_insert_n_before_m () {
local arg_file arg_array arg_string arg_precede
arg_file="${1:?}"
arg_array="${2:?}"
arg_string="${3:?}"
arg_precede="${4:?}"
# Look for a line that contains in this order
# - String "${arg_array}" and a equals sign (=), assign capture group \1
# - Followed by as few characters as possible followed by either an
# opening parenthesis or a space character, assign capture group \2
# - String "${arg_precede}", capture as capture group \3
# - Followed by either a closing parenthesis or a space which are then
# followed by as many non-line break characters as possible, capture
# as capture group \2
#
# For following example text we're assuming that:
# - "${arg_array}" equals 'HOOKS'
# - "${arg_precede}" equals 'filesystems'
# - "${arg_string}" equals 'zfs'
#
# This finds a 'HOOKS=' array definition that contains the string
# 'filesystems' either at the beginning of the 'HOOKS=(...)' opening
# parenthesis, at the very end or somewhere in the middle where it may
# be preceded or followed by one or more space characters. It saves
# 'HOOKS=', it saves whatever precedes 'filesystems' and 'filesystems'
# itself plus whatever comes after 'filesystems' until end of line. It
# lastly inserts 'zfs' and a space character right in front of
# 'filesystems'.
sed -ri \
-e 's'$'\x1''('"${arg_array}"'=)(.*?[( ])('"${arg_precede}"')([) ][^\r\n\f]*)'$'\x1''\1\2'"${arg_string}"' \3\4'$'\x1''g' \
"${arg_file}"
}
function in_file_in_array_insert_n_at_the_end () {
local arg_file arg_array arg_string
arg_file="${1:?}"
arg_array="${2:?}"
arg_string="${3:?}"
# Look for end of array, insert "${arg_string}" right before
#
# For following example text we're assuming that:
# - "${arg_array}" equals 'HOOKS'
# - "${arg_string}" equals 'net'
#
# This:
# - Finds a 'HOOKS=' array definition, saves it as \1
# - Finds as many non-closing parenthesis characters as possible, so all
# characters until just before the final ')' character of the line and
# saves this as \2
# - Finds the closing parenthesis character plus all non-line break
# characters until end of line that come after it and saves this as \3
# - Adds a space character and 'net' after \2
sed -ri \
-e 's'$'\x1''('"${arg_array}"'=)([^)]*)(\)[^\r\n\f]*)'$'\x1''\1\2 '"${arg_string}"'\3'$'\x1''g' \
"${arg_file}"
}
function in_file_in_array_remove_n () {
local arg_file arg_array arg_string
arg_file="${1:?}"
arg_array="${2:?}"
arg_string="${3:?}"
# Look for any line that contains "${arg_string}", delete that string
#
# For following example text we're assuming that:
# - "${arg_array}" equals 'HOOKS'
# - "${arg_string}" equals 'fsck'
#
# First -e expression removes 'fsck' wherever it is defined as the one
# and only element of any HOOKS=(fsck) should such a line exist.
#
# Second -e expression finds string 'fsck' where it's preceded by space
# character(s) and followed by either space character(s) or a closing
# parenthesis.
#
# Third -e expression finds string 'fsck' where it's preceded by space
# character(s) or an opening parenthesis and followed space
# character(s).
sed -ri \
-e 's'$'\x1''((\()('"${arg_string}"')(\)))'$'\x1''\2\4'$'\x1''g' \
-e 's'$'\x1''('"${arg_array}"'=.*?)([[:space:]]+'"${arg_string}"')([[:space:]]+|\))'$'\x1''\1\3'$'\x1''g' \
-e 's'$'\x1''('"${arg_array}"'=.*?)([[:space:]]+|\()('"${arg_string}"'[[:space:]]+)'$'\x1''\1\2'$'\x1''g' \
"${arg_file}"
}
function add_zfs_hook_to_initramfs () {
#1.16
# Add zfs hook, remove fsck hook from initramfs.
in_file_in_array_insert_n_before_m '/mnt/etc/mkinitcpio.conf' 'HOOKS' 'zfs' 'filesystems'
in_file_in_array_remove_n '/mnt/etc/mkinitcpio.conf' 'HOOKS' 'fsck'
# Also unless encryption's unwanted add plain text key file into
# initramfs since it's living inside an encrypted pool anyway.
[[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && sed -ri \
-e 's'$'\x1''^(FILES=)[^\r\n\f]*'$'\x1''\1(/etc/zfs/'"${zpool_name}"'.key)'$'\x1''g' \
'/mnt/etc/mkinitcpio.conf'
}
function set_initramfs_build_list () {
#1.17
# 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'
# Remove any existing fallback initramfs files
find '/mnt/boot' -type f -regextype posix-extended -iregex '^/mnt/boot/initramfs-.*?-fallback.img' -delete
}
function add_zfs_files_to_new_os () {
#1.18
for zfs_file in '/etc/hostid' '/etc/zfs/zpool.cache' $([[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && printf -- '%s' '/etc/zfs/'"${zpool_name}"'.key'); do
rsync -av --itemize-changes {'','/mnt'}"${zfs_file}"
done
}
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 enter_chroot () {
#2.1
arch-chroot /mnt /bin/bash -xe <<EOF
curl --silent '${this_script_url}' | bash
EOF
}
function keep_initiramfs_root_only_rw () {
#2.3
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 "${path_unit}"
}
function pacman_dl_parallel () {
#2.4
# We're setting this in Arch Linux ISO CD while we install proper Arch.
# No need to revert this later as it is ephemeral anyway.
sed -ri -e 's'$'\x1''^.*?(ParallelDownloads)[^\r\n\f]*'$'\x1''\1 = 20'$'\x1''g' '/etc/pacman.conf'
}
function unleash_makepkg () {
#2.5
local path_prefix
path_prefix='/mnt'
if we_are_changerooted; then
path_prefix=''
fi
sed -ri \
-e 's'$'\x1''^(#?(MAKEFLAGS=))[^\r\n\f]*'$'\x1''\2"-j$(nproc --ignore 1)"'$'\x1''g' \
"${path_prefix}"'/etc/makepkg.conf'
}
function add_motd_getting_started_msg () {
#2.6
cat > '/etc/motd' <<"EOF"
####################
GUI basics:
paru -S xorg plasma-meta kde-applications-meta sddm
localectl set-x11-keymap de
useradd --create-home --shell /bin/bash --user-group --groups wheel <user>
passwd <user>
systemctl enable --now sddm.service
####################
EOF
}
function get_aur_helper () {
#2.7
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-bin.git
chown -R 'build:' 'paru-bin'
pushd 'paru-bin'
sudo --user 'build' makepkg -si --noconfirm
popd
rm -rf 'paru-bin'
popd
}
function paru_install () {
sudo --user build paru -S --noconfirm "${@}"
}
function configure_syslinux () {
paru_install 'syslinux'
cp /usr/lib/syslinux/bios/*.c32 /boot/syslinux
extlinux --install /boot/syslinux
cat > /boot/syslinux/syslinux.cfg <<EOF
UI menu.c32
PROMPT 0
MENU TITLE ZFSBootMenu
TIMEOUT 100
DEFAULT zfsbootmenu
LABEL zfsbootmenu
MENU LABEL ZFSBootMenu
KERNEL /zfsbootmenu/vmlinuz-bootmenu
INITRD /zfsbootmenu/initramfs-bootmenu.img
APPEND zfsbootmenu quiet loglevel=0
LABEL zfsbootmenu-backup
MENU LABEL ZFSBootMenu (Backup)
KERNEL /zfsbootmenu/vmlinuz-bootmenu-backup
INITRD /zfsbootmenu/initramfs-bootmenu-backup.img
APPEND zfsbootmenu quiet loglevel=0
EOF
dd bs=440 count=1 conv=notrunc if='/usr/lib/syslinux/bios/mbr.bin' of="${boot_drive}"
}
function configure_zfsbootmenu () {
#2.9
paru_install 'zfsbootmenu'
in_file_in_array_remove_n '/etc/zfsbootmenu/mkinitcpio.conf' 'HOOKS' 'zfsbootmenu'
if [[ "${part_schema}" = 'gpt' ]]; then
cat > '/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
else
configure_syslinux
cat > '/etc/zfsbootmenu/config.yaml' <<EOF
Global:
ManageImages: true
BootMountPoint: /boot/syslinux
InitCPIO: true
Components:
Enabled: true
Versions: false
ImageDir: /boot/syslinux/zfsbootmenu
Kernel:
Prefix: vmlinuz
EOF
fi
# Up here maybe 'ro quiet' instead of 'ro'. This is ZFSBootMenu's kernel
# command line. In MBR/Syslinux/extlinux mode /it/ must pass arguments to
# ZFSBootMenu's kernel so check /boot/syslinux/syslinux.cfg for how we start
# ZFSBootMenu in this mode. The .Kernel.CommandLine in
# /etc/zfsbootmenu/config.yaml is irrelevant for us in MBR/Syslinux/extlinux
# mode.
# Assign cmdline for final kernel start. This is our Arch Linx kernel
# command line.
zfs set org.zfsbootmenu:commandline='rw nowatchdog rd.vconsole.keymap=de-latin1' "${zpool_name}"'/root/'"${zfs_arch_dataset_name}"
}
function get_dropbear_hooks () {
mkdir -p '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main'
git -C '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main' clone 'https://quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook.git' .
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-hook.sh' '/usr/local/bin/pacman-mkinitcpio-dropbear-hook'
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-install.sh' '/usr/local/bin/pacman-mkinitcpio-dropbear-install'
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-hook.hook' '/usr/share/libalpm/hooks/pacman-mkinitcpio-dropbear-hook.hook'
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-install.hook' '/usr/share/libalpm/hooks/pacman-mkinitcpio-dropbear-install.hook'
}
function customize_dropbear_hooks () {
local env_archzbm_ssh_port env_archzbm_ssh_keepalive_intvl
env_archzbm_ssh_port="${ARCHZBM_SSH_PORT:-22}"
env_archzbm_ssh_keepalive_intvl="${ARCHZBM_SSH_KEEPALIVE_INTVL:-1}"
if [[ "${env_archzbm_ssh_port}" -ne '22' ]] || [[ "${env_archzbm_ssh_keepalive_intvl}" -ne '1' ]]; then
paru_install 'rsync'
rsync -av '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/dropbear_hook'{,'.override'}'.patch'
fi
if [[ "${env_archzbm_ssh_port}" -ne '22' ]]; then
sed -ri -e 's'$'\x1''-p [[:digit:]]+'$'\x1''-p '"${env_archzbm_ssh_port}"''$'\x1''g' '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/dropbear_hook.override.patch'
fi
if [[ "${env_archzbm_ssh_keepalive_intvl}" -ne '1' ]]; then
sed -ri -e 's'$'\x1''-K [[:digit:]]+'$'\x1''-K '"${env_archzbm_ssh_keepalive_intvl}"''$'\x1''g' '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/dropbear_hook.override.patch'
fi
}
function set_unique_ip_in_syslinux_kcl () {
local zbm_config default_ip
zbm_config="${1:?}"
default_ip="${2:?}"
# First -e expression removes first looks for lines that contain
# 'APPEND' plus a space character and only in those lines removes all
# occurrences of ' ip=' followed by as many non-space characters as
# possible. This removes whatever 'ip=' definition already was present.
#
# Second -e expression similarly looks for lines that contain 'APPEND'
# plus a space character then at their end inserts a space plus our
# desired new 'ip=' definition. This puts in place the 'ip=' we want.
sed -ri \
-e \\$'\x1''APPEND '$'\x1 s'$'\x1'' ip=([^[:space:]]*)'$'\x1'''$'\x1''gi' \
-e \\$'\x1''APPEND '$'\x1 s'$'\x1''$'$'\x1'' '"${default_ip}"''$'\x1''gi' \
"${zbm_config}"
}
function ensure_ip_in_kcl () {
local default_ip
default_ip='ip='"${ARCHZBM_NET_CLIENT_IP}"':'"${ARCHZBM_NET_SERVER_IP}"':'"${ARCHZBM_NET_GATEWAY_IP}"':'"${ARCHZBM_NET_NETMASK}"':'"${ARCHZBM_NET_HOSTNAME}"':'"${ARCHZBM_NET_DEVICE}"':'"${ARCHZBM_NET_AUTOCONF}"
if [[ "${part_schema}" = 'gpt' ]]; then
local zbm_config kcl_length kcl_string ip_addr_found new_kcl first_kcl_elem
local -a kcl
paru_install 'go-yq'
zbm_config='/etc/zfsbootmenu/config.yaml'
kcl_length="$(yq '.Kernel.CommandLine | length' "${zbm_config}")"
if [[ "${kcl_length}" -eq '0' ]]; then
>&3 printf -- '%s\n' \
'No .Kernel.CommandLine YAML element with content found in '"${zbm_config}"'. Exiting ...'
exit 77
else
kcl_string="$(yq '.Kernel.CommandLine' "${zbm_config}")"
fi
mapfile -t kcl < <(<<<"${kcl_string}" tr ' ' '\n' | sed '/^$/d')
for kcl_elem in "${!kcl[@]}"; do
if grep -Piq -- 'ip=' <<<"${kcl[$kcl_elem]}"; then
ip_addr_found='true'
kcl["${kcl_elem}"]="${default_ip}"
fi
done
if [[ ! "${ip_addr_found}" ]]; then
kcl+=("${default_ip}")
fi
new_kcl=''
first_kcl_elem='true'
for kcl_elem in "${kcl[@]}"; do
if [[ ! "${first_kcl_elem}" ]]; then
new_kcl+=' '"${kcl_elem}"
else
new_kcl+="${kcl_elem}"
unset -v first_kcl_elem
fi
done
yq -i '.Kernel.CommandLine = "'"${new_kcl}"'"' "${zbm_config}"
else
local zbm_config
zbm_config='/boot/syslinux/syslinux.cfg'
set_unique_ip_in_syslinux_kcl "${zbm_config}" "${default_ip}"
fi
}
function set_pub_keys () {
local authorized_keys_file raw_pub_keys
authorized_keys_file="${1:?}"
raw_pub_keys="${2:?}"
:> "${authorized_keys_file}"
while IFS= read -r pub_key_line; do
printf -- '%s\n' "${pub_key_line}" >> "${authorized_keys_file}"
done < <(<<<"${raw_pub_keys}" sed -r -e 's/,,/\n/g')
sed -i '/^$/d' "${authorized_keys_file}"
}
function we_want_ssh () {
#2.10
if [[ "${ARCHZBM_NET_CLIENT_IP}" ]] || \
[[ "${ARCHZBM_NET_SERVER_IP}" ]] || \
[[ "${ARCHZBM_NET_GATEWAY_IP}" ]] || \
[[ "${ARCHZBM_NET_NETMASK}" ]] || \
[[ "${ARCHZBM_NET_HOSTNAME}" ]] || \
[[ "${ARCHZBM_NET_DEVICE}" ]] || \
[[ "${ARCHZBM_NET_AUTOCONF}" ]] || \
[[ "${ARCHZBM_SSH_PORT}" ]] || \
[[ "${ARCHZBM_SSH_KEEPALIVE_INTVL}" ]] || \
[[ "${ARCHZBM_SSH_AUTH_KEYS}" ]]; then
>&3 printf -- '%s\n' 'Installing SSH in ZFSBootMenu'
return 0
fi
>&3 printf -- '%s\n' 'Not installing SSH in ZFSBootMenu'
return 1
}
function configure_ssh_in_zbm () {
#2.11
get_dropbear_hooks
customize_dropbear_hooks
paru_install 'mkinitcpio-nfs-utils' 'dropbear' 'mkinitcpio-dropbear'
in_file_in_array_insert_n_at_the_end '/etc/zfsbootmenu/mkinitcpio.conf' 'HOOKS' 'net'
in_file_in_array_insert_n_at_the_end '/etc/zfsbootmenu/mkinitcpio.conf' 'HOOKS' 'dropbear'
for key_type in 'dss' 'ecdsa' 'ed25519' 'rsa'; do
dropbearkey -t "${key_type}" -f '/etc/dropbear/dropbear_'"${key_type}"'_host_key'
done
set_pub_keys '/etc/dropbear/root_key' "${ARCHZBM_SSH_AUTH_KEYS}"
ensure_ip_in_kcl
}
function add_syslinux_pacman_hook () {
mkdir -p '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main'
git -C '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main' clone 'https://quico.space/quico-os-setup/zbm-syslinux-pacman-hook.git' .
chmod +x '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main/pacman-zbm-syslinux-regen.sh'
ln -s '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main/pacman-zbm-syslinux-regen.sh' '/usr/local/bin/pacman-zbm-syslinux-regen'
ln -s '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main/pacman-zbm-syslinux-regen.hook' '/usr/share/libalpm/hooks/pacman-zbm-syslinux-regen.hook'
}
function add_zbm_pacman_hook () {
mkdir -p '/opt/git/quico.space/quico-os-setup/zbm-regen-pacman-hook/branches/main'
git -C '/opt/git/quico.space/quico-os-setup/zbm-regen-pacman-hook/branches/main' clone 'https://quico.space/quico-os-setup/zbm-regen-pacman-hook.git' .
ln -s '/opt/git/quico.space/quico-os-setup/zbm-regen-pacman-hook/branches/main/pacman-zbm-image-regen.hook' '/usr/share/libalpm/hooks/pacman-zbm-image-regen.hook'
}
function get_disks_with_one_efipart () {
local disks_with_one_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_one_efipart="$(lsblk --output PATH,PARTTYPE --json --tree | jq --raw-output '.[][] | select(.children | length > 0) | select( any (.children[]; (.path | test("^[^[:digit:]]+1")) and (.parttype=="'"${partition_types[gpt_efi]}"'")) and ([select(.children[].parttype=="'"${partition_types[gpt_efi]}"'")] | length == 1) ) | .path')"
if [[ "$(wc -l <<<"${disks_with_one_efipart}")" -eq '1' ]] && [[ "$(wc -c <<<"${disks_with_one_efipart}")" -gt '1' ]]; then
printf -- '%s' "${disks_with_one_efipart}"
return 0
fi
return 1
}
function install_os_in_chroot () {
#2.2
dd if='/dev/zero' of='/swapfile' bs='1M' count='2048'
losetup '/dev/loop9' '/swapfile'
mkswap '/dev/loop9'
swapon '/dev/loop9'
### 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
while ! pacman -S archlinux-keyring --noconfirm --downloadonly; do
sleep 5
done
pacman -S archlinux-keyring --noconfirm
locale-gen
source /etc/locale.conf
keep_initiramfs_root_only_rw #2.3
pacman_dl_parallel #2.4
unleash_makepkg #2.5
add_motd_getting_started_msg #2.6
get_aur_helper #2.7
if [[ "${ARCHZBM_KERNEL_VER}" ]]; then
paru_install 'downgrade'
yes | downgrade --ala-only \
'linux='"${ARCHZBM_KERNEL_VER}" \
'linux-headers='"${ARCHZBM_KERNEL_VER}" \
--ignore always
fi
paru_install 'zfs-dkms' 'zfs-utils' 'jq'
hwclock --systohc
mkinitcpio -P
# Install ZFSBootMenu image
configure_zfsbootmenu #2.9
if we_want_ssh; then #2.10
configure_ssh_in_zbm #2.11
fi
generate-zbm
# Yes, we do this twice so we immediately get a functional backup file
generate-zbm
if [[ "${part_schema}" = 'mbr' ]]; then
paru_install 'rsync'
add_syslinux_pacman_hook
fi
add_zbm_pacman_hook
swapoff '/dev/loop9'
losetup -d '/dev/loop9'
rm '/swapfile'
}
function set_root_pw () {
#3.2
local root_password
if [[ "${ARCHZBM_ROOT_PASSWORD}" ]]; then
root_password="${ARCHZBM_ROOT_PASSWORD}"
else
root_password='password'
fi
printf -- '%s\n' 'root:'"${root_password}" | chpasswd --crypt-method 'SHA512' --root '/mnt'
}
function configure_networking () {
#3.3
local -a dns_addresses ntp_addresses
# Begin network unit file with a default top section
cat > '/mnt/etc/systemd/network/50-wired.network' <<"EOF"
[Match]
Name=en*
[Network]
EOF
# Decide on what comes next in network unit file
if [[ "${ARCHZBM_OS_CLIENT_IP}" ]] || \
[[ "${ARCHZBM_OS_GATEWAY_IP}" ]] || \
[[ "${ARCHZBM_OS_DNS_IP}" ]] || \
[[ "${ARCHZBM_OS_NTP_IP}" ]]; then
cat >> '/mnt/etc/systemd/network/50-wired.network' <<EOF
Address=${ARCHZBM_OS_CLIENT_IP}
Gateway=${ARCHZBM_OS_GATEWAY_IP}
EOF
if [[ "${ARCHZBM_OS_DNS_IP}" ]]; then
mapfile -t dns_addresses < <(<<<"${ARCHZBM_OS_DNS_IP}" tr ',' '\n' | sed '/^$/d')
else
dns_addresses+=('8.8.8.8')
dns_addresses+=('8.8.4.4')
fi
for dns_addr in "${dns_addresses[@]}"; do
cat >> '/mnt/etc/systemd/network/50-wired.network' <<EOF
DNS=${dns_addr}
EOF
done
if [[ "${ARCHZBM_OS_NTP_IP}" ]]; then
mapfile -t ntp_addresses < <(<<<"${ARCHZBM_OS_NTP_IP}" tr ',' '\n' | sed '/^$/d')
for ntp_addr in "${ntp_addresses[@]}"; do
cat >> '/mnt/etc/systemd/network/50-wired.network' <<EOF
NTP=${ntp_addr}
EOF
done
fi
cat >> '/mnt/etc/systemd/network/50-wired.network' <<"EOF"
IPForward=yes
Domains=~.
EOF
else
cat >> '/mnt/etc/systemd/network/50-wired.network' <<"EOF"
DHCP=ipv4
IPForward=yes
[DHCP]
UseDNS=yes
RouteMetric=10
EOF
fi
systemctl enable 'systemd-networkd' --root='/mnt'
systemctl disable 'systemd-networkd-wait-online' --root='/mnt'
}
function configure_sshd () {
#3.4
local pub_key_line
cat >> '/mnt/etc/ssh/sshd_config.d/40-defaults.conf' <<"EOF"
PasswordAuthentication no
PermitRootLogin yes
EOF
while IFS= read -r pub_key_line; do
printf -- '%s\n' "${pub_key_line}" >> '/mnt/root/.ssh/authorized_keys'
done < <(<<<"${ARCHZBM_OS_SSH_AUTH_KEYS}" sed -r -e 's/,,/\n/g')
systemctl enable 'sshd.service' --root='/mnt'
}
function configure_dns () {
#3.5
rm '/mnt/etc/resolv.conf'
ln -s '/run/systemd/resolve/stub-resolv.conf' '/mnt/etc/resolv.conf'
# Optionally you may want /etc/systemd/network/50-wired.network to use
# UseDNS=no and hardcode DNS server(s) here:
# sed -i 's/^#DNS=.*/DNS=1.1.1.1/' /mnt/etc/systemd/resolved.conf
systemctl enable 'systemd-resolved.service' --root='/mnt'
}
function configure_ntp () {
#3.6
systemctl enable 'systemd-timesyncd.service' --root='/mnt'
}
function configure_reflector () {
#3.7
systemctl enable 'reflector.service' 'reflector.timer' --root='/mnt'
}
function configure_zfs () {
#3.8
systemctl enable 'zfs-import-cache.service' 'zfs-mount.service' 'zfs-import.target' 'zfs.target' --root='/mnt'
}
function configure_zfs_mount_gen () {
#3.9
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 set_new_uefi_boot_entries () {
#3.10
declare -a uefi_images
mapfile -t uefi_images < \
<(find '/mnt/efi/EFI/ZBM' -type f -iname '*.efi' -print0 | \
xargs -0 --no-run-if-empty --max-args '1' stat -c '%Y %n' | \
sort -V | \
awk '{print $2}')
if efibootmgr | grep -Piq -- 'ZFSBootMenu'; then
local -a old_uefi_entries
mapfile -t old_uefi_entries < \
<(efibootmgr | \
grep -Pio -- '(?<=^Boot)[^\*[:space:]]+(?=\*? ZFSBootMenu)')
for old_uefi_entry in "${old_uefi_entries[@]}"; do
efibootmgr --bootnum "${old_uefi_entry}" --delete-bootnum &>/dev/null && {
>&3 printf -- '%s\n' \
'EFI boot entry '"${old_uefi_entry}"' deleted.'
}
done
fi
if ! efibootmgr | grep -Piq -- 'ZFSBootMenu'; then
local efi_disks_list
efi_disks_list="$(get_disks_with_one_efipart)"
if grep -Piq -- '^'"${efi_drive}"'$' <<<"${efi_disks_list}"; then
for uefi_image in "${uefi_images[@]}"; do
uefi_image_version="$(basename "${uefi_image%%.EFI}")"
uefi_image_inverted="${uefi_image#/mnt/efi}"
uefi_image_inverted="${uefi_image_inverted//\//\\}"
efibootmgr --disk "${efi_drive}" \
--part 1 \
--create \
--label 'ZFSBootMenu '"${uefi_image_version}" \
--loader "${uefi_image_inverted}" &>/dev/null && {
>&3 printf -- '%s\n' \
'EFI boot entry ZFSBootMenu '"${uefi_image_version}"' added.'
}
done
fi
fi
}
function umount_all () {
#3.11
if [[ "${part_schema}" = 'mbr' ]]; then
umount '/mnt/boot/syslinux'
else
umount '/mnt/efi'
fi
zfs umount -a
zpool export "${zpool_name}"
}
function finalize_os_setup () {
#3.1
set_root_pw #3.2
configure_networking #3.3
if [[ "${ARCHZBM_OS_SSH_AUTH_KEYS}" ]]; then
configure_sshd #3.4
fi
configure_dns #3.5
configure_ntp #3.6
configure_reflector #3.7
configure_zfs #3.8
configure_zfs_mount_gen #3.9
if [[ "${part_schema}" = 'gpt' ]]; then
set_new_uefi_boot_entries #3.10
fi
umount_all #3.11
}
function main () {
if [[ "${#@}" -gt '0' ]]; then
arg_parse "${@}"
fi
if we_are_changerooted; then
install_os_in_chroot #2.2
else
no_kernel_update_in_iso #1.1
set_ntp #1.2
resize_cow_space #1.3
update_pacman_db #1.4
install_pkgs 'jq' #1.5
install_zfs #1.6
uefi_or_bios #1.7
load_settings_file #1.8
setup_zpool #1.9
mount_system #1.10
copy_zpool_cache #1.11
install_archlinux #1.12
gen_fstab #1.13
set_hostname #1.14
set_locale #1.15
add_zfs_hook_to_initramfs #1.16
set_initramfs_build_list #1.17
add_zfs_files_to_new_os #1.18
enter_chroot #2.1
# We're done in chroot
finalize_os_setup #3.1
fi
}
main "${@}"