From 66346740bdf4f7cc3d3cc6e4e79ee9db8a9d636b Mon Sep 17 00:00:00 2001 From: Richard Brown Date: Fri, 24 Nov 2023 16:43:27 +0100 Subject: [PATCH] first readme Update README.md Basic folder/file structure Default configs Fix paths, make DISTRO_DIR unconfigurable New paths Fix paths Supress kernel/systemd errors while running Working storage functions and module loading Update Squish squishme fixme --- README.md | 53 ++++++- etc/tik/config | 19 +++ usr/lib/tik/config | 15 ++ usr/lib/tik/lib/tik-functions | 286 ++++++++++++++++++++++++++++++++++ usr/sbin/tik | 70 +++++++++ 5 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 etc/tik/config create mode 100644 usr/lib/tik/config create mode 100644 usr/lib/tik/lib/tik-functions create mode 100755 usr/sbin/tik diff --git a/README.md b/README.md index 5db45c9..a38a26b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ # tik -Transactional Installation Kit + +Transactional Installation Kit - A toolkit for deploying Operating System images to UEFI hardware from a USB stick. + +## General Premise + +A simple, lightweight, extensible tool for deploying a premade OS images to UEFI hardware. + +It is inspired by the "SelfInstaller" functionality offered by [kiwi](https://github.com/OSInside/kiwi) OEM images, but is designed to be wholly independant of the toolchain used to create the OS images. + +It's core functionality is very similar to kiwi's SelfInstaller, with the basic workflow for deploying an image being a very simple process: + +- Identify storage devices on the system +- Offer the user a list of available devices +- Deploy image to that device + +In addition to the above workflow, tik supports the following additional features + +- Unattended automation of the deployment of the image +- Optional extensions to be run before or after the deployment of the image (eg to support functionality like checking the network for an updated image). This functionality is inspired by [jeos-firstboot](https://github.com/openSUSE/jeos-firstboot/)'s module support +- Support for multiple images provided on the same installation media (eg. openSUSE Aeon and openSUSE MicroOS) + +## tik OS Images + +tik is designed to deploy a .raw/.img disk image, which is expected to contain + +- the full partition table +- a UEFI ESP/EFI partition +- 1 (or more) OS partitions + +tik should not care about the contents of the disk image, which potentially could be of any Operating System built using any toolset (eg kiwi, mkosi, etc) + +Features like expanding the partitions to fill the disk are expected to be handled by tools like systemd-repart on the booting of the deployed OS, not by tik (though in theory optional extensions could be written to impliment this) + +## tik Installation Media + +tik is designed to be run on a different style of media than many traditional OS installers + +Traditional tooling like YaST, Agama, Windows Installer, etc are all expected to be read-only Installation media that aren't modifiable by the user at all + +tik Installtion Media are expected to be a variant of openSUSE MicroOS, designed to be run from portable media (eg a USB stick) + +while the "Install OS" of the Installation Media will therefore be read-only when in use, the "Install OS" will be possible of being updated and configured to the users needs, directly on the USB stick after it's imaged + +More importantly, this also means that the Installation Media will have various read-write locations, including /var/lib/tik/images, the location of tiks .raw/.img files, allowing users to add their own custom variants of such images to be offered when the tik installer boots up + +## tik + ignition + combustion + +because tik installation media are built seperately from the Operating System(s) which tik will offer to deploy, this means that tik installation images can also contain a seperate 'ignition/combustion partition' which can have your ignition/combustion configurations stored within + +These will then be automatically used by any OS image which uses ignition or combustion (eg openSUSE MicroOS) on their first boot after tik has deployed an image, assuming the tik Installation USB stick is still connected + +This makes ignition and/or combustion the perfect tools for making any automated customisations to any OS image deployed via tik diff --git a/etc/tik/config b/etc/tik/config new file mode 100644 index 0000000..db3e194 --- /dev/null +++ b/etc/tik/config @@ -0,0 +1,19 @@ +# Directory for users to add custom configuration and modules +# Default: "/etc/tik" +#TIK_CUSTOM_DIR="/etc/tik" + +# Directory for OS images to be deployed +# Default: "/var/lib/tik" +#TIK_IMG_DIR="/var/lib/tik" + +# Tile to show in the top left during the installation process. This may be the 'distribution name' for tik media that only offers a single distribution +# Default: "tik - Transactional Installation Kit" +#TIK_TITLE="tik - Transactional Installation Kit" + +# For unattended installations the disk device to deploy the image must be defined if more than one is present +# Default: Undefined +#TIK_INSTALL_DEVICE="" + +# For unattended installations the disk image to deploy must be defined if more than one is present +# Default: Undefined +#TIK_INSTALL_IMAGE="" \ No newline at end of file diff --git a/usr/lib/tik/config b/usr/lib/tik/config new file mode 100644 index 0000000..00e1199 --- /dev/null +++ b/usr/lib/tik/config @@ -0,0 +1,15 @@ +# Directory for users to add custom configuration and modules +# Default: "/etc/tik" +TIK_CUSTOM_DIR="/etc/tik" + +# Directory for OS images to be deployed +# Default: "/var/lib/tik" +TIK_IMG_DIR="/var/lib/tik" + +# Tile to show in the top left during the installation process. This may be the 'distribution name' for tik media that only offers a single distribution +# Default: "tik - Transactional Installation Kit" +TIK_TITLE="tik - Transactional Installation Kit" + +# For unattended installations the disk device to deploy the image must be defined +# Default: Undefined +#TIK_INSTALL_DEVICE="" \ No newline at end of file diff --git a/usr/lib/tik/lib/tik-functions b/usr/lib/tik/lib/tik-functions new file mode 100644 index 0000000..7411fbd --- /dev/null +++ b/usr/lib/tik/lib/tik-functions @@ -0,0 +1,286 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2023 SUSE LLC + + +# Read os-release, to enable those variables to be used elsewhere +if [ -e /etc/os-release ]; then + . /etc/os-release +else + . /usr/lib/os-release +fi + +d(){ + while true + do + retval=0 + # Bash makes it a bit annoying to read the output of a different FD into a variable, it + # only supports reading stdout by itself. So redirect 3 to stdout and 1 to the real stdout. + exec {stdoutfd}>&1 + result="$(dialog --backtitle "$TIK_TITLE" --output-fd 3 "$@" 3>&1 1>&${stdoutfd})" || retval=$? + # Word splitting makes it necessary to use eval here. + eval "exec ${stdoutfd}>&-" + case $retval in + 0) + return 0 + ;; + 1|255) + dialog --backtitle "$TIK_TITLE" --yesno $"Do you really want to quit?" 0 0 && exit 1 + continue + ;; + esac + done +} + +udev_pending() { + declare DEVICE_TIMEOUT=${DEVICE_TIMEOUT} + local limit=30 + if [[ "${DEVICE_TIMEOUT}" =~ ^[0-9]+$ ]]; then + limit=$(((DEVICE_TIMEOUT + 1)/ 2)) + fi + udevadm settle --timeout=${limit} +} + +bool() { + # """ + # provides boolean string true|false for given value. + # Only if value matches true return true, in any other + # case return false + # """ + local value=$1 + if [ -n "${value}" ] && [ "${value}" = "true" ] ;then + echo "true" + else + echo "false" + fi +} + +warn() { + echo "tik Warning: $*" >&2 +} + +get_persistent_device_from_unix_node() { + local unix_device=$1 + local schema=$2 + local node + local persistent_name + node=$(basename "${unix_device}") + udev_pending + 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}" + warn "Using original device ${unix_device}" + echo "${unix_device}" +} + +get_disk_list() { + # 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 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 + + 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_device="$(echo "${disk_meta}" | cut -f1 -d:)" + 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_size=$(echo "${disk_meta}" | cut -f2 -d:) + disk_device_by_id=$( + get_persistent_device_from_unix_node "${disk_device}" "${disk_id}" + ) + if [ -n "${disk_device_by_id}" ];then + disk_device=${disk_device_by_id} + fi + list_items="${list_items} ${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" + report_and_quit "${no_dev}" + fi + if [ ! -b "${device}" ];then + local no_block_dev="Given device ${device} is not a block special" + report_and_quit "${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="${device} ${device_size}" + message="tik installation device set to to: ${device}" + info "${message}" >&2 + fi + if [ -z "${list_items}" ];then + local no_device_text="No device(s) for installation found" + report_and_quit "${no_device_text}" + fi + echo "${list_items}" +} + +get_disk() { + local disk_list + local device_array + disk_list=$(get_disk_list) + 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 + TIK_INSTALL_DEVICE="${device_array[0]}" + else + # manually select from storage list + d --menu "Select Installation Disk:" 0 0 0 $(get_disk_list) + TIK_INSTALL_DEVICE="$result" + fi + fi +} + +get_img_list() { + local list_items + local message + local img_meta + local img_item + # Images are assumed to be named to the following standard + # $ProductName.$Version.raw.xz + # Any extraneous fields may confuse tik's detection, selection and presentation of the image to the user + for img_meta in $( + eval cd $TIK_IMG_DIR && (stat --printf="%n\t%s\n" *.raw.xz | 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 + 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" + report_and_quit "${no_img}" + fi + if [ ! -s "${img}" ];then + local empty_img="Given image ${img} is empty" + report_and_quit "${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}" + info "${message}" >&2 + fi + if [ -z "${list_items}" ];then + local no_image_text="No images(s) for installation found" + report_and_quit "${no_image_text}" + fi + echo ${list_items} +} + +get_img() { + local img_list + local img_array + img_list=$(get_img_list) + 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 + # one single disk image found, use it + TIK_INSTALL_IMAGE="${img_array[0]}" + else + # manually select from storage list + d --menu "Select Installation Image:" 0 0 0 $(get_img_list) + TIK_INSTALL_IMAGE="$result" + fi + fi +} + +function dump_image { + local image_source_files=$1 + local image_target=$2 + local image_source + local image_basename + image_source="$(echo "${image_source_files}" | cut -f1 -d\|)" + image_basename=$(basename "${image_source}") + local load_text="Loading ${image_basename}..." + local title_text="Installation..." + + local ack_dump_text="Destroying ALL data on\n ${image_target}\nContinue?" + d --yesno "${ack_dump_text}" 7 80 + + if [ -n "$dry" ]; then + echo "DEBUG: ${load_text} [${image_target}]..." + fi + (pv -n ${image_source} | run dd of=${image_target} bs=64k) 2>&1 | d --gauge "${title_text}" 7 65 0 + d --msgbox "${image_basename}\n has been installed to\n${image_target}" 7 80 +} + + +load_modules() { +local module_dir +if [[ $2 = "etc" ]]; 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 + . $f + done +fi +} \ No newline at end of file diff --git a/usr/sbin/tik b/usr/sbin/tik new file mode 100755 index 0000000..f3080f5 --- /dev/null +++ b/usr/sbin/tik @@ -0,0 +1,70 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2023 SUSE LLC + +tik_dir=/usr/lib/tik + +# Read configuration files, /usr first, then /etc +. $tik_dir/config +. $TIK_CUSTOM_DIR/config + +# Read libraries +. $tik_dir/lib/tik-functions + +# Check essential paths exist +if [ ! -d "$TIK_IMG_DIR" ]; then + echo "$TIK_IMG_DIR does not exist" + exit 1 +fi + +# for testing we may run as non root +if [ -w /run ]; then + export TMPDIR=/run +else + dry=1 +fi + +if [ -n "$dry" ]; then + run() { + echo "$@" + } +else + run() { + "$@" + } +fi + +cleanup() { + # reenable systemd and kernel logs + # Try the race-free DBus method first + if ! run dbus-send --system --print-reply --dest=org.freedesktop.systemd1 /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager.SetShowStatus string: &>/dev/null; then + # Fall back to using signals + run kill -s SIGRTMAX-10 1 + fi + run setterm -msg on 2>/dev/null || true + echo +} +trap cleanup EXIT + +# avoid kernel messages spamming our console +run setterm -msg off 2>/dev/null || true +# Avoid systemd messages spamming our console +# Try the race-free DBus method first +if ! run dbus-send --system --print-reply --dest=org.freedesktop.systemd1 /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager.SetShowStatus string:off &>/dev/null; then + # Fall back to using signals + run kill -s SIGRTMAX-9 1 + # sleep to avoid systemd bug, bsc#1119382 + sleep 1 +fi + +load_modules "pre" +load_modules "pre" "etc" + +get_disk +get_img +dump_image "${TIK_INSTALL_IMAGE}" "${TIK_INSTALL_DEVICE}" + +load_modules "post" +load_modules "post" "etc" \ No newline at end of file