gentoo-install/scripts/utils.sh

448 lines
11 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# shellcheck source=./scripts/protection.sh
source "$GENTOO_INSTALL_REPO_DIR/scripts/protection.sh" || exit 1
function elog() {
echo "[+] $*"
}
function einfo() {
echo "[+] $*"
}
function ewarn() {
echo "[!] $*" >&2
}
function eerror() {
echo "error: $*" >&2
}
function die() {
eerror "$*"
[[ -v GENTOO_INSTALL_REPO_SCRIPT_PID && $$ -ne $GENTOO_INSTALL_REPO_SCRIPT_PID ]] \
&& kill "$GENTOO_INSTALL_REPO_SCRIPT_PID"
exit 1
}
# Prints an error with file:line info of the nth "stack frame".
# 0 is this function, 1 the calling function, 2 its parent, and so on.
function die_trace() {
local idx="${1:-0}"
shift
echo "${BASH_SOURCE[$((idx + 1))]}:${BASH_LINENO[$idx]}: error: ${FUNCNAME[$idx]}: $*" >&2
exit 1
}
function for_line_in() {
while IFS="" read -r line || [[ -n $line ]]; do
"$2" "$line"
done <"$1"
}
function flush_stdin() {
local empty_stdin
# Unused variable is intentional.
# shellcheck disable=SC2034
while read -r -t 0.01 empty_stdin; do true; done
}
function ask() {
local response
while true; do
flush_stdin
read -r -p "$* (Y/n) " response \
|| die "Error in read"
case "${response,,}" in
'') return 0 ;;
y|yes) return 0 ;;
n|no) return 1 ;;
*) continue ;;
esac
done
}
function try() {
local response
local cmd_status
local prompt_parens="(Shell/retry/abort/continue/print)"
# Outer loop, allows us to retry the command
while true; do
# Try command
"$@"
cmd_status="$?"
if [[ $cmd_status != 0 ]]; then
echo " * Command failed: \$ $*"
echo "Last command failed with exit code $cmd_status"
# Prompt until input is valid
while true; do
echo -n "Specify next action $prompt_parens "
flush_stdin
read -r response \
|| die "Error in read"
case "${response,,}" in
''|s|shell)
echo "You will be prompted for action again after exiting this shell."
/bin/bash --init-file <(echo "init_bash")
;;
r|retry) continue 2 ;;
a|abort) die "Installation aborted" ;;
c|continue) return 0 ;;
p|print) echo "\$ $*" ;;
*) ;;
esac
done
fi
return
done
}
function countdown() {
echo -n "$1" >&2
local i="$2"
while [[ $i -gt 0 ]]; do
echo -n "$i " >&2
i=$((i - 1))
sleep 1
done
echo >&2
}
function download_stdout() {
wget --quiet --https-only --secure-protocol=PFS -O - -- "$1"
}
function download() {
wget --quiet --https-only --secure-protocol=PFS --show-progress -O "$2" -- "$1"
}
function get_blkid_field_by_device() {
local blkid_field="$1"
local device="$2"
blkid -g -c /dev/null \
|| die "Error while executing blkid"
partprobe &>/dev/null
local val
val="$(blkid -c /dev/null -o export "$device")" \
|| die "Error while executing blkid '$device'"
val="$(grep -- "^$blkid_field=" <<< "$val")" \
|| die "Could not find $blkid_field=... in blkid output"
val="${val#"$blkid_field="}"
echo -n "$val"
}
function get_blkid_uuid_for_id() {
local dev
dev="$(resolve_device_by_id "$1")" \
|| die "Could not resolve device with id=$dev"
local uuid
uuid="$(get_blkid_field_by_device 'UUID' "$dev")" \
|| die "Could not get UUID from blkid for device=$dev"
echo -n "$uuid"
}
function get_device_by_blkid_field() {
local blkid_field="$1"
local field_value="$2"
blkid -g -c /dev/null \
|| die "Error while executing blkid"
type partprobe &>/dev/null && partprobe &>/dev/null
local dev
dev="$(blkid -c /dev/null -o export -t "$blkid_field=$field_value")" \
|| die "Error while executing blkid to find $blkid_field=$field_value"
dev="$(grep DEVNAME <<< "$dev")" \
|| die "Could not find DEVNAME=... in blkid output"
dev="${dev#"DEVNAME="}"
echo -n "$dev"
}
function get_device_by_partuuid() {
if [[ -e "/dev/disk/by-partuuid/$1" ]]; then
echo -n "/dev/disk/by-partuuid/$1"
else
get_device_by_blkid_field 'PARTUUID' "$1"
fi
}
function get_device_by_uuid() {
if [[ -e "/dev/disk/by-uuid/$1" ]]; then
echo -n "/dev/disk/by-uuid/$1"
else
get_device_by_blkid_field 'UUID' "$1"
fi
}
function cache_lsblk_output() {
CACHED_LSBLK_OUTPUT="$(lsblk --all --path --pairs --output NAME,PTUUID,PARTUUID)" \
|| die "Error while executing lsblk to cache output"
}
function get_device_by_ptuuid() {
local ptuuid="${1,,}"
local dev
if [[ -v CACHED_LSBLK_OUTPUT && -n "$CACHED_LSBLK_OUTPUT" ]]; then
dev="$CACHED_LSBLK_OUTPUT"
else
dev="$(lsblk --all --path --pairs --output NAME,PTUUID,PARTUUID)" \
|| die "Error while executing lsblk to find PTUUID=$ptuuid"
fi
dev="$(grep "ptuuid=\"$ptuuid\" partuuid=\"\"" <<< "${dev,,}")" \
|| die "Could not find PTUUID=... in lsblk output"
dev="${dev%'" ptuuid='*}"
dev="${dev#'name="'}"
echo -n "$dev"
}
function uuid_to_mduuid() {
local mduuid="${1,,}"
mduuid="${mduuid//-/}"
mduuid="${mduuid:0:8}:${mduuid:8:8}:${mduuid:16:8}:${mduuid:24:8}"
echo -n "$mduuid"
}
function get_device_by_mdadm_uuid() {
local mduuid
mduuid="$(uuid_to_mduuid "$1")" \
|| die "Could not resolve mduuid from uuid=$1"
local dev
dev="$(mdadm --examine --scan)" \
|| die "Error while executing mdadm to find array with UUID=$mduuid"
dev="$(grep "uuid=$mduuid" <<< "${dev,,}")" \
|| die "Could not find UUID=... in mdadm output"
dev="${dev%'metadata='*}"
dev="${dev#'array'}"
dev="${dev#"${dev%%[![:space:]]*}"}"
dev="${dev%"${dev##*[![:space:]]}"}"
echo -n "$dev"
}
function get_device_by_luks_name() {
echo -n "/dev/mapper/$1"
}
function create_resolve_entry() {
local id="$1"
local type="$2"
local arg="${3,,}"
DISK_ID_TO_RESOLVABLE[$id]="$type:$arg"
}
function create_resolve_entry_device() {
local id="$1"
local dev="$2"
DISK_ID_TO_RESOLVABLE[$id]="device:$dev"
}
# Returns the basename of the device, if its path starts with /dev/disk/by-id/
function shorten_device() {
echo -n "${1#/dev/disk/by-id/}"
}
# Return matching device from /dev/disk/by-id/ if possible,
# otherwise return the parameter unchanged.
function canonicalize_device() {
given_dev="$(realpath "$1")"
for dev in /dev/disk/by-id/*; do
if [[ "$(realpath "$dev")" == "$given_dev" ]]; then
echo -n "$dev"
return 0
fi
done
echo -n "$1"
}
function resolve_device_by_id() {
local id="$1"
[[ -v DISK_ID_TO_RESOLVABLE[$id] ]] \
|| die "Cannot resolve id='$id' to a block device (no table entry)"
local type="${DISK_ID_TO_RESOLVABLE[$id]%%:*}"
local arg="${DISK_ID_TO_RESOLVABLE[$id]#*:}"
local dev
case "$type" in
'partuuid') dev=$(get_device_by_partuuid "$arg") ;;
'ptuuid') dev=$(get_device_by_ptuuid "$arg") ;;
'uuid') dev=$(get_device_by_uuid "$arg") ;;
'mdadm') dev=$(get_device_by_mdadm_uuid "$arg") ;;
'luks') dev=$(get_device_by_luks_name "$arg") ;;
'device') dev="$arg" ;;
*) die "Cannot resolve '$type:$arg' to device (unknown type)"
esac
canonicalize_device "$dev"
}
function load_or_generate_uuid() {
local uuid
local uuid_file="$UUID_STORAGE_DIR/$1"
if [[ -e $uuid_file ]]; then
uuid="$(cat "$uuid_file")"
else
uuid="$(uuidgen -r)"
mkdir -p "$UUID_STORAGE_DIR"
echo -n "$uuid" > "$uuid_file"
fi
echo -n "$uuid"
}
# Parses named arguments and stores them in the associative array `arguments`.
# If given, the associative array `known_arguments` must contain a list of arguments
# prefixed with + (mandatory) or ? (optional). "at least one of" can be expressed by +a|b|c.
function parse_arguments() {
local key
local value
local a
for a in "$@"; do
key="${a%%=*}"
value="${a#*=}"
if [[ $key == "$a" ]]; then
extra_arguments+=("$a")
continue
fi
arguments[$key]="$value"
done
declare -A allowed_keys
if [[ -v known_arguments ]]; then
local m
for m in "${known_arguments[@]}"; do
case "${m:0:1}" in
'+')
m="${m:1}"
local has_opt=false
local m_opt
# Splitting is intentional here
# shellcheck disable=SC2086
for m_opt in ${m//|/ }; do
allowed_keys[$m_opt]=true
if [[ -v arguments[$m_opt] ]]; then
has_opt=true
fi
done
[[ $has_opt == "true" ]] \
|| die_trace 2 "Missing mandatory argument $m=..."
;;
'?')
allowed_keys[${m:1}]=true
;;
*) die_trace 2 "Invalid start character in known_arguments, in argument '$m'" ;;
esac
done
for a in "${!arguments[@]}"; do
[[ -v allowed_keys[$a] ]] \
|| die_trace 2 "Unknown argument '$a'"
done
fi
}
# $1: program
# $2: checkfile
function has_program() {
local program="$1"
local checkfile="$2"
if [[ -z "$checkfile" ]]; then
type "$program" &>/dev/null \
|| return 1
elif [[ "${checkfile:0:1}" == "/" ]]; then
[[ -e "$checkfile" ]] \
|| return 1
else
type "$checkfile" &>/dev/null \
|| return 1
fi
return 0
}
function check_wanted_programs() {
local missing_required=()
local missing_wanted=()
local tuple
local program
local checkfile
for tuple in "$@"; do
program="${tuple%%=*}"
checkfile=""
[[ "$tuple" == *=* ]] \
&& checkfile="${tuple##*=}"
if ! has_program "${program#"?"}" "$checkfile"; then
if [[ "$program" == "?"* ]]; then
missing_wanted+=("${program#"?"}")
else
missing_required+=("$program")
fi
fi
done
[[ "${#missing_required[@]}" -eq 0 && "${#missing_wanted[@]}" -eq 0 ]] \
&& return
if [[ "${#missing_required[@]}" -gt 0 ]]; then
elog "The following programs are required for the installer to work, but are currently missing on your system:" >&2
elog " ${missing_required[*]}" >&2
fi
if [[ "${#missing_wanted[@]}" -gt 0 ]]; then
elog "Missing optional programs:" >&2
elog " ${missing_wanted[*]}" >&2
fi
if type pacman &>/dev/null; then
declare -A pacman_packages
pacman_packages=(
[ntpd]=ntp
[zfs]=""
)
elog "Detected pacman package manager."
if ask "Do you want to install all missing programs automatically?"; then
local packages
local need_zfs=false
for program in "${missing_required[@]}" "${missing_wanted[@]}"; do
[[ "$program" == "zfs" ]] \
&& need_zfs=true
if [[ -v "pacman_packages[$program]" ]]; then
# Assignments to the empty string are explcitly ignored,
# as for example zfs needs to be handeled separately.
[[ -n "${pacman_packages[$program]}" ]] \
&& packages+=("${pacman_packages[$program]}")
else
packages+=("$program")
fi
done
pacman -Sy "${packages[@]}"
if [[ "$need_zfs" == true ]]; then
elog "On an Arch live-stick you need the archzfs repository and some tools and modifications to use zfs."
elog "There is an automated installer available at https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init."
if ask "Do you want to automatically download and execute this zfs installation script?"; then
curl -s "https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init" | bash
fi
fi
return
fi
fi
if [[ "${#missing_required[@]}" -gt 0 ]]; then
die "Aborted installer because of missing required programs."
else
ask "Continue without recommended programs?"
fi
}