diff --git a/usr/bin/tik b/usr/bin/tik index 7ddfb13..e2924ac 100755 --- a/usr/bin/tik +++ b/usr/bin/tik @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: Copyright 2023-2024 SUSE LLC # SPDX-FileCopyrightText: Copyright 2023-2024 Richard Brown +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens # Define variables # Style notes @@ -12,8 +13,10 @@ tik_log=~/tik.log tik_dir=/usr/lib/tik tik_module="tik" -# Read libraries +# Common helpers used by tik and modules . ${tik_dir}/lib/tik-functions +# Core installer logic used only by tik and phase scripts +. ${tik_dir}/lib/tik-core # Start logging exec 2> >(exec tee -i -a "${tik_log}" >&2) @@ -26,8 +29,8 @@ if [[ $1 == "--debug" ]]; then fi # Check if graphical display is available -XDG_SESSION="${XDG_SESSION_TYPE:=unspecified}" -if [ $XDG_SESSION_TYPE = "wayland" ] || [ $XDG_SESSION_TYPE = "x11" ] ; then +XDG_SESSION_TYPE="${XDG_SESSION_TYPE:=unspecified}" +if [ "${XDG_SESSION_TYPE}" = "wayland" ] || [ "${XDG_SESSION_TYPE}" = "x11" ] ; then gui=true else gui=false @@ -42,6 +45,9 @@ if [ ! -d "${TIK_IMG_DIR}" ]; then error "${TIK_IMG_DIR} does not exist" fi +# Determine name of to-be-installed system +tik_detect_identity + cleanup() { retval=$? log "[STOP][${retval}] $0" @@ -58,16 +64,21 @@ cleanup() { } trap cleanup EXIT +# Run pre modules +tik_init_phase_modules "pre" load_modules "pre" load_modules "pre" "custom" +# Select installation disk and image, then deploy image to disk get_disk get_img dump_image "${TIK_INSTALL_IMAGE}" "${TIK_INSTALL_DEVICE}" reread_partitiontable +# Run post modules +tik_init_phase_modules "post" load_modules "post" load_modules "post" "custom" wipe_keyfile -set_boot_target +set_boot_target \ No newline at end of file diff --git a/usr/lib/tik/lib/tik-core b/usr/lib/tik/lib/tik-core new file mode 100644 index 0000000..4fd3de3 --- /dev/null +++ b/usr/lib/tik/lib/tik-core @@ -0,0 +1,394 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2025 SUSE LLC +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens + +# Helper functions for tik-core +. ${tik_dir}/lib/tik-core-helper + +tik_detect_identity() { + # establish TIK_OS_NAME + TIK_CRYPT_MAPPER. + # Priority: + # 0. Config provided => keep it + # 1. Selfdeploy (no images found) => read /etc/os-release + # 2. One image => try read os-release from image; else name fallback + # 3. Multiple images => if all match => use that; else fallback to TIK now + + # If config already provided identity, prefer it + if [ -n "${TIK_OS_NAME}" ]; then + if [ -n "${TIK_CRYPT_MAPPER}" ]; then + export TIK_OS_NAME + export TIK_CRYPT_MAPPER + return 0 + fi + + # Only OS name provided -> derive ID + mapper from sanitized name + tik_set_identity_from_name "${TIK_OS_NAME}" + return 0 + fi + + local imgs="" + local file_type + local img_meta + local img_filename + local count=0 + + for file_type in '*.raw.xz' '*.raw'; do + for img_meta in $(cd "${TIK_IMG_DIR}" && (stat --printf="%n\t%s\n" ${file_type} 2>/dev/null | tr ' ' ":")); do + img_filename="$(echo "${img_meta}" | cut -f1 -d:)" + imgs="${imgs} ${img_filename}" + count=$((count + 1)) + done + done + + if [ -n "${TIK_INSTALL_IMAGE}" ]; then + # If config pins an image, treat as single-image identity source. + tik_set_identity_for_image "${TIK_INSTALL_IMAGE}" + return 0 + fi + + if [ "${count}" -eq 0 ]; then + tik_set_identity_for_selfdeploy + return 0 + fi + + if [ "${count}" -eq 1 ]; then + tik_set_identity_for_image ${imgs} + return 0 + fi + + if tik_read_info_from_images ${imgs}; then + return 0 + fi + + tik_set_identity_unknown + return 0 +} + +get_disk() { + tik_volid="TIKINSTALL" + local disk_id="by-id" + local disk_size + local disk_device + local disk_device_by_id + local disk_meta + local disk_list + local device_array + local list_items + local blk_opts="-p -n -r -o NAME,SIZE,TYPE" + local message + local blk_opts_plus_label="${blk_opts},LABEL" + local tik_install_disk_part + local part_meta + local part_count + local part_size + local part_info + local part_fs + local blk_opts_part_info="${blk_opts_plus_label},FSTYPE" + local usb_match_1="usb" + local usb_match_2=":0" + + tik_install_disk_part=$( + eval lsblk "${blk_opts_plus_label}" | \ + tr -s ' ' ":" | \ + grep ":${tik_volid}" | \ + cut -f1 -d: + ) + + for disk_meta in $( + eval lsblk "${blk_opts}" | grep -E "disk|raid" | tr ' ' ":" + ); do + disk_size=$(echo "${disk_meta}" | cut -f2 -d:) + if [[ "${disk_size}" == "0B" ]]; then + continue + fi + disk_device="$(echo "${disk_meta}" | cut -f1 -d:)" + part_count=0 + part_info="" + for part_meta in $( + eval lsblk "${blk_opts_part_info}" | grep -E "${disk_device}.+part.+" | tr ' ' ":" + ); do + part_count=$(expr $part_count + 1) + part_size=$(echo "${part_meta}" | cut -f2 -d:) + part_fs=$(echo "${part_meta}" | cut -f5 -d:) + if [ -n "${part_info}" ]; then + part_info="${part_info}," + fi + if [ -n "${part_fs}" ]; then + part_info="${part_info}${part_fs}(${part_size})" + else + part_info="${part_info}unknown(${part_size})" + fi + done + if [[ ${part_count} -eq 0 ]]; then + part_info="none" + fi + if [[ "${tik_install_disk_part}" == "${disk_device}"* ]]; then + continue + fi + if [[ ${disk_device} =~ ^/dev/fd ]]; then + continue + fi + if [[ ${disk_device} =~ ^/dev/zram ]]; then + continue + fi + disk_device_by_id=$( + get_persistent_device_from_unix_node "${disk_device}" "${disk_id}" + ) + if [[ ( "${TIK_ALLOW_USB_INSTALL_DEVICES}" -ne 1 ) && ( "${disk_device_by_id}" == *"${usb_match_1}"* || "${disk_device_by_id}" == *"${usb_match_2}"* ) ]]; then + continue + fi + if [ -n "${disk_device_by_id}" ]; then + disk_device=${disk_device_by_id} + fi + list_items="${list_items} $(basename ${disk_device}) ${disk_size} ${part_count} ${part_info}" + disk_list="${disk_list} $(basename ${disk_device}) ${disk_size}" + done + + if [ -n "${TIK_INSTALL_DEVICE}" ]; then + local device=${TIK_INSTALL_DEVICE} + local device_meta + local device_size + if [ ! -e "${device}" ]; then + local no_dev="Given device ${device} does not exist." + error "${no_dev}" + fi + if [ ! -b "${device}" ]; then + local no_block_dev="Given device ${device} is not a block special." + error "${no_block_dev}" + fi + device_meta=$( + eval lsblk "${blk_opts}" "${device}" |\ + grep -E "disk|raid" | tr ' ' ":" + ) + device_size=$(echo "${device_meta}" | cut -f2 -d:) + list_items="$(basename ${device}) ${device_size}" + disk_list="$(basename ${device}) ${device_size}" + message="[get_disk] tik installation device set to to: ${device}" + log "${message}" + fi + + if [ -z "${list_items}" ]; then + local no_device_text="No device(s) for installation found." + error "${no_device_text}" + fi + + if [ -n "${disk_list}" ]; then + local count=0 + local device_index=0 + for entry in ${disk_list}; do + if [ $((count % 2)) -eq 0 ]; then + device_array[${device_index}]=${entry} + device_index=$((device_index + 1)) + fi + count=$((count + 1)) + done + if [ "${device_index}" -eq 1 ]; then + TIK_INSTALL_DEVICE="/dev/disk/${disk_id}/${device_array[0]}" + if [ ! -e "${TIK_INSTALL_DEVICE}" ]; then + TIK_INSTALL_DEVICE="/dev/${device_array[0]}" + fi + else + d --list --column=Disk --column=Size --column=Partitions --column=Filesystems --width=1050 --height=340 --title="Select A Disk" --text="Select the disk to install the operating system to. Make sure any important documents and files have been backed up.\n" ${list_items} + TIK_INSTALL_DEVICE="/dev/disk/${disk_id}/${result}" + if [ ! -e "${TIK_INSTALL_DEVICE}" ]; then + TIK_INSTALL_DEVICE="/dev/${result}" + fi + fi + fi +} + +get_img() { + local list_items + local message + local img_meta + local img_list + local img_array + local file_type + + for file_type in '*.raw.xz' '*.raw'; do + for img_meta in $(cd "${TIK_IMG_DIR}" && (stat --printf="%n\t%s\n" ${file_type} 2>/dev/null | tr ' ' ":")); do + img_filename="$(echo "${img_meta}" | cut -f1 -d:)" + img_size="$(echo "${img_meta}" | cut -f2 -d:)" + list_items="${list_items} ${img_filename} ${img_size}" + done + done + + if [ -n "${TIK_INSTALL_IMAGE}" ]; then + local img=${TIK_INSTALL_IMAGE} + local img_size + if [ ! -e "${img}" ]; then + local no_img="Given image ${img} does not exist." + error "${no_img}" + fi + if [ ! -s "${img}" ]; then + local empty_img="Given image ${img} is empty." + error "${empty_img}" + fi + img_meta=$( + eval cd "${TIK_IMG_DIR}" && (stat --printf="%n\t%s\n" "${img}" | tr ' ' ":") + ) + img_filename="$(echo "${img_meta}" | cut -f1 -d:)" + img_size="$(echo "${img_meta}" | cut -f2 -d:)" + list_items="${list_items} ${img_filename} ${img_size}" + message="[get_disk] tik installation image set to to: ${img}" + log "${message}" + fi + + if [ -z "${list_items}" ]; then + TIK_INSTALL_IMAGE='TIK_SELFDEPLOY' + return 0 + fi + + img_list=${list_items} + if [ -n "${img_list}" ]; then + local count=0 + local img_index=0 + for entry in ${img_list}; do + if [ $((count % 2)) -eq 0 ]; then + img_array[${img_index}]=${entry} + img_index=$((img_index + 1)) + fi + count=$((count + 1)) + done + if [ "${img_index}" -eq 1 ]; then + TIK_INSTALL_IMAGE="${img_array[0]}" + else + d --list --column=Image --column=Size --title="Select A Image" --text="Select the operating system image to install.\n" ${list_items} + TIK_INSTALL_IMAGE="${result}" + fi + fi + + # If identity was TIK due to multi-image mismatch, finalize it now from the selected image. + if [ "${TIK_INSTALL_IMAGE}" != "TIK_SELFDEPLOY" ]; then + if [ "${TIK_OS_NAME}" = "TIK" ] || [ -z "${TIK_CRYPT_MAPPER}" ]; then + tik_set_identity_for_image "${TIK_INSTALL_IMAGE}" + fi + fi +} + +reread_partitiontable() { + log "[reread_partitiontable] Re-reading partition table" + sleep 3 + prun /usr/sbin/blockdev --rereadpt "${TIK_INSTALL_DEVICE}" + sleep 3 +} + +dump_image() { + local image_source_files=$1 + local image_target=$2 + + d --question --no-wrap --title="Begin Installation?" --text="Once the installation begins the changes to the selected disk are irreversible.\n\nProceeding will fully erase the disk.\n\nContinue with installation?" + + case "${image_source_files}" in + *.raw.xz) + dump_image_dd "${image_source_files}" "${image_target}" + ;; + *.raw) + dump_image_repart_image "${image_source_files}" "${image_target}" + ;; + TIK_SELFDEPLOY) + dump_image_repart_self "${image_target}" + ;; + *) + error "invalid image type provided" + esac +} + +set_boot_target() { + local efipartnum + + if [ "${debug}" == "1" ]; then + log "[debug] Not setting EFI boot target" + elif [ -n "${efi_already_set}" ]; then + log "[set_boot_target] boot target already set, not setting again" + else + prun-opt /usr/sbin/efibootmgr -B -L "openSUSE Boot Manager" + prun-opt /usr/sbin/efibootmgr -B -L "${TIK_OS_NAME} Boot Manager" + prun /usr/sbin/efibootmgr -O + + # TIK_ESP_PART is already set during mounting + efipartnum=$(lsblk "${TIK_ESP_PART}" -p -n -r -o PARTN) + prun /usr/sbin/efibootmgr -c -L "${TIK_OS_NAME} Boot Manager" -d "${TIK_INSTALL_DEVICE}" -l "\EFI\systemd\shim.efi" -p ${efipartnum} + log "[set_boot_target] $(prun /usr/sbin/efibootmgr)" + efi_already_set=1 + fi +} + +tik_init_phase_modules() { + local phase=$1 + local dir + TIK_CURRENT_PHASE="${phase}" + TIK_TOTAL_MODULES=0 + TIK_CURRENT_MODULE_INDEX=0 + + for dir in "${tik_dir}/modules/${phase}" "${TIK_CUSTOM_DIR}/modules/${phase}"; do + if [ -d "${dir}" ]; then + for f in "${dir}"/*; do + [ -f "${f}" ] || continue + TIK_TOTAL_MODULES=$((TIK_TOTAL_MODULES + 1)) + done + fi + done + + case $phase in + "pre") TIK_PROGRESS_TITLE="Preparing Installation" ;; + "post") TIK_PROGRESS_TITLE="Finishing Installation" ;; + *) TIK_PROGRESS_TITLE="Installation" ;; + esac + + export TIK_CURRENT_PHASE + export TIK_PROGRESS_TITLE + + log "[tik_init_phase_modules] phase=${phase} total_modules=${TIK_TOTAL_MODULES}" +} + +load_modules() { + local phase=$1 + local module_dir + + if [[ $2 = "custom" ]]; then + module_dir=$TIK_CUSTOM_DIR/modules/$phase + else + module_dir=$tik_dir/modules/$phase + fi + + if [ -n "$(ls -A "$module_dir" 2>/dev/null)" ]; then + for f in "$module_dir"/*; do + [ -f "$f" ] || continue + TIK_CURRENT_MODULE_INDEX=$((TIK_CURRENT_MODULE_INDEX + 1)) + tik_module="$f" + log "[START] $f (phase=${phase} module_index=${TIK_CURRENT_MODULE_INDEX}/${TIK_TOTAL_MODULES})" + . "$f" + log "[STOP] $f" + + if [ -n "${TIK_TOTAL_MODULES}" ] && [ "${TIK_TOTAL_MODULES}" -gt 0 ] && \ + [ "${TIK_CURRENT_MODULE_INDEX}" -eq "${TIK_TOTAL_MODULES}" ]; then + log "[load_modules] last module of phase '${phase}' completed, closing progress" + tik_close_progress + tik_cleanup_mounts + fi + done + fi + tik_module="tik" +} + +wipe_keyfile() { + log "[wipe_keyfile] Deleting keyfile ${tik_keyfile}" + + local crypt_part="${TIK_CRYPT_PART}" + + if [ -z "${crypt_part}" ]; then + probe_partitions "${TIK_INSTALL_DEVICE}" "crypto_LUKS" + crypt_part="${probedpart}" + fi + + if [ -n "${crypt_part}" ] && [ -n "${tik_keyfile}" ] && [ -f "${tik_keyfile}" ]; then + prun /usr/bin/systemd-cryptenroll --unlock-key-file="${tik_keyfile}" --wipe-slot=0 "${crypt_part}" + else + log "[wipe_keyfile] no LUKS partition or keyfile found, skipping slot wipe" + fi + + prun-opt /usr/bin/rm "${tik_keyfile}" + prun-opt keyctl revoke "${tik_keyid}" + prun-opt keyctl reap +} diff --git a/usr/lib/tik/lib/tik-core-helper b/usr/lib/tik/lib/tik-core-helper new file mode 100644 index 0000000..fb4e6a9 --- /dev/null +++ b/usr/lib/tik/lib/tik-core-helper @@ -0,0 +1,350 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2025 SUSE LLC +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens + +tik_sanitize_os_id() { + # output: sanitized id (lowercase, underscores, letters only; digits removed) + local raw="$1" + local s + + s="$(echo "${raw}" | tr '[:upper:]' '[:lower:]')" + s="$(echo "${s}" | sed -E 's/[[:space:]-]+/_/g')" + s="$(echo "${s}" | sed -E 's/[0-9]+//g')" + s="$(echo "${s}" | sed -E 's/[^a-z_]+/_/g')" + s="$(echo "${s}" | sed -E 's/_+/_/g; s/^_+//; s/_+$//')" + + echo "${s}" +} + +tik_set_identity_from_name() { + local pretty="$1" + local sid + + [ -n "${pretty}" ] || pretty="TIK" + + sid="$(tik_sanitize_os_id "${pretty}")" + [ -n "${sid}" ] || sid="cr" + + TIK_OS_NAME="${pretty}" + export TIK_OS_NAME + + TIK_OS_ID="${sid}" + export TIK_OS_ID + + TIK_CRYPT_MAPPER="${sid}_root" + export TIK_CRYPT_MAPPER +} + +tik_read_info_from_root() { + local osr="/etc/os-release" + [ -f "${osr}" ] || return 1 + + TIK_DETECTED_PRETTY_NAME="$(. "${osr}" 2>/dev/null; echo "${PRETTY_NAME}")" + TIK_DETECTED_ID="$(. "${osr}" 2>/dev/null; echo "${ID}")" + + [ -n "${TIK_DETECTED_PRETTY_NAME}" ] || return 1 + [ -n "${TIK_DETECTED_ID}" ] || return 1 + return 0 +} + +tik_read_info_from_image() { + local image_path="$1" + local mnt + local osr + + TIK_DETECTED_PRETTY_NAME="" + TIK_DETECTED_ID="" + + mnt="$(mktemp -d /tmp/tik-dissect.XXXXXXXXXX)" + osr="${mnt}/etc/os-release" + + # If this takes too long or fails, we just give up and fall back. + prun-opt /usr/bin/timeout 1s /usr/bin/systemd-dissect --quiet --mount "${image_path}" "${mnt}" + if [ "${retval}" != "0" ]; then + prun-opt /usr/bin/rmdir "${mnt}" + return 1 + fi + + if [ -f "${osr}" ]; then + TIK_DETECTED_PRETTY_NAME="$(. "${osr}" 2>/dev/null; echo "${PRETTY_NAME}")" + TIK_DETECTED_ID="$(. "${osr}" 2>/dev/null; echo "${ID}")" + fi + + prun-opt /usr/bin/umount "${mnt}" + prun-opt /usr/bin/rmdir "${mnt}" + + [ -n "${TIK_DETECTED_PRETTY_NAME}" ] || return 1 + [ -n "${TIK_DETECTED_ID}" ] || return 1 + return 0 +} + +tik_set_identity() { + local pretty="$1" + local raw_id="$2" + local sid + + sid="$(tik_sanitize_os_id "${raw_id}")" + if [ -z "${sid}" ]; then + sid="cr" + fi + + TIK_OS_NAME="${pretty}" + export TIK_OS_NAME + + TIK_OS_ID="${sid}" + export TIK_OS_ID + + TIK_CRYPT_MAPPER="${TIK_OS_ID}_root" + export TIK_CRYPT_MAPPER +} + +tik_set_identity_unknown() { + TIK_OS_NAME="TIK" + export TIK_OS_NAME + + TIK_OS_ID="cr" + export TIK_OS_ID + + TIK_CRYPT_MAPPER="cr_root" + export TIK_CRYPT_MAPPER +} + +tik_set_identity_for_selfdeploy() { + if tik_read_info_from_root; then + tik_set_identity "${TIK_DETECTED_PRETTY_NAME}" "${TIK_DETECTED_ID}" + return 0 + fi + tik_set_identity_unknown + return 0 +} + +tik_set_identity_for_image() { + local img="$1" + local base + local token + + if tik_read_info_from_image "${TIK_IMG_DIR}/${img}"; then + tik_set_identity "${TIK_DETECTED_PRETTY_NAME}" "${TIK_DETECTED_ID}" + return 0 + fi + + base="$(basename "${img}")" + base="${base%.raw}" + base="${base%.xz}" + + token="$(echo "${base}" | sed -nE 's/^tik-osimage-([^-\.\ ]+).*/\1/p')" + if [ -n "${token}" ]; then + TIK_OS_NAME="${token}" + export TIK_OS_NAME + + TIK_OS_ID="$(tik_sanitize_os_id "${token}")" + [ -n "${TIK_OS_ID}" ] || TIK_OS_ID="cr" + export TIK_OS_ID + + TIK_CRYPT_MAPPER="${TIK_OS_ID}_root" + export TIK_CRYPT_MAPPER + return 0 + fi + + tik_set_identity_unknown + return 0 +} + +tik_read_info_from_images() { + # If multiple images are available and all have the same ID & PRETTY_NAME, use that. + # Returns 0 if identity was set, otherwise 1. + local img + local first_pretty="" + local first_id="" + local ok=1 + + for img in "$@"; do + if ! tik_read_info_from_image "${TIK_IMG_DIR}/${img}"; then + ok=0 + break + fi + if [ -z "${first_pretty}" ]; then + first_pretty="${TIK_DETECTED_PRETTY_NAME}" + first_id="${TIK_DETECTED_ID}" + else + if [ "${TIK_DETECTED_PRETTY_NAME}" != "${first_pretty}" ] || [ "${TIK_DETECTED_ID}" != "${first_id}" ]; then + ok=0 + break + fi + fi + done + + if [ "${ok}" = "1" ] && [ -n "${first_pretty}" ] && [ -n "${first_id}" ]; then + tik_set_identity "${first_pretty}" "${first_id}" + return 0 + fi + return 1 +} + +get_persistent_device_from_unix_node() { + local unix_device=$1 + local schema=$2 + local node + local persistent_name + node=$(basename "${unix_device}") + for persistent_name in /dev/disk/"${schema}"/*; do + if [ "$(basename "$(readlink "${persistent_name}")")" = "${node}" ]; then + if [[ ${persistent_name} =~ ^/dev/disk/"${schema}"/nvme-eui ]]; then + # Filter out nvme-eui nodes as they are not descriptive to the user + continue + fi + echo "${persistent_name}" + return + fi + done + warn "Could not find ${schema} representation of ${node}. Using original device ${unix_device}" + echo "${unix_device}" +} + +probe_partitions() { + local probe_dir=/var/lib/tik/probe + local filesystem_type=$2 + local filematch=$3 + local device=$1 + local mountops + local part + + if [[ "${filesystem_type}" == "btrfs" ]]; then + mountops="-o compress=zstd:1" + fi + + prun /usr/bin/mkdir -p ${probe_dir}/mnt + probedpart="" + + for part in $(lsblk ${device} -p -n -r -o ID-LINK,FSTYPE | tr -s ' ' ";" | grep ";${filesystem_type}" | cut -d\; -f1); do + if [ -z "${filematch}" ]; then + log "[probe_partitions] no file match required" + # Fallback to unix device in order to fix issue with USB devices + probedpart="$(/usr/bin/readlink -f "/dev/disk/by-id/""${part}")" + log "[probe_partitions] Partition ${probedpart} found" + break + else # Check if ${filematch} exists + # Fallback to unix device in order to fix issue with USB devices + part="$(/usr/bin/readlink -f "/dev/disk/by-id/""${part}")" + prun /usr/bin/mount ${mountops} ${part} "${probe_dir}/mnt" + if [ -f ${probe_dir}/mnt/${filematch} ]; then + log "[probe_partitions] File ${filematch} found" + # Fallback to unix device in order to fix issue with USB devices + probedpart="${part}" + log "[probe_partitions] Partition ${probedpart} found" + prun-opt /usr/bin/umount ${probe_dir}/mnt + break + fi + prun-opt /usr/bin/umount ${probe_dir}/mnt + fi + done + + prun /usr/bin/rmdir ${probe_dir}/mnt +} + +create_keyfile() { + tik_keyfile=$(prun mktemp /tmp/tik.XXXXXXXXXX) + log "[create_keyfile] Creating keyfile ${tik_keyfile}" + /usr/bin/base64 -w 0 /dev/urandom | head -c 1k | prun tee "${tik_keyfile}" + prun /usr/bin/chmod 400 "${tik_keyfile}" + tik_keyid=$(prun cat "${tik_keyfile}" | prun keyctl padd user cryptenroll @u) +} + +dump_image_dd() { + local image_source_files=$1 + local image_target=$2 + log "[dump_image_dd] deploying ${TIK_IMG_DIR}/${image_source_files}" + (xzcat "${TIK_IMG_DIR}/${image_source_files}" | pv -f -F "# %b copied in %t %r" | prun /usr/bin/dd of="${image_target}" bs=64k) 2>&1 | d --progress --title="Installing ${TIK_OS_NAME}" --pulsate --auto-close --no-cancel --width=400 + prun /usr/bin/sync | d --progress --title="Syncing" --pulsate --auto-close --no-cancel --width=400 +} + +dump_image_repart_image() { + local image_source_files=$1 + local image_target=$2 + local success=0 + local max_attempts=5 + local attempt_num=1 + + create_keyfile + log "[dump_image_repart_image] deploying ${TIK_IMG_DIR}/${image_source_files}" + + while [ ${success} = 0 ] && [ ${attempt_num} -lt ${max_attempts} ]; do + prun-opt systemd-repart --no-pager --pretty=0 --empty=force --dry-run=no --key-file="${tik_keyfile}" --image="${TIK_IMG_DIR}/${image_source_files}" --image-policy=root=unprotected "${image_target}" > >(d --progress --title="Installing ${TIK_OS_NAME}" --text="Deploying OS Image" --pulsate --auto-close --no-cancel --width=400) + if [ ${retval} -eq 0 ]; then + success=1 + else + log "[dump_image_repart_image] systemd-repart attempt ${attempt_num} failed. Trying again..." + sleep 1 + attempt_num=$(( attempt_num + 1 )) + fi + done + if [ ${success} = 1 ]; then + log "[dump_image_repart_image] systemd-repart succeeded after ${attempt_num} attempts" + else + error "systemd-repart failed" + fi +} + +dump_image_repart_self() { + local image_target=$1 + create_keyfile + prun-opt rm -rf /etc/fstab.repart + log "[dump_image_repart_self] self-deploying" + prun systemd-repart --no-pager --pretty=0 --empty=force --dry-run=no --key-file="${tik_keyfile}" --generate-fstab=/etc/fstab.repart "${image_target}" > >(d --progress --title="Installing ${TIK_OS_NAME}" --text="Deploying OS Image" --pulsate --auto-close --no-cancel --width=400) +} + +tik_target_unmount() { + [ "${TIK_MOUNTED_TARGET}" = "1" ] || return 0 + + log "[tik_target_unmount] Unmounting target system" + + # Unmount everything mounted under the target mount root + tik_unmount "${TIK_ROOT_MNT}" + + # As a last resort, unmount the root mount lazily if still busy + prun-opt /usr/bin/umount -l "${TIK_ROOT_MNT}" || true + + # Close luks mapping + if [ -n "${TIK_CRYPT_PART}" ]; then + local mapper_name="${TIK_OPENED_MAPPER:-${TIK_CRYPT_MAPPER:-cr_root}}" + log "[tik_target_unmount] closing encrypted root for ${TIK_CRYPT_PART} (mapper=${mapper_name})" + prun-opt /usr/sbin/cryptsetup luksClose "${mapper_name}" + fi + + TIK_MOUNTED_POINTS="" + TIK_MOUNTED_TARGET=0 + export TIK_MOUNTED_TARGET + + TIK_OPENED_MAPPER="" + export TIK_OPENED_MAPPER +} + +tik_cleanup_mounts() { + if [ -n "${TIK_MOUNTED_POINTS}" ] || [ "${TIK_MOUNTED_TARGET}" = "1" ]; then + log "[tik_cleanup_mounts] mounts detected after phase '${TIK_CURRENT_PHASE}', unmounting" + tik_target_unmount + fi +} + +tik_close_progress() { + if [ -n "${TIK_PROGRESS_PID}" ]; then + log "[tik_close_progress] stopping progress monitor pid=${TIK_PROGRESS_PID}" + + if [ -n "${TIK_PIPE}" ] && [ -p "${TIK_PIPE}" ]; then + echo "100" > "${TIK_PIPE}" 2>/dev/null || true + fi + + kill -TERM -- "-${TIK_PROGRESS_PID}" 2>/dev/null || true + wait "${TIK_PROGRESS_PID}" 2>/dev/null || true + + kill -KILL -- "-${TIK_PROGRESS_PID}" 2>/dev/null || true + + unset TIK_PROGRESS_PID + fi + + if [ -n "${TIK_PIPE}" ] && [ -p "${TIK_PIPE}" ]; then + log "[tik_close_progress] removing progress pipe ${TIK_PIPE}" + rm -f "${TIK_PIPE}" 2>/dev/null || true + fi + unset TIK_PIPE +} diff --git a/usr/lib/tik/lib/tik-functions b/usr/lib/tik/lib/tik-functions index c243342..233f1fe 100644 --- a/usr/lib/tik/lib/tik-functions +++ b/usr/lib/tik/lib/tik-functions @@ -1,10 +1,14 @@ # SPDX-License-Identifier: MIT -# SPDX-FileCopyrightText: Copyright 2023-2024 SUSE LLC -# SPDX-FileCopyrightText: Copyright 2023-2024 Richard Brown +# SPDX-FileCopyrightText: Copyright 2023-2025 SUSE LLC +# SPDX-FileCopyrightText: Copyright 2023-2025 Richard Brown +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens . /usr/lib/tik/lib/cenity -log(){ +# Helper functions for tik-functions +. ${tik_dir}/lib/tik-functions-helper + +log() { if $logging; then echo "[${tik_module}][$(date +"%Y%m%d-%T")][LOG] $*" 1>&2 fi @@ -21,7 +25,7 @@ error() { exit 1 } -d(){ +d() { while true do retval=0 @@ -47,7 +51,7 @@ d(){ done } -d_opt(){ +d_opt() { retval=0 if $gui; then result="$(zenity "$@")" || retval=$? @@ -79,418 +83,243 @@ prun() { fi } -get_persistent_device_from_unix_node() { - local unix_device=$1 - local schema=$2 - local node - local persistent_name - node=$(basename "${unix_device}") - for persistent_name in /dev/disk/"${schema}"/*; do - if [ "$(basename "$(readlink "${persistent_name}")")" = "${node}" ];then - if [[ ${persistent_name} =~ ^/dev/disk/"${schema}"/nvme-eui ]]; then - # Filter out nvme-eui nodes as they are not descriptive to the user - continue - fi - echo "${persistent_name}" - return - fi - done - warn "Could not find ${schema} representation of ${node}. Using original device ${unix_device}" - echo "${unix_device}" -} +tik_progress_step() { + # Create progress UI when first used in this phase + [ -n "${TIK_PROGRESS_PID}" ] || tik_prepare_progress_pipe + + local message=$1 + local module_percent=$2 -probe_partitions() { - local probe_dir=/var/lib/tik/probe - local filesystem_type=$2 - local filematch=$3 - local device=$1 - local mountops - local part - if [[ "${filesystem_type}" == "btrfs" ]]; then - mountops="-o compress=zstd:1" + if [ -z "${TIK_PIPE}" ]; then + return 0 fi - prun /usr/bin/mkdir -p ${probe_dir}/mnt - probedpart="" - for part in $(lsblk ${device} -p -n -r -o ID-LINK,FSTYPE|tr -s ' ' ";"|grep ";${filesystem_type}"|cut -d\; -f1); do - if [ -z ${filematch} ]; then - log "[probe_partitions] no file match required" - # Fallback to unix device in order to fix issue with USB devices - probedpart="$(/usr/bin/readlink -f "/dev/disk/by-id/""${part}")" - log "[probe_partitions] Partition ${probedpart} found" - else # Check if ${filematch} exists - # Fallback to unix device in order to fix issue with USB devices - part="$(/usr/bin/readlink -f "/dev/disk/by-id/""${part}")" - prun /usr/bin/mount ${mountops} ${part} "${probe_dir}/mnt" - if [ -f ${probe_dir}/mnt/${filematch} ]; then - log "[probe_partitions] File ${filematch} found" - # Fallback to unix device in order to fix issue with USB devices - probedpart="${part}" - log "[probe_partitions] Partition ${probedpart} found" - if grep -q 'PRETTY_NAME="openSUSE MicroOS"' ${probe_dir}/mnt/${filematch} && [ -f ${probe_dir}/mnt/usr/bin/gnome-shell ]; then - # Found legacy Aeon, activate easter egg - log "Legacy Aeon Install FOUND" - legacy_aeon=1 - fi - fi - prun-opt /usr/bin/umount ${probe_dir}/mnt - fi - done - prun /usr/bin/rmdir ${probe_dir}/mnt + + local overall_percent + + if [ -n "${TIK_TOTAL_MODULES}" ] && [ "${TIK_TOTAL_MODULES}" -gt 0 ] && \ + [ -n "${TIK_CURRENT_MODULE_INDEX}" ] && [ "${TIK_CURRENT_MODULE_INDEX}" -gt 0 ]; then + # Map module-local 0–100% into global 0–100% based on module index + # overall = ((index-1)*100 + module_percent) / total_modules + local base=$(( (TIK_CURRENT_MODULE_INDEX - 1) * 100 )) + local num=$(( base + module_percent )) + overall_percent=$(( num / TIK_TOTAL_MODULES )) + else + overall_percent=${module_percent} + fi + + echo "# ${message}" > "${TIK_PIPE}" + echo "${overall_percent}" > "${TIK_PIPE}" } -get_disk() { - # Volume label for the tik install media must be set to "TIKINSTALL" to filter it out from the device list - tik_volid="TIKINSTALL" - local disk_id="by-id" - local disk_size - local disk_device - local disk_device_by_id - local disk_meta - local disk_list - local device_array - local list_items - local blk_opts="-p -n -r -o NAME,SIZE,TYPE" - local message - local blk_opts_plus_label="${blk_opts},LABEL" - local tik_install_disk_part - local part_meta - local part_count - local part_size - local part_info - local part_fs - local blk_opts_part_info="${blk_opts_plus_label},FSTYPE" - local usb_match_1="usb" - local usb_match_2=":0" - - tik_install_disk_part=$( - eval lsblk "${blk_opts_plus_label}" | \ - tr -s ' ' ":" | \ - grep ":${tik_volid}" | \ - cut -f1 -d: - ) - - for disk_meta in $( - eval lsblk "${blk_opts}" | grep -E "disk|raid" | tr ' ' ":" - );do - disk_size=$(echo "${disk_meta}" | cut -f2 -d:) - if [[ "${disk_size}" == "0B" ]]; then - # ignore disks with no size, e.g. empty SD card readers - continue - fi - disk_device="$(echo "${disk_meta}" | cut -f1 -d:)" - # find partitions and info for this disk - part_count=0 - part_info="" - for part_meta in $( - eval lsblk "${blk_opts_part_info}" | grep -E "${disk_device}.+part.+" | tr ' ' ":" - );do - part_count=$(expr $part_count + 1) - part_size=$(echo "${part_meta}" | cut -f2 -d:) - part_fs=$(echo "${part_meta}" | cut -f5 -d:) - if [ -n "${part_info}" ]; then - part_info="${part_info}," - fi - if [ -n "${part_fs}" ]; then - part_info="${part_info}${part_fs}(${part_size})" - else - part_info="${part_info}unknown(${part_size})" +tik_unmount() { + local prefix="$1" + local listvar="${2:-TIK_MOUNTED_POINTS}" + local mp + + [ -n "${prefix}" ] || return 0 + + local -n _ml="${listvar}" + + log "[tik_unmount] unmounting ${prefix} (list=${listvar})" + while IFS= read -r mp; do + [ -z "${mp}" ] && continue + case "${mp}" in + "${prefix}"|${prefix}/*) + prun-opt /usr/bin/umount "${mp}" + ;; + esac + done <<< "${_ml}" + + tik_untrack_mountpoint "${prefix}" "${listvar}" +} + +tik_mount() { + # Mount a device and track it. + # If the 2nd argument is an absolute path, mount there. + # Otherwise, mount at a subfolder of TIK_ROOT_MNT. + local dev="$1" + local path="$2" + local opts="$3" + local fstype="$4" + local listvar="${5:-TIK_MOUNTED_POINTS}" + local overlay_root_prefix="$6" + + [ -n "${dev}" ] || error "tik_mount: missing device" + [ -n "${path}" ] || error "tik_mount: missing target/subpath" + + local target="" + + case "${path}" in + /*) + target="${path}" + ;; + *) + [ -n "${TIK_ROOT_MNT}" ] || error "tik_mount: TIK_ROOT_MNT not set" + if echo "${path}" | grep -qE '(^|/)\.\.($|/)'; then + error "tik_mount: unsafe subpath '${path}'" fi - done - if [[ ${part_count} -eq 0 ]]; then - part_info="none" - fi - if [[ "${tik_install_disk_part}" == "${disk_device}"* ]]; then - # ignore install source device - continue - fi - if [[ ${disk_device} =~ ^/dev/fd ]];then - # ignore floppy disk devices - continue - fi - if [[ ${disk_device} =~ ^/dev/zram ]];then - # ignore zram devices - continue - fi - disk_device_by_id=$( - get_persistent_device_from_unix_node "${disk_device}" "${disk_id}" - ) - if [[ ( "${TIK_ALLOW_USB_INSTALL_DEVICES}" -ne 1 ) && ( "{$disk_device_by_id}" == *"${usb_match_1}"* || "{$disk_device_by_id}" == *"${usb_match_2}"* ) ]]; then - # ignore USB devices if TIK_ALLOW_USB_INSTALL_DEVICES not set in config - continue - fi - if [ -n "${disk_device_by_id}" ];then - disk_device=${disk_device_by_id} + target="${TIK_ROOT_MNT%/}/${path}" + ;; + esac + + case "${target}" in + /*) ;; + *) error "tik_mount: target must be absolute (got '${target}')" ;; + esac + + log "[tik_mount] mounting ${dev} at ${target} opts='${opts}' fstype='${fstype}' (list=${listvar})" + prun /usr/bin/mkdir -p "${target}" + + if [ "${fstype}" = "none" ] && (tik_is_opt_set "${opts}" "bind" || tik_is_opt_set "${opts}" "rbind"); then + if tik_is_opt_set "${opts}" "rbind"; then + prun /usr/bin/mount --rbind "${dev}" "${target}" + else + prun /usr/bin/mount --bind "${dev}" "${target}" fi - list_items="${list_items} $(basename ${disk_device}) ${disk_size} ${part_count} ${part_info}" - disk_list="${disk_list} $(basename ${disk_device}) ${disk_size}" - done - if [ -n "${TIK_INSTALL_DEVICE}" ];then - # install device overwritten by config. - local device=${TIK_INSTALL_DEVICE} - local device_meta - local device_size - if [ ! -e "${device}" ];then - local no_dev="Given device ${device} does not exist." - error "${no_dev}" + if tik_is_opt_set "${opts}" "ro" || tik_is_opt_set "${opts}" "nosuid" || tik_is_opt_set "${opts}" "nodev" || tik_is_opt_set "${opts}" "noexec"; then + prun /usr/bin/mount -o "remount,${opts}" "${target}" fi - if [ ! -b "${device}" ];then - local no_block_dev="Given device ${device} is not a block special." - error "${no_block_dev}" + + elif [ "${fstype}" = "overlay" ]; then + local newopts="${opts}" + if [ -n "${overlay_root_prefix}" ]; then + newopts="$(tik_rewrite_overlay_opts "${opts}" "${overlay_root_prefix}")" fi - device_meta=$( - eval lsblk "${blk_opts}" "${device}" |\ - grep -E "disk|raid" | tr ' ' ":" - ) - device_size=$(echo "${device_meta}" | cut -f2 -d:) - # this case is not shown in manual selection, threfore we don't need partition info - list_items="$(basename ${device}) ${device_size}" - disk_list="$(basename ${device}) ${device_size}" - message="tik installation device set to to: ${device}" - log "${message}" - fi - if [ -z "${list_items}" ];then - local no_device_text="No device(s) for installation found." - error "${no_device_text}" - fi - if [ -n "${disk_list}" ];then - local count=0 - local device_index=0 - for entry in ${disk_list};do - if [ $((count % 2)) -eq 0 ];then - device_array[${device_index}]=${entry} - device_index=$((device_index + 1)) - fi - count=$((count + 1)) - done - if [ "${device_index}" -eq 1 ];then - # one single disk device found, use it - # Add back full path to it - TIK_INSTALL_DEVICE="/dev/disk/${disk_id}/${device_array[0]}" - - # Fallback to unix device in case by-id does not exist - # see get_persistent_device_from_unix_node, it does fallback like this. - if [ ! -e "${TIK_INSTALL_DEVICE}" ]; then - TIK_INSTALL_DEVICE="/dev/${device_array[0]}" + prun /usr/bin/mount -t overlay overlay -o "${newopts}" "${target}" + + else + if [ -n "${fstype}" ]; then + if [ -n "${opts}" ]; then + prun /usr/bin/mount -t "${fstype}" -o "${opts}" "${dev}" "${target}" + else + prun /usr/bin/mount -t "${fstype}" "${dev}" "${target}" fi else - # manually select from storage list - d --list --column=Disk --column=Size --column=Partitions --column=Filesystems --width=1050 --height=340 --title="Select A Disk" --text="Select the disk to install the operating system to. Make sure any important documents and files have been backed up.\n" ${list_items} - # Add back full path to it - TIK_INSTALL_DEVICE="/dev/disk/${disk_id}/${result}" - - # Fallback to unix device in case by-id does not exist - # see get_persistent_device_from_unix_node, it does fallback like this. - if [ ! -e "${TIK_INSTALL_DEVICE}" ]; then - TIK_INSTALL_DEVICE="/dev/${result}" + if [ -n "${opts}" ]; then + prun /usr/bin/mount -o "${opts}" "${dev}" "${target}" + else + prun /usr/bin/mount "${dev}" "${target}" fi fi fi + + tik_track_mountpoint "${target}" "${listvar}" } -get_img() { - local list_items - local message - local img_meta - local img_item - local img_list - local img_array - local file_type - # Images are assumed to be named to the following standard - # $ProductName.$Version.raw.xz for block devices - # $ProductName.$Version.raw for systemd-repart images - # Any extraneous fields may confuse tik's detection, selection and presentation of the image to the user - for file_type in '*.raw.xz' '*.raw';do - for img_meta in $(cd $TIK_IMG_DIR && (stat --printf="%n\t%s\n" ${file_type} | tr ' ' ":"));do - img_filename="$(echo $img_meta | cut -f1 -d:)" - img_size="$(echo $img_meta | cut -f2 -d:)" - list_items="${list_items} ${img_filename} ${img_size}" +tik_target_mount() { + # $1 = mapper override + # $2 = mode: required | optional + local mapper_override="$1" + local mode="${2:-required}" + + [ "${TIK_MOUNTED_TARGET}" = "1" ] && return 0 + + tik_mount_prepare + + # open encrypted device(s) + local crypt_parts="" + crypt_parts="$( + lsblk "${TIK_INSTALL_DEVICE}" -p -n -r -o ID-LINK,FSTYPE | tr -s ' ' ";" | grep ";crypto_LUKS" | cut -d\; -f1 + )" + + if [ -n "${crypt_parts}" ]; then + local crypt_part + for crypt_part in ${crypt_parts}; do + tik_crypt_open "${mapper_override}" "${mode}" "${crypt_part}" + case "$?" in + 0) ;; + *) + return "$?" + ;; + esac + + # If we opened something, check whether it looks like the root filesystem + if [ -n "${TIK_ROOT_DEV}" ]; then + if tik_is_root_partition "${TIK_ROOT_DEV}"; then + break + fi + + # Not root -> close and try the next encrypted partition + if [ -n "${TIK_OPENED_MAPPER}" ]; then + prun-opt /usr/sbin/cryptsetup luksClose "${TIK_OPENED_MAPPER}" + fi + TIK_CRYPT_PART="" + export TIK_CRYPT_PART + TIK_ROOT_DEV="" + export TIK_ROOT_DEV + TIK_OPENED_MAPPER="" + export TIK_OPENED_MAPPER + fi done - done - if [ -n "${TIK_INSTALL_IMAGE}" ];then - # install image overwritten by config. - local img=${TIK_INSTALL_IMAGE} - local img_meta - local img_size - if [ ! -e "${img}" ];then - local no_img="Given image ${img} does not exist." - error "${no_img}" - fi - if [ ! -s "${img}" ];then - local empty_img="Given image ${img} is empty." - error "${empty_img}" - fi - img_meta=$( - eval cd $TIK_IMG_DIR && (stat --printf="%n\t%s\n" $img | tr ' ' ":") - ) - img_filename="$(echo $img_meta | cut -f1 -d:)" - img_size="$(echo $img_meta | cut -f2 -d:)" - list_items="${list_items} ${img_filename} ${img_size}" - message="tik installation image set to to: ${img}" - log "${message}" - fi - if [ -z "${list_items}" ];then - TIK_INSTALL_IMAGE='TIK_SELFDEPLOY' fi - img_list=${list_items} - if [ -n "${img_list}" ];then - local count=0 - local img_index=0 - for entry in ${img_list};do - if [ $((count % 2)) -eq 0 ];then - img_array[${img_index}]=${entry} - img_index=$((img_index + 1)) + # No enrypted root partition found -> search all partitions for a root filesystem + if [ -z "$TIK_ROOT_DEV" ]; then + local part + local parts="" + parts="$( + lsblk "${TIK_INSTALL_DEVICE}" -p -n -r -o NAME,TYPE | awk '$2=="part"{print $1}' + )" + + for part in ${parts}; do + if tik_is_root_partition "${part}"; then + TIK_ROOT_DEV="${part}" + export TIK_ROOT_DEV + break fi - count=$((count + 1)) done - if [ "${img_index}" -eq 1 ];then - # one single disk image found, use it - TIK_INSTALL_IMAGE="${img_array[0]}" - else - # manually select from storage list - d --list --column=Image --column=Size --title="Select A Image" --text="Select the operating system image to install.\n" ${list_items} - TIK_INSTALL_IMAGE="$result" + # Also no root partition found + if [ "${mode}" = "required" ]; then + error "No existing system found" fi + return 1 fi -} + export TIK_ROOT_DEV -reread_partitiontable() { - # We've just done a lot to $TIK_INSTALL_DEVICE and it's probably a good idea to make sure the partition table is clearly read so tools like dracut dont get confused. - log "[reread_partitiontable] Re-reading partition table" - # sleeps added to let the system finish doing partition stuff before rereading, and then a few secs to finish reading the table. Honestly, I'm not sure the 2nd one is needed, but doesn't hurt, so *shrug* - sleep 3 - prun /usr/sbin/blockdev --rereadpt ${TIK_INSTALL_DEVICE} - sleep 3 -} + log "[tik_target_mount] root device is ${TIK_ROOT_DEV}" -create_keyfile() { - # Even if there's no partitions using encryption, systemd-repart will need a key-file defined for the --key-file parameter. - tik_keyfile=$(prun mktemp /tmp/tik.XXXXXXXXXX) - log "[create_keyfile] Creating keyfile ${tik_keyfile}" - /usr/bin/base64 -w 0 /dev/urandom | head -c 1k | prun tee ${tik_keyfile} - prun /usr/bin/chmod 400 ${tik_keyfile} - # Add the key to roots cryptenroll keyring, and record tik_keyid for either interactions later - tik_keyid=$(prun cat ${tik_keyfile} | prun keyctl padd user cryptenroll @u) -} + # mount root + tik_mount_root "${TIK_ROOT_DEV}" "${TIK_ROOT_MNT}" -wipe_keyfile() { - # We made a keyfile and need to clean it up at the end of the installation, possibly wiping it from the newly installed device - log "[wipe_keyfile] Deleting keyfile ${tik_keyfile}" - probe_partitions ${TIK_INSTALL_DEVICE} "crypto_LUKS" - if [ -n "${probedpart}" ]; then - # Assumes Slot 0 is always by the key-file at enrolment - prun /usr/bin/systemd-cryptenroll --unlock-key-file=${tik_keyfile} --wipe-slot=0 ${probedpart} - fi - # We're done with the key-file, so remove it from the filesystem and keyring - prun /usr/bin/rm ${tik_keyfile} - prun-opt keyctl revoke ${tik_keyid} - prun-opt keyctl reap -} + # Assemble fstab in a writable location + local assembled + assembled="$(tik_fstab_assemble "${TIK_ROOT_MNT}")" + TIK_TARGET_FSTAB="${assembled}" + export TIK_TARGET_FSTAB + TIK_ASSEMBLED_FSTAB="${assembled}" + export TIK_ASSEMBLED_FSTAB -dump_image() { - local image_source_files=$1 - local image_target=$2 + # mount everything described by assembled fstab + tik_mount_from_fstab "${TIK_ROOT_MNT}" - d --question --no-wrap --title="Begin Installation?" --text="Once the installation begins the changes to the selected disk are irreversible.\n\nProceeding will fully erase the disk.\n\nContinue with installation?" + # Probe ESP + log "[tik_target_mount] probing for ESP partition on ${TIK_INSTALL_DEVICE}" + probe_partitions "${TIK_INSTALL_DEVICE}" "vfat" + if [ -n "${probedpart}" ]; then + TIK_ESP_PART="${probedpart}" + export TIK_ESP_PART + log "[tik_target_mount] found ESP ${TIK_ESP_PART}" + else + log "[tik_target_mount] no ESP found (continuing)" + TIK_ESP_PART="" + export TIK_ESP_PART + fi - case "${image_source_files}" in - *.raw.xz) - dump_image_dd ${image_source_files} ${image_target} - ;; - *.raw) - dump_image_repart_image ${image_source_files} ${image_target} - ;; - TIK_SELFDEPLOY) - dump_image_repart_self ${image_target} - ;; - *) - error "invalid image type provided" - esac -} + # bind pseudo fs for chroot usage + tik_mount_pseudofs "${TIK_ROOT_MNT}" -dump_image_dd() { - local image_source_files=$1 - local image_target=$2 - log "[dump_image_dd] deploying ${TIK_IMG_DIR}/${image_source_files}" - (xzcat ${TIK_IMG_DIR}/${image_source_files} | pv -f -F "# %b copied in %t %r" | prun /usr/bin/dd of=${image_target} bs=64k) 2>&1 | d --progress --title="Installing ${TIK_OS_NAME}" --pulsate --auto-close --no-cancel --width=400 - prun /usr/bin/sync | d --progress --title="Syncing" --pulsate --auto-close --no-cancel --width=400 -} + log "[tik_target_mount] Target system mounted" -dump_image_repart_image() { - local image_source_files=$1 - local image_target=$2 - local success=0 - local max_attempts=5 - local attempt_num=1 - create_keyfile - log "[dump_image_repart_image] deploying ${TIK_IMG_DIR}/${image_source_files}" - # systemd-repart doesn't always parse the contents of the image perfectly first time, so retry a few times before declaring it a failure - while [ ${success} = 0 ] && [ ${attempt_num} -lt ${max_attempts} ]; do - prun-opt systemd-repart --no-pager --pretty=0 --empty=force --dry-run=no --key-file=${tik_keyfile} --image=${TIK_IMG_DIR}/${image_source_files} --image-policy=root=unprotected ${image_target} > >(d --progress --title="Installing ${TIK_OS_NAME}" --text="Deploying OS Image" --pulsate --auto-close --no-cancel --width=400) - if [ ${retval} -eq 0 ]; then - success=1 - else - # repart couldn't find a root partition - log "[dump_image_repart_image] systemd-repart attempt $attempt_num failed. Trying again..." - sleep 1 - # Increment the attempt counter - attempt_num=$(( attempt_num + 1 )) - fi - done - if [ ${success} = 1 ]; then - log "[dump_image_repart_image] systemd-repart succeeded after $attempt_num attempts" - else - error "systemd-repart failed" - fi + TIK_MOUNTED_TARGET=1 + export TIK_MOUNTED_TARGET } -dump_image_repart_self() { - local image_target=$1 - create_keyfile - log "[dump_image_repart_self] self-deploying" - prun systemd-repart --no-pager --pretty=0 --empty=force --dry-run=no --key-file=${tik_keyfile} --generate-fstab=/etc/fstab.repart ${image_target} > >(d --progress --title="Installing ${TIK_OS_NAME}" --text="Deploying OS Image" --pulsate --auto-close --no-cancel --width=400) -} +tik_write_fstab() { + [ "${TIK_MOUNTED_TARGET}" = "1" ] || error "tik_write_fstab: target system not mounted" -set_boot_target() { - local efipartnum - if [ "${debug}" == "1" ]; then - log "[debug] Not setting EFI boot target" - elif [ -n "${efi_already_set}" ]; then - log "[set_boot_target] boot target already set, not setting again" + # Install assembled fstab into the target if possible + if [ -n "${TIK_ASSEMBLED_FSTAB}" ] && [ -f "${TIK_ASSEMBLED_FSTAB}" ]; then + tik_fstab_install "${TIK_ROOT_MNT}" "${TIK_ASSEMBLED_FSTAB}" || true else - # Cleanup any existing openSUSE boot entries - prun-opt /usr/sbin/efibootmgr -B -L "openSUSE Boot Manager" - # Cleanup any existing ${TIK_OS_NAME} boot entries - prun-opt /usr/sbin/efibootmgr -B -L "${TIK_OS_NAME} Boot Manager" - prun /usr/sbin/efibootmgr -O - log "[set_boot_target] searching for ESP partition containing /EFI/systemd/shim.efi on ${TIK_INSTALL_DEVICE}" - probe_partitions ${TIK_INSTALL_DEVICE} "vfat" "/EFI/systemd/shim.efi" - if [ -z "${probedpart}" ]; then - error "esp partition not found" - fi - efipartnum=$(lsblk ${probedpart} -p -n -r -o PARTN) - log "[set_boot_target] found ESP on ${probedpart}, partition number ${efipartnum}" - prun /usr/sbin/efibootmgr -c -L "${TIK_OS_NAME} Boot Manager" -d ${TIK_INSTALL_DEVICE} -l "\EFI\systemd\shim.efi" -p ${efipartnum} - # Log to show the resulting eficonfig - log "[set_boot_target] $(prun /usr/sbin/efibootmgr)" - efi_already_set=1 + error "tik_write_fstab: no assembled fstab available" fi } - -load_modules() { -local module_dir -if [[ $2 = "custom" ]]; then - module_dir=$TIK_CUSTOM_DIR/modules/$1 -else - module_dir=$tik_dir/modules/$1 -fi -if [ -n "$(ls -A $module_dir)" ]; then -for f in $module_dir/* - do - tik_module="$f" - log "[START] $module_dir/$f" - . $f - log "[STOP] $module_dir/$f" - done -fi -tik_module="tik" -} diff --git a/usr/lib/tik/lib/tik-functions-helper b/usr/lib/tik/lib/tik-functions-helper new file mode 100644 index 0000000..a85e3b1 --- /dev/null +++ b/usr/lib/tik/lib/tik-functions-helper @@ -0,0 +1,640 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2025 SUSE LLC +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens + +tik_prompt_passphrase() { + # Prompt the user for a passphrase + local title="$1" + local cancel_label="${2:-Cancel}" + local passphrase="" + + if $gui; then + passphrase="$(zenity --password --title="${title}" --cancel-label="${cancel_label}")" || true + else + cenity result --password --title="${title}" --cancel-label="${cancel_label}" || true + passphrase="${result}" + fi + + echo -n "${passphrase}" +} + +tik_mount_prepare() { + TIK_ROOT_MNT="${TIK_ROOT_MNT:=/var/lib/tik/root}" + export TIK_ROOT_MNT + prun /usr/bin/mkdir -p "${TIK_ROOT_MNT}" + + TIK_MOUNTED_POINTS="" + TIK_MOUNTED_TARGET=0 + export TIK_MOUNTED_TARGET + + TIK_TARGET_FSTAB="" + export TIK_TARGET_FSTAB + TIK_ASSEMBLED_FSTAB="" + export TIK_ASSEMBLED_FSTAB + + TIK_ESP_PART="" + export TIK_ESP_PART + + TIK_OPENED_MAPPER="" + export TIK_OPENED_MAPPER +} + +tik_is_root_partition() { + local dev="$1" + local fs + + [ -n "${dev}" ] || return 1 + + for fs in btrfs ext4 xfs f2fs; do + probe_partitions "${dev}" "${fs}" "/etc/fstab" + if [ -n "${probedpart}" ]; then + echo "${probedpart}" + return 0 + fi + probe_partitions "${dev}" "${fs}" "/etc/os-release" + if [ -n "${probedpart}" ]; then + echo "${probedpart}" + return 0 + fi + done + return 1 +} + +tik_crypt_open() { + # $1 = mapper override + # $2 = mode: "required" | "optional" + local mapper_override="$1" + local mode="${2:-required}" + local crypt_part="$3" + + TIK_CRYPT_PART="" + TIK_ROOT_DEV="" + + [ -n "${crypt_part}" ] || return 0 + + local crypt_byid="/dev/disk/by-id/${crypt_part}" + local crypt_real="" + + if [ -e "${crypt_byid}" ]; then + crypt_real="$(/usr/bin/readlink -f "${crypt_byid}")" + else + crypt_real="$(/usr/bin/readlink -f "/dev/disk/by-id/${crypt_part}")" + fi + + [ -n "${crypt_real}" ] || crypt_real="${crypt_byid}" + + TIK_CRYPT_PART="${crypt_real}" + export TIK_CRYPT_PART + + local mapper_name="${mapper_override:-${TIK_CRYPT_MAPPER:-cr_root}}" + + log "[tik_crypt_open] encrypted partition detected: ${TIK_CRYPT_PART} (mapper=${mapper_name})" + + if [ -e "/dev/mapper/${mapper_name}" ]; then + # Mapper already exists. Only reuse it if it maps to the partition we are trying to open. + local mapped_dev="" + mapped_dev="$(/usr/sbin/cryptsetup status "${mapper_name}" 2>/dev/null | /usr/bin/awk -F': *' '$1=="device"{print $2; exit}')" + + if [ -n "${mapped_dev}" ]; then + mapped_dev="$(/usr/bin/readlink -f "${mapped_dev}")" + fi + + if [ -n "${mapped_dev}" ] && [ "${mapped_dev}" = "${TIK_CRYPT_PART}" ]; then + log "[tik_crypt_open] mapper ${mapper_name} already open for ${mapped_dev}, reusing" + TIK_ROOT_DEV="/dev/mapper/${mapper_name}" + export TIK_ROOT_DEV + TIK_OPENED_MAPPER="${mapper_name}" + export TIK_OPENED_MAPPER + return 0 + fi + + error "Mapper ${mapper_name} is already open for ${mapped_dev:-unknown}, cannot open ${TIK_CRYPT_PART}." + fi + + if [ -n "${tik_keyfile}" ] && [ -f "${tik_keyfile}" ]; then + prun /usr/sbin/cryptsetup luksOpen --key-file="${tik_keyfile}" "${TIK_CRYPT_PART}" "${mapper_name}" + TIK_ROOT_DEV="/dev/mapper/${mapper_name}" + export TIK_ROOT_DEV + TIK_OPENED_MAPPER="${mapper_name}" + export TIK_OPENED_MAPPER + return 0 + fi + + while true; do + local pw + local cancel_label="Cancel" + + if [ "${mode}" = "optional" ]; then + cancel_label="Skip" + fi + + pw="$(tik_prompt_passphrase "Encrypted partition (${crypt_part}) detected" "${cancel_label}")" + + if [ -z "${pw}" ]; then + if [ "${mode}" = "optional" ]; then + log "[tik_crypt_open] user skipped unlocking" + return 0 + fi + error "Encrypted system detected but no passphrase provided" + fi + + echo -n "${pw}" | prun /usr/sbin/cryptsetup luksOpen "${crypt_byid}" "${mapper_name}" + if [ "${retval}" = "0" ]; then + TIK_ROOT_DEV="/dev/mapper/${mapper_name}" + export TIK_ROOT_DEV + TIK_OPENED_MAPPER="${mapper_name}" + export TIK_OPENED_MAPPER + return 0 + fi + + d --warning --no-wrap --title="Incorrect passphrase" --text="Failed to unlock encrypted partition ${crypt_part}." + done +} + +tik_mountopts_filter() { + local opts="$1" + local ignore_list="$2" + + if [ -z "${opts}" ] || [ "${opts}" = "-" ]; then + echo "${opts}" + return 0 + fi + + # Build ignore tables from ignore_list + local -A drop_exact=() + local -A drop_key=() + local ig + + IFS=',' read -r -a _ignore_arr <<< "${ignore_list}" + for ig in "${_ignore_arr[@]}"; do + ig="${ig#"${ig%%[![:space:]]*}"}" + ig="${ig%"${ig##*[![:space:]]}"}" + [ -n "${ig}" ] || continue + + drop_exact["${ig}"]=1 + + # If ignore entry is "key=" drop any key=... + if [[ "${ig}" == *"=" ]]; then + drop_key["${ig%=}"]=1 + # If ignore entry is "key=value" also mark key for dropping + elif [[ "${ig}" == *"="* ]]; then + drop_key["${ig%%=*}"]=1 + else + drop_key["${ig}"]=1 + fi + done + + local out=() + local o k + IFS=',' read -r -a _opts_arr <<< "${opts}" + for o in "${_opts_arr[@]}"; do + o="${o#"${o%%[![:space:]]*}"}" + o="${o%"${o##*[![:space:]]}"}" + [ -n "${o}" ] || continue + + # Exact ignore match + if [ -n "${drop_exact[${o}]}" ]; then + continue + fi + + # key=value case + if [[ "${o}" == *"="* ]]; then + k="${o%%=*}" + if [ -n "${drop_key[${k}]}" ] || [ -n "${drop_exact[${k}=]}" ]; then + continue + fi + else + # plain key case + if [ -n "${drop_key[${o}]}" ]; then + continue + fi + fi + + out+=("${o}") + done + + if [ "${#out[@]}" -eq 0 ]; then + echo "-" + else + local joined + (IFS=','; joined="${out[*]}"; echo "${joined}") + fi +} + +tik_mountopts_ignore_list() { + echo "${TIK_MOUNTOPTS_IGNORE_LIST:-ro,ro=vfs}" +} + +tik_mountopts_apply_filter() { + local opts="$1" + local ignore_opts + ignore_opts="$(tik_mountopts_ignore_list)" + tik_mountopts_filter "${opts}" "${ignore_opts}" +} + +tik_fstab_get_mount_opts() { + local fstab="$1" + local mountpoint="$2" + local line spec mp fstype opts rest + + [ -f "${fstab}" ] || return 1 + + while IFS= read -r line; do + # strip leading whitespace + line="${line#"${line%%[![:space:]]*}"}" + [ -n "${line}" ] || continue + [[ "${line}" = \#* ]] && continue + + # split into fields + read -r spec mp fstype opts rest <<< "${line}" + [ -n "${spec}" ] || continue + [ -n "${mp}" ] || continue + + if [ "${mp}" = "${mountpoint}" ]; then + echo "${opts}" + return 0 + fi + done < "${fstab}" + + return 1 +} + +tik_fstab_read_entry() { + # Reads a single fstab-like line and outputs 4 fields via globals: + # TIK_FSTAB_SPEC, TIK_FSTAB_MP, TIK_FSTAB_FSTYPE, TIK_FSTAB_OPTS + # Returns: + # 0 = parsed entry + # 1 = skip (blank/comment/invalid) + local line="$1" + local rest + + TIK_FSTAB_SPEC="" + TIK_FSTAB_MP="" + TIK_FSTAB_FSTYPE="" + TIK_FSTAB_OPTS="" + + line="${line#"${line%%[![:space:]]*}"}" + [ -n "${line}" ] || return 1 + [[ "${line}" = \#* ]] && return 1 + + read -r TIK_FSTAB_SPEC TIK_FSTAB_MP TIK_FSTAB_FSTYPE TIK_FSTAB_OPTS rest <<< "${line}" + + [ -n "${TIK_FSTAB_SPEC}" ] || return 1 + [ -n "${TIK_FSTAB_MP}" ] || return 1 + [ -n "${TIK_FSTAB_FSTYPE}" ] || return 1 + + return 0 +} + +tik_mount_root() { + local rootdev=$1 + local mnt=$2 + + # Temporarily mount the root to determine the real mount options + local tmp_mnt + local root_opts + tmp_mnt="$(prun /usr/bin/mktemp -d /tmp/tik-rootprobe.XXXXXXXXXX)" + log "[tik_mount_root] probing mount options for / by temporarily mounting ${rootdev} on ${tmp_mnt}" + + prun /usr/bin/mount -o ro "${rootdev}" "${tmp_mnt}" + + if [ -f "${tmp_mnt}/etc/fstab" ]; then + root_opts="$(tik_fstab_get_mount_opts "${tmp_mnt}/etc/fstab" "/")" + log "[tik_mount_root] probed root mount options: '${root_opts}'" + else + log "[tik_mount_root] no ${tmp_mnt}/etc/fstab found while probing, falling back to default mount options" + root_opts="" + fi + + prun-opt /usr/bin/umount "${tmp_mnt}" + prun-opt /usr/bin/rmdir "${tmp_mnt}" + + if [ -n "${root_opts}" ] && [ "${root_opts}" != "-" ]; then + root_opts="$(tik_mountopts_apply_filter "${root_opts}")" + log "[tik_mount_root] filtered root mount options: '${root_opts}'" + fi + + if [ -n "${root_opts}" ] && [ "${root_opts}" != "-" ]; then + tik_mount "${rootdev}" "${mnt}" "${root_opts}" + else + tik_mount "${rootdev}" "${mnt}" + fi +} + +tik_is_safe_mount() { + local spec=$1 + local mp=$2 + local fstype=$3 + + # Require absolute mountpoints + if [ -z "${mp}" ] || [ "${mp#"/"}" = "${mp}" ]; then + return 1 + fi + + # Basic path traversal guard + if echo "${mp}" | grep -qE '(^|/)\.\.($|/)'; then + return 1 + fi + + # Do not allow overriding the root mount or pseudo filesystems + case "${mp}" in + "/"|"/proc"|"/sys"|"/dev"|"/run"|"/tmp") + return 1 + ;; + esac + + return 0 +} + +tik_rewrite_overlay_opts() { + local opts="$1" + local root="$2" + + opts="$(echo "$opts" | sed -E "s#lowerdir=/#lowerdir=${root}/#g; s#upperdir=/#upperdir=${root}/#g; s#workdir=/#workdir=${root}/#g")" + opts="$(echo "$opts" | sed -E "s#lowerdir=${root}//#lowerdir=${root}/#g; s#upperdir=${root}//#upperdir=${root}/#g; s#workdir=${root}//#workdir=${root}/#g")" + opts="$(echo "$opts" | sed -E "s#lowerdir=(${root}/[^,]*):/#lowerdir=\1:${root}/#g")" + + echo "$opts" +} + +tik_is_opt_set() { + echo ",$1," | grep -q ",$2," +} + +tik_override_mountpoint() { + local fstab=$1 + local mp=$2 + local newline=$3 + + local tmp + tmp="$(prun /usr/bin/mktemp /tmp/tik-fstab.XXXXXXXXXX)" + + local replaced=0 + local line spec cur_mp rest + + { + while IFS= read -r line || [ -n "${line}" ]; do + # Keep comments as-is + if [[ "${line}" =~ ^[[:space:]]*# ]]; then + printf '%s\n' "${line}" + continue + fi + + # If we can't read at least 2 fields, keep as-is + spec="" + cur_mp="" + rest="" + read -r spec cur_mp rest <<< "${line}" + if [ -z "${spec}" ] || [ -z "${cur_mp}" ]; then + printf '%s\n' "${line}" + continue + fi + + # Replace first matching mountpoint + if [ "${replaced}" -eq 0 ] && [ "${cur_mp}" = "${mp}" ]; then + printf '%s\n' "${newline}" + replaced=1 + continue + fi + + printf '%s\n' "${line}" + done < <(prun /usr/bin/cat "${fstab}") + + # If not replaced, append new entry + if [ "${replaced}" -eq 0 ]; then + printf '%s\n' "${newline}" + fi + } | prun /usr/bin/tee "${tmp}" >/dev/null + + prun /usr/bin/cp -a "${tmp}" "${fstab}" + prun /usr/bin/rm -f "${tmp}" +} + +tik_can_write_etc() { + local mnt=$1 + local probe="${mnt}/etc/.tik-write-probe.$$" + prun-opt /usr/bin/sh -c "touch '${probe}' && rm -f '${probe}'" + [ "${retval}" = "0" ] +} + +tik_fstab_assemble() { + local mnt=$1 + + local fstab="${mnt}/etc/fstab" + local fstab_repart="${mnt}/etc/fstab.repart" + local fstab_tik="${mnt}/etc/fstab.tik" + + local assembled + assembled="$(prun /usr/bin/mktemp /tmp/tik-assembled-fstab.XXXXXXXXXX)" + TIK_ASSEMBLED_FSTAB="${assembled}" + export TIK_ASSEMBLED_FSTAB + + # Prefer systemd-repart generated fstab if present, otherwise use existing fstab. + if [ -f "${fstab_repart}" ]; then + log "[tik_fstab_assemble] using ${fstab_repart} as base fstab" + prun /usr/bin/cp -a "${fstab_repart}" "${assembled}" + elif [ -f "${fstab}" ]; then + log "[tik_fstab_assemble] using ${fstab} as base fstab" + prun /usr/bin/cp -a "${fstab}" "${assembled}" + else + log "[tik_fstab_assemble] no base fstab found, leaving assembled fstab empty at ${assembled}" + prun /usr/bin/tee "${assembled}" >/dev/null < ${sicu_pipe} - log "[find_crypt] finding encrypted partition" - probe_partitions ${TIK_INSTALL_DEVICE} "crypto_LUKS" - if [ -z "${probedpart}" ]; then - error "encrypted partition not found" - fi - cryptpart=${probedpart} - log "[find_crypt] found ${cryptpart}" - echo "14" > ${sicu_pipe} -} - -find_esp() { - echo "# Finding ESP partition" > ${sicu_pipe} - log "[find_esp] finding ESP" - probe_partitions ${TIK_INSTALL_DEVICE} "vfat" - if [ -z "${probedpart}" ]; then - error "esp partition not found" - fi - esppart=${probedpart} - log "[find_esp] found ${esppart}" - echo "28" > ${sicu_pipe} -} - -open_partition() { - echo "# Opening ${cryptpart}" > ${sicu_pipe} - log "[open_partition] opening ${cryptpart} and mounting for chroot" - prun /usr/sbin/cryptsetup luksOpen --key-file=${tik_keyfile} ${cryptpart} aeon_root - echo "35" > ${sicu_pipe} - prun /usr/bin/mount -o compress=zstd:1 /dev/mapper/aeon_root ${sicu_dir}/mnt - prun /usr/bin/mount -t proc /proc "${sicu_dir}/mnt/proc" - prun /usr/bin/mount --bind /sys "${sicu_dir}/mnt/sys" - prun /usr/bin/mount -t securityfs securityfs "${sicu_dir}/mnt/sys/kernel/security" - prun /usr/bin/mount -t efivarfs efivarfs "${sicu_dir}/mnt/sys/firmware/efi/efivars" - prun /usr/bin/mount --bind /dev "${sicu_dir}/mnt/dev" - prun /usr/bin/mount --bind /run "${sicu_dir}/mnt/run" - prun /usr/bin/mount --bind /tmp "${sicu_dir}/mnt/tmp" - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/.snapshots /dev/mapper/aeon_root ${sicu_dir}/mnt/.snapshots - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/var /dev/mapper/aeon_root ${sicu_dir}/mnt/var - # Detect whether /etc is overlay else assume it's a T-U 5.0+ later bind mount - if grep -qF 'overlay /etc' ${sicu_dir}/mnt/etc/fstab ; then - etcmountcmd=$(cat ${sicu_dir}/mnt/etc/fstab | grep "overlay /etc" | sed 's/\/sysroot\//${sicu_dir}\/mnt\//g' | sed 's/\/work-etc.*/\/work-etc ${sicu_dir}\/mnt\/etc\//' | sed 's/overlay \/etc overlay/\/usr\/bin\/mount -t overlay overlay -o/') - eval prun "$etcmountcmd" - else - prun /usr/bin/mount -o bind ${sicu_dir}/mnt/etc ${sicu_dir}/mnt/etc - fi - prun /usr/bin/mount ${esppart} ${sicu_dir}/mnt/boot/efi - echo "42" > ${sicu_pipe} -} - sicu() { - echo "# Writing fstab" > ${sicu_pipe} - log "[sicu] Writing fstab" - prun /usr/bin/cat ${sicu_dir}/mnt/etc/fstab.repart | prun tee ${sicu_dir}/mnt/etc/fstab - echo "/etc /etc none bind,x-initrd.mount 0 0" | prun tee -a ${sicu_dir}/mnt/etc/fstab - prun /usr/bin/rm /etc/fstab.repart - echo "56" > ${sicu_pipe} - echo "# Cleaning up tik installer" > ${sicu_pipe} + tik_target_mount "" "required" + tik_write_fstab + + tik_progress_step "Cleaning up installer user" 0 log "[sicu] Deleting tik user" - prun /usr/bin/chroot ${sicu_dir}/mnt userdel -r tik - log "[sicu] Enabling initial-setup" - prun /usr/bin/rm ${sicu_dir}/mnt/var/lib/gdm/block-initial-setup + prun /usr/bin/chroot "${TIK_ROOT_MNT}" userdel -r tik + if [ -e "${TIK_ROOT_MNT}/var/lib/gdm/block-initial-setup" ]; then + log "[sicu] Enabling initial-setup" + prun /usr/bin/rm "${TIK_ROOT_MNT}/var/lib/gdm/block-initial-setup" + fi log "[sicu] Disabling tik autologin" - prun /usr/bin/sed -i 's/DISPLAYMANAGER_AUTOLOGIN="tik"/DISPLAYMANAGER_AUTOLOGIN=""/' ${sicu_dir}/mnt/etc/sysconfig/displaymanager - echo "70" > ${sicu_pipe} -} - -close_partition() { - echo "# Closing ${cryptpart}" > ${sicu_pipe} - log "[close_partition] unmounting and closing ${cryptpart}" - for i in proc dev tmp 'boot/efi' etc var '.snapshots' 'sys/kernel/security' 'sys/firmware/efi/efivars' sys run; do - prun /usr/bin/umount "${sicu_dir}/mnt/$i" - done - prun /usr/bin/umount "${sicu_dir}/mnt" - prun /usr/sbin/cryptsetup luksClose aeon_root - echo "100" > ${sicu_pipe} + prun /usr/bin/sed -i 's/DISPLAYMANAGER_AUTOLOGIN="tik"/DISPLAYMANAGER_AUTOLOGIN=""/' "${TIK_ROOT_MNT}/etc/sysconfig/displaymanager" } -sicu_progress & -find_crypt -find_esp -open_partition sicu -close_partition +tik_progress_step "Installer user cleaned up" 100 diff --git a/usr/lib/tik/modules/post/15-encrypt b/usr/lib/tik/modules/post/15-encrypt index a7b53f0..ee40bb5 100644 --- a/usr/lib/tik/modules/post/15-encrypt +++ b/usr/lib/tik/modules/post/15-encrypt @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: Copyright 2024 SUSE LLC # SPDX-FileCopyrightText: Copyright 2024 Richard Brown +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens # Module does not actually do any encryption, but is intended to finish installation of an encrypted image, such as one deployed via systemd-repart # Module expects to find a single ESP partition (find_esp) and a single LUKS2 partition (find_crypt) on $TIK_INSTALL_DEVICE, upon which it will do the following @@ -18,195 +19,123 @@ # - Remove the temporary key-file and replace it either with TPM enrollment or a user-supplied passphrase (add_key) # It is expected the LUKS2 partition is already encrypted with a key-file in the only populated keyslot. -encrypt_dir=/var/lib/tik/encrypt -encrypt_pipe=/tmp/encryptpipe -if [ ! -d ${encrypt_dir}/mnt ]; then - prun /usr/bin/mkdir -p ${encrypt_dir}/mnt -fi -if [ ! -p ${encrypt_pipe} ]; then - mkfifo ${encrypt_pipe} -fi - -crypt_progress() { - log "[crypt_progress] Monitoring encryption progress" - (tail -f ${encrypt_pipe}) | d --progress --title="Configuring Encryption" --auto-close --no-cancel --width=400 - rm ${encrypt_pipe} - log "[crypt_progress] Encryption progress reached 100%" +generate_recoveryKey() { + tik_progress_step "Generating recovery key" 0 + log "[generate_recoveryKey] generating recovery key" + modhex=('c' 'b' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'n' 'r' 't' 'u' 'v') + mapfile -t raw_key < <(hexdump -v --format '1/1 "%u\n"' -n 32 /dev/random) + [ "${#raw_key[@]}" = 32 ] + key="" + for ((i=0;i<"${#raw_key[@]}";++i)); do + [ "$i" -gt 0 ] && [ "$((i%4))" -eq 0 ] && key="${key}-" + c="${raw_key[i]}" + key="${key}${modhex[$((c>>4))]}${modhex[$((c&15))]}" + done + log "[generate_recoveryKey] adding recovery key to roots sdbootutil user keyring" + logging=false + pkexec keyctl add user sdbootutil ${key} @u + logging=true } -find_crypt() { - echo "# Finding encrypted partition" > ${encrypt_pipe} - log "[find_crypt] finding encrypted partition" - probe_partitions ${TIK_INSTALL_DEVICE} "crypto_LUKS" - if [ -z "${probedpart}" ]; then - error "encrypted partition not found" - fi - cryptpart=${probedpart} - log "[find_crypt] found ${cryptpart}" - echo "14" > ${encrypt_pipe} +display_recoveryKey() { + local defaultmsg="This ${TIK_OS_NAME} system is encrypted and checks its own integrity on every boot\nIn the event of these integrity checks failing, you will need to use the Recovery Key provided below to enter this system\n\nLikely reasons for integrity checks failing include:\n\n• Secure Boot changed from enabled or disabled\n• Boot drive was moved to a different computer\n• Disk partitions were changed\n• Boot loader or initrd were altered unexpectedly\n\nIf you are unaware as to why the system is requesting the recovery key, this systems security may have been compromised\nThe best course of action may be to not unlock the disk until you can determine what changed to require the Recovery Key\n\nThis systems Recovery Key is:\n\n ${key}\n\nPlease save this secret Recovery Key in a secure location\n\n" + local fallbackmsg="In addition to your Passphrase a Recovery Key has been generated:\n\n ${key}\n\nPlease save this secret Recovery Key in a secure location\nIt may be used to regain access to this system if the other Passphrase becomes lost or forgotten\n\n" + local message + [ "${tik_encrypt_mode}" == 0 ] && message=${defaultmsg} + [ "${tik_encrypt_mode}" == 1 ] && message=${fallbackmsg} + log "[display_recoveryKey] displaying recovery key" + logging=false + d --width=500 --height=500 --no-wrap --warning --icon=security-high-symbolic --title="Encryption Recovery Key" --text="${message}You may optionally scan the recovery key off screen:\n$(qrencode ${key} -t UTF8i)\nFor more information please visit https://aeondesktop.org/encrypt" + logging=true + log "[display_recoveryKey] recovery key dialogue dismissed" } -find_esp() { - echo "# Finding encrypted partition" > ${encrypt_pipe} - log "[find_esp] finding ESP" - probe_partitions ${TIK_INSTALL_DEVICE} "vfat" - if [ -z "${probedpart}" ]; then - error "esp partition not found" - fi - esppart=${probedpart} - log "[find_esp] found ${esppart}" - echo "28" > ${encrypt_pipe} -} +configure_encryption() { + tik_target_mount "" "required" -open_partition() { - echo "# Opening ${cryptpart}" > ${encrypt_pipe} - log "[open_partition] opening ${cryptpart} and mounting for chroot" - prun /usr/sbin/cryptsetup luksOpen --key-file=${tik_keyfile} ${cryptpart} aeon_root - echo "35" > ${encrypt_pipe} - prun /usr/bin/mount -o compress=zstd:1 /dev/mapper/aeon_root ${encrypt_dir}/mnt - prun /usr/bin/mount -t proc /proc "${encrypt_dir}/mnt/proc" - prun /usr/bin/mount --bind /sys "${encrypt_dir}/mnt/sys" - prun /usr/bin/mount -t securityfs securityfs "${encrypt_dir}/mnt/sys/kernel/security" - prun /usr/bin/mount -t efivarfs efivarfs "${encrypt_dir}/mnt/sys/firmware/efi/efivars" - prun /usr/bin/mount --bind /dev "${encrypt_dir}/mnt/dev" - prun /usr/bin/mount --bind /run "${encrypt_dir}/mnt/run" - prun /usr/bin/mount --bind /tmp "${encrypt_dir}/mnt/tmp" - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/.snapshots /dev/mapper/aeon_root ${encrypt_dir}/mnt/.snapshots - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/var /dev/mapper/aeon_root ${encrypt_dir}/mnt/var - # Detect whether /etc is overlay else assume it's a T-U 5.0+ later bind mount - if grep -qF 'overlay /etc' ${encrypt_dir}/mnt/etc/fstab ; then - etcmountcmd=$(cat ${encrypt_dir}/mnt/etc/fstab | grep "overlay /etc" | sed 's/\/sysroot\//${encrypt_dir}\/mnt\//g' | sed 's/\/work-etc.*/\/work-etc ${encrypt_dir}\/mnt\/etc\//' | sed 's/overlay \/etc overlay/\/usr\/bin\/mount -t overlay overlay -o/') - eval prun "$etcmountcmd" - else - prun /usr/bin/mount -o bind ${encrypt_dir}/mnt/etc ${encrypt_dir}/mnt/etc - fi - prun /usr/bin/mount ${esppart} ${encrypt_dir}/mnt/boot/efi - echo "42" > ${encrypt_pipe} -} + tik_progress_step "Configuring encryption and boot" 20 + + log "[configure_encryption] configuring cmdline, crypttab, PCR policy, fstab and populating ${TIK_ESP_PART}" + + espUUID="$(lsblk -n -r -o UUID "${TIK_ESP_PART}" | head -n1)" + [ -n "${espUUID}" ] || error "ESP UUID could not be determined for ${TIK_ESP_PART}" + prun /usr/bin/gawk -v espUUID="${espUUID}" -i inplace '$2 == "/boot/efi" { $1 = "UUID="espUUID } { print $0 }' "${TIK_ROOT_MNT}/etc/fstab" -configure_encryption() { - echo "# Writing cmdline, crypttab, and fstab" > ${encrypt_pipe} - log "[configure_encryption] configuring cmdline, crypttab, PCR policy, fstab and populating ${esppart}" - espUUID=$(lsblk -n -r -o UUID ${esppart}) - prun /usr/bin/gawk -v espUUID=$espUUID -i inplace '$2 == "/boot/efi" { $1 = "UUID="espUUID } { print $0 }' ${encrypt_dir}/mnt/etc/fstab # root=UUID= cmdline definition is a hard requirement of sdbootutil for updating predictions - rootUUID=$(lsblk -n -r -o UUID /dev/mapper/aeon_root) - prun /usr/bin/sed -i -e "s,\$, root=UUID=${rootUUID}," ${encrypt_dir}/mnt/etc/kernel/cmdline + rootUUID=$(lsblk -n -r -o UUID "${TIK_ROOT_DEV}") + prun /usr/bin/sed -i -e "s,\$, root=UUID=${rootUUID}," "${TIK_ROOT_MNT}/etc/kernel/cmdline" + # /etc/crypttab is a hard requirement of sdbootutil for updating predictions - cryptUUID=$(lsblk -n -r -d -o UUID ${cryptpart}) - echo "aeon_root UUID=${cryptUUID} none x-initrd.attach" | prun tee ${encrypt_dir}/mnt/etc/crypttab - echo "# Installing boot loader" > ${encrypt_pipe} + cryptUUID=$(lsblk -n -r -d -o UUID "${TIK_CRYPT_PART}") + cryptName="${TIK_CRYPT_MAPPER:-cr_root}" + echo "${cryptName} UUID=${cryptUUID} none x-initrd.attach" | prun tee "${TIK_ROOT_MNT}/etc/crypttab" + # FIXME: Dracut gets confused by previous installations on occasion with the default config, override the problematic option temporarily - /usr/bin/echo 'hostonly_cmdline="no"' | prun tee ${encrypt_dir}/mnt/etc/dracut.conf.d/99-tik.conf + echo "hostonly_cmdline=\"no\"" | prun tee "${TIK_ROOT_MNT}/etc/dracut.conf.d/99-tik.conf" + # Install bootloader with sdbootutil - prun /usr/bin/chroot ${encrypt_dir}/mnt sdbootutil -vv --esp-path /boot/efi --no-variables install 1>&2 - echo "56" > ${encrypt_pipe} - echo "# Enrolling recovery key" > ${encrypt_pipe} - prun /usr/bin/chroot ${encrypt_dir}/mnt sdbootutil -vv --esp-path /boot/efi --method=recovery-key enroll 1>&2 - echo "70" > ${encrypt_pipe} + prun /usr/bin/chroot "${TIK_ROOT_MNT}" sdbootutil -vv --esp-path /boot/efi --no-variables install 1>&2 + + tik_progress_step "Enrolling recovery key" 40 + prun /usr/bin/chroot "${TIK_ROOT_MNT}" sdbootutil -vv --esp-path /boot/efi --method=recovery-key enroll 1>&2 + # If Default mode has been detected, configure PCR policy. # `etc/sysconfig/fde-tools` must be created before any calls to sdbtools, # because sdbootutil expects at least one of the configuration files being # present. See # https://github.com/openSUSE/sdbootutil/commit/8d3db8b01f5681c11054c37145aad3e3973a7741 if [ "${tik_encrypt_mode}" == 0 ]; then - echo "# Enrolling TPM key" > ${encrypt_pipe} + tik_progress_step "Enrolling TPM key" 60 # Explaining the chosen PCR list below # - 4 - Bootloader and drivers, should never recovery key as bootloader should only be updated with new PCR measurements # - 5 - GPT Partition table, should never require recovery key as partition layout shouldn't change # - 7 - SecureBoot state, will require recovery key if SecureBoot is enabled/disabled # - 9 - initrd - should never require recovery key as initrd should only be updated with new PCR measurements - echo "FDE_SEAL_PCR_LIST=4,5,7,9" | prun tee ${encrypt_dir}/mnt/etc/sysconfig/fde-tools + echo "FDE_SEAL_PCR_LIST=4,5,7,9" | prun tee "${TIK_ROOT_MNT}/etc/sysconfig/fde-tools" # Explaining why the following PCRs were not used # - 0 - UEFI firmware, will require recovery key after firmware update and is particularly painful to re-enrol # - 1 - Not only changes with CPU/RAM/hardware changes, but also when UEFI config changes are made, which is too common to lockdown # - 2 - Includes option ROMs on pluggable hardware, such as external GPUs. Attaching a GPU to your laptop shouldn't hinder booting. # - 3 - Firmware from pluggable hardware. Attaching hardware to your laptop shouldn't hinder booting - prun /usr/bin/chroot ${encrypt_dir}/mnt sdbootutil -vv --esp-path /boot/efi --method=tpm2 enroll 1>&2 - else - echo "# Enrolling fallback password" > ${encrypt_pipe} + prun /usr/bin/chroot "${TIK_ROOT_MNT}" sdbootutil -vv --esp-path /boot/efi --method=tpm2 enroll 1>&2 + else + tik_progress_step "Setting encryption passphrase" 60 d --width=500 --height=300 --no-wrap --warning --icon=security-high-symbolic --title="Set Encryption Passphrase" --text="This ${TIK_OS_NAME} system is encrypted and will require a Passphrase on every boot\n\nYou will be prompted to set the Passphrase on the next screen\n\nFor more information please visit https://aeondesktop.org/encrypt" - log "[configure_encryption] Fallback Mode - Prompting user for passphrase for ${cryptpart}" - # Not using 'd' function to avoid logging the password - # FIXME - Now use 'd' function and logging=false - while true - do - if $gui; then - pw=$(zenity --password --title='Set Encryption Passphrase') - pw_check=$(zenity --password --title='Type Passphrase Again') - else - cenity pw --password --title="Set Encryption Passphrase" - cenity pw_check --password --title="Type Passphrase Again" + log "[configure_encryption] Fallback Mode - Prompting user for passphrase for ${TIK_CRYPT_PART}" + + while true; do + logging=false + d_opt --password --title="Set Encryption Passphrase" + pw="${result}" + d_opt --password --title="Type Passphrase Again" + pw_check="${result}" + logging=true + + # User cancelled both dialogs -> no passphrase set, just exit loop + if [ -z "${pw}" ] && [ -z "${pw_check}" ]; then + break fi - # Ask again, and double check the user is putting the right passphrase again. + if [ "${pw}" != "${pw_check}" ]; then d --warning --no-wrap --title="Passphrase did not match" --text="Please try again" - # Reset variable, so we can try again pw="" + pw_check="" + continue fi - if [ -n "${pw}" ]; then - prun /usr/sbin/cryptsetup luksAddKey --key-file=${tik_keyfile} --batch-mode --force-password "${cryptpart}" <<<"${pw}" - # Initrd wasn't generated by install or enroll as no TPM interaction, so do it now. - prun /usr/bin/chroot ${encrypt_dir}/mnt sdbootutil -vv --esp-path /boot/efi mkinitrd 1>&2 - fi + + prun /usr/sbin/cryptsetup luksAddKey --key-file="${tik_keyfile}" --batch-mode --force-password "${TIK_CRYPT_PART}" <<<"${pw}" + # Initrd wasn't generated by install or enroll as no TPM interaction, so do it now. + prun /usr/bin/chroot "${TIK_ROOT_MNT}" sdbootutil -vv --esp-path /boot/efi mkinitrd 1>&2 break done fi - # FIXME: Dracut gets confused by previous installations on occasion with the default config, remove override now initrd done - prun /usr/bin/rm ${encrypt_dir}/mnt/etc/dracut.conf.d/99-tik.conf - echo "84" > ${encrypt_pipe} - -} - -close_partition() { - echo "# Closing ${cryptpart}" > ${encrypt_pipe} - log "[close_partition] unmounting and closing ${cryptpart}" - for i in proc dev tmp 'boot/efi' etc var '.snapshots' 'sys/kernel/security' 'sys/firmware/efi/efivars' sys; do - prun /usr/bin/umount "${encrypt_dir}/mnt/$i" - done - prun /usr/bin/umount -l ${encrypt_dir}/mnt/run - prun /usr/bin/umount ${encrypt_dir}/mnt - prun /usr/sbin/cryptsetup luksClose aeon_root - echo "100" > ${encrypt_pipe} -} - -generate_recoveryKey() { - echo "# Generating recovery key" > ${encrypt_pipe} - log "[generate_recoveryKey] generating recovery key" - modhex=('c' 'b' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'n' 'r' 't' 'u' 'v') - mapfile -t raw_key < <(hexdump -v --format '1/1 "%u\n"' -n 32 /dev/random) - [ "${#raw_key[@]}" = 32 ] - key="" - for ((i=0;i<"${#raw_key[@]}";++i)); do - [ "$i" -gt 0 ] && [ "$((i%4))" -eq 0 ] && key="${key}-" - c="${raw_key[i]}" - key="${key}${modhex[$((c>>4))]}${modhex[$((c&15))]}" - done - log "[generate_recoveryKey] adding recovery key to roots sdbootutil user keyring" - logging=false - pkexec keyctl add user sdbootutil ${key} @u - logging=true - echo "50" > ${encrypt_pipe} -} -display_recoveryKey() { - local defaultmsg="This ${TIK_OS_NAME} system is encrypted and checks its own integrity on every boot\nIn the event of these integrity checks failing, you will need to use the Recovery Key provided below to enter this system\n\nLikely reasons for integrity checks failing include:\n\n• Secure Boot changed from enabled or disabled\n• Boot drive was moved to a different computer\n• Disk partitions were changed\n• Boot loader or initrd were altered unexpectedly\n\nIf you are unaware as to why the system is requesting the recovery key, this systems security may have been compromised\nThe best course of action may be to not unlock the disk until you can determine what changed to require the Recovery Key\n\nThis systems Recovery Key is:\n\n ${key}\n\nPlease save this secret Recovery Key in a secure location\n\n" - local fallbackmsg="In addition to your Passphrase a Recovery Key has been generated:\n\n ${key}\n\nPlease save this secret Recovery Key in a secure location\nIt may be used to regain access to this system if the other Passphrase becomes lost or forgotten\n\n" - local message - [ "${tik_encrypt_mode}" == 0 ] && message=${defaultmsg} - [ "${tik_encrypt_mode}" == 1 ] && message=${fallbackmsg} - log "[display_recoveryKey] displaying recovery key" - logging=false - d --width=500 --height=500 --no-wrap --warning --icon=security-high-symbolic --title="Encryption Recovery Key" --text="${message}You may optionally scan the recovery key off screen:\n$(qrencode ${key} -t UTF8i)\nFor more information please visit https://aeondesktop.org/encrypt" - logging=true - log "[display_recoveryKey] recovery key dialogue dismissed" + # FIXME: Dracut gets confused by previous installations on occasion with the default config, remove override now initrd done + prun /usr/bin/rm "${TIK_ROOT_MNT}/etc/dracut.conf.d/99-tik.conf" + tik_progress_step "Encryption configuration complete" 80 } -crypt_progress & -find_crypt -find_esp -open_partition generate_recoveryKey configure_encryption -close_partition display_recoveryKey +tik_progress_step "Encryption configured" 100 diff --git a/usr/lib/tik/modules/post/20-mig b/usr/lib/tik/modules/post/20-mig index bf23504..63e00ac 100644 --- a/usr/lib/tik/modules/post/20-mig +++ b/usr/lib/tik/modules/post/20-mig @@ -1,9 +1,10 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: Copyright 2024 SUSE LLC # SPDX-FileCopyrightText: Copyright 2024 Richard Brown +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens writemigdesktop() { - prun-opt /usr/bin/tee $1/.config/autostart/aeon-mig-firstboot.desktop << "EOF" + prun-opt /usr/bin/tee "$1/.config/autostart/aeon-mig-firstboot.desktop" << "EOF" [Desktop Entry] Name=Aeon Migration FirstBoot Setup Comment=Sets up Aeon Correctly On FirstBoot after Migration @@ -13,63 +14,69 @@ Type=Application Categories=Utility;System; Name[en_GB]=startup EOF - prun-opt /usr/bin/chmod 666 $1/.config/autostart/aeon-mig-firstboot.desktop + prun-opt /usr/bin/chmod 666 "$1/.config/autostart/aeon-mig-firstboot.desktop" } - if [ "${migrate}" == 1 ]; then - probe_partitions ${TIK_INSTALL_DEVICE} "crypto_LUKS" - [ -z "${probedpart}" ] || prun /usr/sbin/cryptsetup luksOpen --key-file=${tik_keyfile} ${cryptpart} aeon_root - - probe_partitions $TIK_INSTALL_DEVICE "btrfs" "/usr/lib/os-release" - - [ -n "${probedpart}" ] || error "MIGRATION FAILED: New Installation NOT FOUND" - - prun /usr/bin/mkdir ${mig_dir}/mnt - prun /usr/bin/mount -o compress=zstd:1 ${probedpart} ${mig_dir}/mnt - prun /usr/bin/systemd-repart --pretty 0 --root ${mig_dir}/mnt --dry-run=0 ${probedpart} - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/var ${probedpart} ${mig_dir}/mnt/var - prun /lib/systemd/systemd-growfs ${mig_dir}/mnt/var - # Detect whether /etc is overlay else assume it's a T-U 5.0+ later bind mount - if grep -qF 'overlay /etc' ${mig_dir}/mnt/etc/fstab ; then - etcmountcmd=$(cat ${mig_dir}/mnt/etc/fstab | grep "overlay /etc" | sed 's/\/sysroot\//${mig_dir}\/mnt\//g' | sed 's/\/work-etc.*/\/work-etc ${mig_dir}\/mnt\/etc\//' | sed 's/overlay \/etc overlay/\/usr\/bin\/mount -t overlay overlay -o/') - eval prun "$etcmountcmd" - else - prun /usr/bin/mount -o bind ${mig_dir}/mnt/etc ${mig_dir}/mnt/etc - fi - prun /usr/bin/cat ${mig_dir}/passwd.out | prun tee -a ${mig_dir}/mnt/etc/passwd - prun /usr/bin/cat ${mig_dir}/group.out | prun tee -a ${mig_dir}/mnt/etc/group - prun /usr/bin/cat ${mig_dir}/shadow.out | prun tee -a ${mig_dir}/mnt/etc/shadow - prun /usr/bin/sed -i "/^wheel:/ s/$/$(head -n 1 ${mig_dir}/passwd.out | awk -F'[/:]' '{print $1}')/" ${mig_dir}/mnt/etc/group - prun /usr/bin/cp -a ${mig_dir}/subuid ${mig_dir}/mnt/etc/subuid - prun /usr/bin/cp -a ${mig_dir}/subgid ${mig_dir}/mnt/etc/subgid + tik_target_mount "" "required" + + [ -n "${TIK_ROOT_MNT}" ] || error "MIGRATION FAILED: TIK_ROOT_MNT not set" + [ -n "${TIK_ROOT_DEV}" ] || error "MIGRATION FAILED: TIK_ROOT_DEV not set" + + tik_progress_step "Preparing migration restore" 0 + + prun /usr/bin/mkdir -p "${TIK_ROOT_MNT}/mnt" + + tik_unmount "${TIK_ROOT_MNT}/home" + tik_mount "${TIK_ROOT_DEV}" "mnt" "compress=zstd:1,subvol=/@" + + tik_progress_step "Applying target repart configuration" 10 + prun /usr/bin/systemd-repart --pretty 0 --root "${TIK_ROOT_MNT}" --dry-run=0 "${TIK_ROOT_DEV}" + + tik_progress_step "Restoring user/account data" 25 + prun /usr/bin/cat "${mig_dir}/passwd.out" | prun /usr/bin/tee -a "${TIK_ROOT_MNT}/etc/passwd" + prun /usr/bin/cat "${mig_dir}/group.out" | prun /usr/bin/tee -a "${TIK_ROOT_MNT}/etc/group" + prun /usr/bin/cat "${mig_dir}/shadow.out" | prun /usr/bin/tee -a "${TIK_ROOT_MNT}/etc/shadow" + prun /usr/bin/sed -i "/^wheel:/ s/$/$(head -n 1 "${mig_dir}/passwd.out" | awk -F'[/:]' '{print $1}')/" "${TIK_ROOT_MNT}/etc/group" + prun /usr/bin/cp -a "${mig_dir}/subuid" "${TIK_ROOT_MNT}/etc/subuid" + prun /usr/bin/cp -a "${mig_dir}/subgid" "${TIK_ROOT_MNT}/etc/subgid" # It's not guaranteed that the system will have existing network configs, localtime or AccountsService - prun-opt /usr/bin/cp -a ${mig_dir}/system-connections/* ${mig_dir}/mnt/etc/NetworkManager/system-connections - prun-opt /usr/bin/cp -a ${mig_dir}/localtime ${mig_dir}/mnt/etc/localtime - prun-opt /usr/bin/cp -a ${mig_dir}/users/* ${mig_dir}/mnt/var/lib/AccountsService/users - prun-opt /usr/bin/cp -a ${mig_dir}/icons/* ${mig_dir}/mnt/var/lib/AccountsService/icons - prun-opt /usr/bin/cp -a ${mig_dir}/bluetooth/* ${mig_dir}/mnt/var/lib/bluetooth - prun-opt /usr/bin/cp -a ${mig_dir}/fprint/* ${mig_dir}/mnt/var/lib/fprint - prun-opt /usr/bin/cp -a ${mig_dir}/openvpn/* ${mig_dir}/mnt/etc/openvpn - prun /usr/bin/umount ${mig_dir}/mnt/etc - prun /usr/bin/umount ${mig_dir}/mnt/var - prun /usr/bin/umount ${mig_dir}/mnt - prun /usr/bin/mount -o compress=zstd:1,subvol=/@ ${probedpart} ${mig_dir}/mnt - prun /usr/sbin/btrfs subvolume delete ${mig_dir}/mnt/home - (prun /usr/sbin/btrfs send ${mig_dir}/${snap_dir} | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive ${mig_dir}/mnt) 2>&1 | d --progress --title="Restoring /home" --pulsate --auto-close --no-cancel --width=400 - prun /usr/bin/mv ${mig_dir}/mnt/${snap_dir} ${mig_dir}/mnt/home - prun /usr/sbin/btrfs property set -f -ts ${mig_dir}/mnt/home ro false - for subsubvol in $(prun-opt /usr/sbin/btrfs subvolume list -o ${mig_dir}/${snap_dir} --sort=path | rev | cut -f1 -d' ' | rev | sed 's/^@//'); do - subsubvolname=$(basename $subsubvol) - subsubdirname=$(dirname $subsubvol | awk -F "${mig_dir}/${snap_dir}" '{print $2}') - (prun /usr/sbin/btrfs send ${subsubvol} | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive ${mig_dir}/mnt/home/${subsubdirname} ) 2>&1 | d --progress --title="Restoring containers" --pulsate --auto-close --no-cancel --width=400 - prun /usr/sbin/btrfs property set -f -ts ${mig_dir}/mnt/home/${subsubdirname}/${subsubvolname} ro false - prun-opt /usr/bin/sed -i 's/driver = "overlay"/driver = "btrfs"/g' ${mig_dir}/mnt/etc/containers/storage.conf + prun-opt /usr/bin/cp -a "${mig_dir}/system-connections/"* "${TIK_ROOT_MNT}/etc/NetworkManager/system-connections" + prun-opt /usr/bin/cp -a "${mig_dir}/localtime" "${TIK_ROOT_MNT}/etc/localtime" + prun-opt /usr/bin/cp -a "${mig_dir}/users/"* "${TIK_ROOT_MNT}/var/lib/AccountsService/users" + prun-opt /usr/bin/cp -a "${mig_dir}/icons/"* "${TIK_ROOT_MNT}/var/lib/AccountsService/icons" + prun-opt /usr/bin/cp -a "${mig_dir}/bluetooth/"* "${TIK_ROOT_MNT}/var/lib/bluetooth" + prun-opt /usr/bin/cp -a "${mig_dir}/fprint/"* "${TIK_ROOT_MNT}/var/lib/fprint" + prun-opt /usr/bin/cp -a "${mig_dir}/openvpn/"* "${TIK_ROOT_MNT}/etc/openvpn" + + log "[mig] deleting existing /home subvolume under ${TIK_ROOT_MNT}/mnt" + tik_progress_step "Removing existing /home" 35 + prun /usr/sbin/btrfs subvolume delete "${TIK_ROOT_MNT}/mnt/home" + + tik_progress_step "Restoring /home" 55 + (prun /usr/sbin/btrfs send "${mig_dir}/${snap_dir}" | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive "${TIK_ROOT_MNT}/mnt") 2>&1 >/dev/null + + prun /usr/bin/mv "${TIK_ROOT_MNT}/mnt/${snap_dir}" "${TIK_ROOT_MNT}/mnt/home" + prun /usr/sbin/btrfs property set -f -ts "${TIK_ROOT_MNT}/mnt/home" ro false + + for subsubvol in $(prun-opt /usr/sbin/btrfs subvolume list -o "${mig_dir}/${snap_dir}" --sort=path | rev | cut -f1 -d' ' | rev | sed 's/^@//'); do + subsubvolname="$(basename "$subsubvol")" + subsubdirname="$(dirname "$subsubvol" | awk -F "${mig_dir}/${snap_dir}" '{print $2}')" + + tik_progress_step "Restoring containers" 70 + (prun /usr/sbin/btrfs send "${subsubvol}" | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive "${TIK_ROOT_MNT}/mnt/home/${subsubdirname}") 2>&1 >/dev/null + + prun /usr/sbin/btrfs property set -f -ts "${TIK_ROOT_MNT}/mnt/home/${subsubdirname}/${subsubvolname}" ro false + prun-opt /usr/bin/sed -i 's/driver = "overlay"/driver = "btrfs"/g' "${TIK_ROOT_MNT}/mnt/etc/containers/storage.conf" done - for userhome in ${mig_dir}/mnt/home/*/; do - writemigdesktop $userhome + tik_mount "${TIK_ROOT_DEV}" "home" "compress=zstd:1,subvol=/@/home" + + tik_progress_step "Writing firstboot autostart entries" 90 + for userhome in "${TIK_ROOT_MNT}/mnt/home"/*/; do + writemigdesktop "${userhome}" done - prun /usr/bin/umount ${mig_dir}/mnt - prun /usr/bin/rmdir ${mig_dir}/mnt - [ ! -e /dev/mapper/aeon_root ] || prun /usr/sbin/cryptsetup luksClose aeon_root + + tik_unmount "${TIK_ROOT_MNT}/mnt" + + tik_progress_step "Migration restore complete" 100 fi diff --git a/usr/lib/tik/modules/pre/05-setup-gnome-env b/usr/lib/tik/modules/pre/05-setup-gnome-env new file mode 100644 index 0000000..20b95d0 --- /dev/null +++ b/usr/lib/tik/modules/pre/05-setup-gnome-env @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2025 SUSE LLC +# SPDX-FileCopyrightText: Copyright 2025 Richard Brown +# SPDX-FileCopyrightText: Copyright 2024 Raymond Yip +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens + +setup_env() { + # Setup environment for installation, mostly setting GNOME/gsettings vars to prevent screenlocking, etc + log "[setup_env] Setting up environment for Installation" + gsettings set org.gnome.shell favorite-apps [''] + gsettings set org.gnome.desktop.session idle-delay '0' + gsettings set org.gnome.desktop.screensaver lock-enabled 'false' + gsettings set org.gnome.desktop.screensaver user-switch-enabled 'false' + gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing' + gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' + gsettings set org.gnome.desktop.lockdown disable-lock-screen 'true' + gsettings set org.gnome.desktop.lockdown disable-log-out 'true' + gsettings set org.gnome.desktop.lockdown disable-printing 'true' + gsettings set org.gnome.desktop.lockdown disable-print-setup 'true' + gsettings set org.gnome.desktop.lockdown disable-user-switching 'true' + gsettings set org.gnome.desktop.lockdown user-administration-disabled 'true' + gsettings set org.gnome.software allow-updates 'false' + gsettings set org.gnome.software download-updates 'false' + gsettings set org.gnome.software download-updates-notify 'false' +} + +setup_env diff --git a/usr/lib/tik/modules/pre/10-welcome b/usr/lib/tik/modules/pre/10-welcome index 4d35b80..568a857 100644 --- a/usr/lib/tik/modules/pre/10-welcome +++ b/usr/lib/tik/modules/pre/10-welcome @@ -2,9 +2,10 @@ # SPDX-FileCopyrightText: Copyright 2025 SUSE LLC # SPDX-FileCopyrightText: Copyright 2025 Richard Brown # SPDX-FileCopyrightText: Copyright 2024 Raymond Yip +# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens proceedInstall() { - d --info --ok-label="Install Now" --no-wrap --width=300 --height=300 --icon=distributor-logo-Aeon-symbolic --title="" --text="Welcome to ${TIK_OS_NAME}\n\nPlease press Install Now to continue" + d --info --ok-label="Install Now" --no-wrap --width=300 --height=300 --icon=distributor-logo-symbolic --title="" --text="Welcome to ${TIK_OS_NAME}\n\nPlease press Install Now to continue" } displayACWarningMsg() { @@ -12,27 +13,27 @@ displayACWarningMsg() { } checkLaptop() { - chassis=`cat /sys/class/dmi/id/chassis_type` - #Test for respectively Handheld, Notebook, Laptop, and Portable - #if chassis variable matches 8 9 10 or 11 function continues else it proceeds to test AC power and Battery - [[ "$chassis" =~ ^(8|9|10|11)$ ]] || return - #Tested machine is confirmed mobile - givePowerRecommendation=false - #Only check for AC and Battery power connections with upower - updevices=`/usr/bin/upower -e|grep -E 'AC|BAT'` - for pdev in $updevices; do - #Get detailed info for each AC and BAT device in upower - upinfo=`/usr/bin/upower -i $pdev|grep -E 'online|state'` - #Check for discharging state or AC power offline which is equal to no state - if [[ "$upinfo" =~ (discharging|no) ]]; then - #Give power recommendation only once, so set this to true - givePowerRecommendation=true - fi - done - if [ "$givePowerRecommendation" = true ]; then - log "AC Power disconnected and Battery is not charging" - displayACWarningMsg + chassis=$(cat /sys/class/dmi/id/chassis_type) + # Test for respectively Handheld, Notebook, Laptop, and Portable + # If chassis variable matches 8 9 10 or 11 function continues else it proceeds to test AC power and Battery + [[ "$chassis" =~ ^(8|9|10|11)$ ]] || return + # Tested machine is confirmed mobile + givePowerRecommendation=false + # Only check for AC and Battery power connections with upower + updevices=$(/usr/bin/upower -e | grep -E 'AC|BAT') + for pdev in $updevices; do + # Get detailed info for each AC and BAT device in upower + upinfo=$(/usr/bin/upower -i "$pdev" | grep -E 'online|state') + # Check for discharging state or AC power offline which is equal to no state + if [[ "$upinfo" =~ (discharging|no) ]]; then + # Give power recommendation only once, so set this to true + givePowerRecommendation=true fi + done + if [ "$givePowerRecommendation" = true ]; then + log "[welcome] AC Power disconnected and Battery is not charging" + displayACWarningMsg + fi } verify_efi() { @@ -44,27 +45,6 @@ verify_efi() { fi } -setup_env() { - # Setup environment for installation, mostly setting GNOME/gsettings vars to prevent screenlocking, etc - log "[setup_env] Setting up environment for Installation" - gsettings set org.gnome.shell favorite-apps [''] - gsettings set org.gnome.desktop.session idle-delay '0' - gsettings set org.gnome.desktop.screensaver lock-enabled 'false' - gsettings set org.gnome.desktop.screensaver user-switch-enabled 'false' - gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing' - gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' - gsettings set org.gnome.desktop.lockdown disable-lock-screen 'true' - gsettings set org.gnome.desktop.lockdown disable-log-out 'true' - gsettings set org.gnome.desktop.lockdown disable-printing 'true' - gsettings set org.gnome.desktop.lockdown disable-print-setup 'true' - gsettings set org.gnome.desktop.lockdown disable-user-switching 'true' - gsettings set org.gnome.desktop.lockdown user-administration-disabled 'true' - gsettings set org.gnome.software allow-updates 'false' - gsettings set org.gnome.software download-updates 'false' - gsettings set org.gnome.software download-updates-notify 'false' -} - -setup_env proceedInstall verify_efi checkLaptop diff --git a/usr/lib/tik/modules/pre/20-mig b/usr/lib/tik/modules/pre/20-mig index 5073fc6..c1ea37e 100644 --- a/usr/lib/tik/modules/pre/20-mig +++ b/usr/lib/tik/modules/pre/20-mig @@ -4,19 +4,21 @@ mig_dir=/var/lib/tik/mig snap_dir=homebk +home_size=0 +tik_stick_size=0 if [ ! -d ${mig_dir} ]; then prun /usr/bin/mkdir -p ${mig_dir} fi if [ ! -z "$(ls -A ${mig_dir})" ]; then - log "existing backup found" + log "[mig] existing backup found" d_opt --question --no-wrap --cancel-label="No, Delete Backup" --title="Existing user backup detected" --text="These users can be restored to the new installation\n\nWould you like to use this backup?" oldbackupyn=$? - log "[oldbackupyn][${oldbackupyn}]" + log "[mig] [oldbackupyn][${oldbackupyn}]" if [ "${oldbackupyn}" == 0 ]; then skipbackup=1 migrate=1 - log "backup skipped, migration will use existing backup" + log "[mig] backup skipped, migration will use existing backup" else prun-opt /usr/sbin/btrfs property set -f -ts ${mig_dir}/${snap_dir} ro false for subsubvol in $(prun-opt /usr/sbin/btrfs subvolume list -o ${mig_dir}/${snap_dir} --sort=path | rev | cut -f1 -d' ' | rev | sed "s/^@//"); do @@ -46,69 +48,56 @@ fi get_disk if [ -z "${skipbackup}" ]; then - # Although Legacy Aeon didn't officially support LUKS encrypted installations, - # some users might have nevertheless enabled encryption anyway. - # Search for existing crypto_LUKS partitions and, if found, prompt the user - # to unlock those so that the migration module can find existing data. - for encrypted_partition in $(lsblk ${TIK_INSTALL_DEVICE} -p -n -r -o ID-LINK,FSTYPE|tr -s ' ' ";"|grep ";crypto_LUKS"|cut -d\; -f1); do - if [ -e /dev/mapper/crypt_${encrypted_partition} ]; then - # Already opened for some reason... do not prompt for the passphrase - # but ensure we will clean up afterwards - crypt_opened="${crypt_opened} crypt_${encrypted_partition}" - else - while [ 1 ]; do - if $gui; then - passphrase=$(zenity --password --title="Encrypted partition (${encrypted_partition}) detected" --cancel-label="Skip") || break - else - cenity passphrase --password --title="Encrypted partition (${encrypted_partition}) detected" --cancel-label="Skip" || break - fi - if [ -n "${passphrase}" ]; then - echo -n "${passphrase}" | prun /usr/sbin/cryptsetup luksOpen /dev/disk/by-id/${encrypted_partition} crypt_${encrypted_partition} - if [ "${?}" -eq 0 ]; then - crypt_opened="${crypt_opened} crypt_${encrypted_partition}" - # Wait for the mapped device to appear - wait_count=0 - while [ ! -e /dev/mapper/crypt_${encrypted_partition} ] && [ ${wait_count} -lt 5 ]; do - sleep 1 - wait_count=$((wait_count + 1)) - done - break - fi - fi - done + # Mount the existing system + tik_target_mount "tik_old_root" "optional" + case "$?" in + 0) ;; + *) + log "[mig] no migratable system found, skipping migration" + migrate=0 + return 0 + ;; + esac + + # Determine whether migration is feasible by checking /home size + if [ -d "${TIK_ROOT_MNT}/home" ]; then + # If /home is btrfs, run quota rescan and check qgroup usage. + # Quotas may not be enabled, so qgroup show can fail. Fall back to du in that case. + prun-opt /usr/sbin/btrfs quota show "${TIK_ROOT_MNT}/home" >/dev/null 2>&1 + if [ "${retval}" -eq 0 ]; then + tik_progress_step "Scanning existing /home (btrfs quotas)" 10 + prun-opt /usr/sbin/btrfs quota rescan -w "${TIK_ROOT_MNT}/home" >/dev/null 2>&1 + if [ "${retval}" -eq 0 ]; then + home_size="$(prun-opt /usr/sbin/btrfs qgroup show --raw -f "${TIK_ROOT_MNT}/home" | awk 'NR==2{print $2}')" fi - done - unset passphrase - - # Probe selected disk for a btrfs partition containing /usr/lib/os-release - probe_partitions $TIK_INSTALL_DEVICE "btrfs" "/usr/lib/os-release" - - if [ -n "${probedpart}" ]; then - prun /usr/bin/mkdir ${mig_dir}/mnt - prun-opt /usr/bin/mount -o compress=zstd:1,subvol=/@/home ${probedpart} ${mig_dir}/mnt - if [ ${retval} -eq 0 ]; then - prun /usr/sbin/btrfs quota rescan -w ${mig_dir}/mnt | d --progress --title="Detected existing /home subvolume.." --pulsate --auto-close --no-cancel --width=400 - home_size=$(prun /usr/sbin/btrfs qgroup show --raw -f ${mig_dir}/mnt | grep @/home$ | awk '{print $2}') - tik_stick_size=$(prun /usr/sbin/btrfs fi usage --raw ${mig_dir} | grep estimated | awk '{print $3}') - if [ ${home_size} -gt ${tik_stick_size} ]; then + else + home_size="" + fi + + if [ -z "${home_size}" ] || [ "${home_size}" = "0" ]; then + log "[mig] btrfs quotas not enabled (or qgroup size unavailable), falling back to du -sb for /home size" + tik_progress_step "Scanning existing /home size" 15 + home_size="$(prun /usr/bin/du -sb "${TIK_ROOT_MNT}/home" 2>/dev/null | awk '{print $1}')" + fi + + tik_progress_step "Checking available space" 25 + tik_stick_size=$(prun /usr/sbin/btrfs fi usage --raw ${mig_dir} | grep estimated | awk '{print $3}') + if [ -n "${home_size}" ] && [ -n "${tik_stick_size}" ]; then + if [ "${home_size}" -gt "${tik_stick_size}" ]; then # Not enough space to offer migration migrate=0 fi - if [ ${home_size} -le 16384 ]; then + if [ "${home_size}" -le 16384 ]; then # /home subvolume is empty migrate=0 fi - prun /usr/bin/umount ${mig_dir}/mnt else - log "no @/home subvolume found on ${probedpart}" + log "[mig] could not determine /home size or stick size reliably, migration disabled" migrate=0 fi - prun /usr/bin/rmdir ${mig_dir}/mnt - # partition found, /home subvolume found, no known reason to not migrate, so ask the user + + # partition found, /home found, no known reason to not migrate, so ask the user if [ -z "${migrate}" ]; then - if [ "${legacy_aeon}" == 1 ]; then - d --info --width=300 --height=300 --icon=distributor-logo-Aeon-symbolic --no-wrap --title="Message from the Aeon Team" --text="We'd like to thank you for adopting openSUSE Aeon so early in it's development,\nbefore we fully understood what we were building or how we wanted it to look\n\nWe are sorry that you need to reinstall your system\n\nThank you so much for your support.\nWe hope you enjoy the new look openSUSE Aeon" - fi d_opt --question --no-wrap --title="Backup users from the existing install?" --text="These users will be restored to the new installation." migrateyn=$? if [ "${migrateyn}" == 0 ]; then @@ -117,70 +106,58 @@ if [ -z "${skipbackup}" ]; then migrate=0 fi fi + else + log "[mig] no /home found on mounted system, migration disabled" + migrate=0 fi if [ "${migrate}" == 1 ]; then # We're migrating, lets go! - prun /usr/bin/mkdir ${mig_dir}/mnt - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/home ${probedpart} ${mig_dir}/mnt # Check for existing snapshot from interrupted backup and delete if it exists boo#1224824 - if [ -d ${mig_dir}/mnt/${snap_dir} ]; then - prun /usr/sbin/btrfs subvolume delete ${mig_dir}/mnt/${snap_dir} + tik_progress_step "Preparing /home snapshot" 35 + if [ -d "${TIK_ROOT_MNT}/home/${snap_dir}" ]; then + prun /usr/sbin/btrfs subvolume delete "${TIK_ROOT_MNT}/home/${snap_dir}" fi - prun /usr/sbin/btrfs subvolume snapshot -r ${mig_dir}/mnt ${mig_dir}/mnt/${snap_dir} - (prun /usr/sbin/btrfs send ${mig_dir}/mnt/${snap_dir} | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive ${mig_dir}) 2>&1 | d --progress --title="Backing up /home" --pulsate --auto-close --no-cancel --width=400 - prun /usr/sbin/btrfs subvolume delete ${mig_dir}/mnt/${snap_dir} - # Probe for subvolumes nested beneath /home and back them up also - if (prun-opt /usr/sbin/btrfs subvolume list -o ${mig_dir}/mnt | grep -q "ID "); then - prun /usr/sbin/btrfs property set -f -ts ${mig_dir}/${snap_dir} ro false - for subsubvol in $(prun-opt /usr/sbin/btrfs subvolume list -o ${mig_dir}/mnt --sort=path | rev | cut -f1 -d' ' | rev | sed 's/^@\/home//'); do - subsubvolname=$(basename $subsubvol) - subsubdirname=$(dirname $subsubvol) - prun /usr/sbin/btrfs subvolume snapshot -r ${mig_dir}/mnt/${subsubvol} ${mig_dir}/mnt/${subsubvolname} - (prun /usr/sbin/btrfs send ${mig_dir}/mnt/${subsubvolname} | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive ${mig_dir}/${snap_dir}/${subsubdirname}) 2>&1 | d --progress --title="Backing up containers" --pulsate --auto-close --no-cancel --width=400 - prun /usr/sbin/btrfs subvolume delete ${mig_dir}/mnt/${subsubvolname} - done - prun /usr/sbin/btrfs property set -f -ts ${mig_dir}/${snap_dir} ro true - fi - prun /usr/bin/umount ${mig_dir}/mnt - prun /usr/bin/mount -o compress=zstd:1 ${probedpart} ${mig_dir}/mnt - prun /usr/bin/mount -o compress=zstd:1,subvol=/@/var ${probedpart} ${mig_dir}/mnt/var - etcmntcmd=$(cat ${mig_dir}/mnt/etc/fstab | grep "overlay /etc" | sed 's/\/sysroot\//${mig_dir}\/mnt\//g' | sed 's/\/work-etc.*/\/work-etc ${mig_dir}\/mnt\/etc\//' | sed 's/overlay \/etc overlay/\/usr\/bin\/mount -t overlay overlay -o/') - eval prun "$etcmntcmd" - prun /usr/bin/awk -F'[/:]' '($3 >= 1000 && $3 != 65534)' ${mig_dir}/mnt/etc/passwd | prun /usr/bin/awk -F':' '{ $7="/bin/bash"; print };' OFS=':' | prun tee ${mig_dir}/passwd.out - prun /usr/bin/awk -F'[/:]' '($3 >= 1000 && $3 != 65534 && $3 != 65533)' ${mig_dir}/mnt/etc/group | prun tee ${mig_dir}/group.out - prun /usr/bin/awk -F'[/:]' '{if ($3 >= 1000 && $3 != 65534) print $1}' ${mig_dir}/mnt/etc/passwd | prun /usr/bin/grep -f - ${mig_dir}/mnt/etc/shadow | prun tee ${mig_dir}/shadow.out - prun /usr/bin/cp -a ${mig_dir}/mnt/etc/subuid ${mig_dir}/subuid - prun /usr/bin/cp -a ${mig_dir}/mnt/etc/subgid ${mig_dir}/subgid - # It's not guaranteed that the system will have existing network configs, custom localtime or AccountsService - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/etc/NetworkManager/system-connections ${mig_dir}/system-connections - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/etc/localtime ${mig_dir}/localtime - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/var/lib/AccountsService/users ${mig_dir}/users - prun-opt /usr/bin/chmod 744 ${mig_dir}/users - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/var/lib/AccountsService/icons ${mig_dir}/icons - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/var/lib/bluetooth ${mig_dir}/bluetooth - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/var/lib/fprint ${mig_dir}/fprint - prun-opt /usr/bin/cp -a ${mig_dir}/mnt/etc/openvpn ${mig_dir}/openvpn - prun-opt /usr/bin/umount ${mig_dir}/mnt/etc - prun /usr/bin/umount ${mig_dir}/mnt/var - prun /usr/bin/umount ${mig_dir}/mnt - prun /usr/bin/rmdir ${mig_dir}/mnt - fi + prun /usr/sbin/btrfs subvolume snapshot -r "${TIK_ROOT_MNT}/home" "${TIK_ROOT_MNT}/home/${snap_dir}" - # Close eventual mapped LUKS devices - if [ -n "${crypt_opened}" ]; then - for mapped_partition in ${crypt_opened}; do - if [ -e /dev/mapper/crypt_${encrypted_partition} ]; then - # We are going to replace the encrypted partition anyway, so if - # we're unable to gracefully close the device after 5 seconds, - # just give up (hence the prun-opt usage) - wait_count=0 - while ! prun-opt /usr/sbin/cryptsetup luksClose /dev/mapper/${mapped_partition} && [ ${wait_count} -lt 5 ]; do - sleep 1 - wait_count=$((wait_count + 1)) - done - fi - done - fi + tik_progress_step "Backing up /home" 45 + (prun /usr/sbin/btrfs send "${TIK_ROOT_MNT}/home/${snap_dir}" | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive ${mig_dir}) 2>&1 >/dev/null + + tik_progress_step "Cleaning up snapshot" 55 + prun /usr/sbin/btrfs subvolume delete "${TIK_ROOT_MNT}/home/${snap_dir}" + # Probe for subvolumes nested beneath /home and back them up also + if (prun-opt /usr/sbin/btrfs subvolume list -o "${TIK_ROOT_MNT}/home" | grep -q "ID "); then + prun /usr/sbin/btrfs property set -f -ts ${mig_dir}/${snap_dir} ro false + for subsubvol in $(prun-opt /usr/sbin/btrfs subvolume list -o "${TIK_ROOT_MNT}/home" --sort=path | rev | cut -f1 -d' ' | rev | sed 's/^@\/home//'); do + subsubvolname=$(basename $subsubvol) + subsubdirname=$(dirname $subsubvol) + prun /usr/sbin/btrfs subvolume snapshot -r "${TIK_ROOT_MNT}/home/${subsubvol}" "${TIK_ROOT_MNT}/home/${subsubvolname}" + + tik_progress_step "Backing up containers" 65 + (prun /usr/sbin/btrfs send "${TIK_ROOT_MNT}/home/${subsubvolname}" | pv -f -F "# %b copied in %t %r" | prun /usr/sbin/btrfs receive ${mig_dir}/${snap_dir}/${subsubdirname}) 2>&1 >/dev/null + + prun /usr/sbin/btrfs subvolume delete "${TIK_ROOT_MNT}/home/${subsubvolname}" + done + prun /usr/sbin/btrfs property set -f -ts ${mig_dir}/${snap_dir} ro true + fi + + tik_progress_step "Exporting user/account data" 80 + prun /usr/bin/awk -F'[/:]' '($3 >= 1000 && $3 != 65534)' "${TIK_ROOT_MNT}/etc/passwd" | prun /usr/bin/awk -F':' '{ $7="/bin/bash"; print };' OFS=':' | prun tee ${mig_dir}/passwd.out + prun /usr/bin/awk -F'[/:]' '($3 >= 1000 && $3 != 65534 && $3 != 65533)' "${TIK_ROOT_MNT}/etc/group" | prun tee ${mig_dir}/group.out + prun /usr/bin/awk -F'[/:]' '{if ($3 >= 1000 && $3 != 65534) print $1}' "${TIK_ROOT_MNT}/etc/passwd" | prun /usr/bin/grep -f - "${TIK_ROOT_MNT}/etc/shadow" | prun tee ${mig_dir}/shadow.out + prun /usr/bin/cp -a "${TIK_ROOT_MNT}/etc/subuid" ${mig_dir}/subuid + prun /usr/bin/cp -a "${TIK_ROOT_MNT}/etc/subgid" ${mig_dir}/subgid + # It's not guaranteed that the system will have existing network configs, custom localtime or AccountsService + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/etc/NetworkManager/system-connections" ${mig_dir}/system-connections + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/etc/localtime" ${mig_dir}/localtime + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/var/lib/AccountsService/users" ${mig_dir}/users + prun-opt /usr/bin/chmod 744 ${mig_dir}/users + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/var/lib/AccountsService/icons" ${mig_dir}/icons + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/var/lib/bluetooth" ${mig_dir}/bluetooth + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/var/lib/fprint" ${mig_dir}/fprint + prun-opt /usr/bin/cp -a "${TIK_ROOT_MNT}/etc/openvpn" ${mig_dir}/openvpn + + tik_progress_step "Migration backup prepared" 100 + fi fi