#!/usr/bin/env bash # -*- coding: utf-8 -*- # region header # Copyright Torben Sickert (info["~at~"]torben.website) 29.10.2015 # Janosch Dobler (info["~at~"]jandob.com) 29.10.2015 # License # ------- # This library written by Torben Sickert and Janosch Dobler stand under a # creative commons naming 3.0 unported license. # see http://creativecommons.org/licenses/by/3.0/deed.de # This tool provides a generic way to install systemd based remote linux # initramfs. # Examples # -------- # Start install progress command (Assuming internet is available): # >>> ./build-initramfs.sh # Note that you only get very necessary output until you provide "--verbose" as # commandline options. declare -rg _root_dir="$(readlink -f $(dirname ${BASH_SOURCE[0]}))" declare -rg _repo_dir="${_root_dir}/systemd-init.git" declare -rg _git_source="git://git.openslx.org/openslx-ng/systemd-init.git" declare -rg _git_branch="master" ## region ensure presence of needed dependencies set -o errexit if [ ! -e "$_repo_dir" ]; then echo "Missing dracut modules repository, loading them." if ! hash git; then echo "Needed dependency \"git\" isn't available." echo "Please install \"git\" or provide the main repository in \"${_repo_dir}\"." fi git clone --branch "$_git_branch" --single-branch --depth 1 \ "$_git_source" "${_repo_dir}" pushd "${_repo_dir}" git submodule init # try to clone submodules as shallowy as possible, since we cannot just # use '--depth 1' on submodules residing on non-master branches... for mod in $(grep -Po '(?<=^\[submodule ")([^"]+)' .gitmodules); do url="$(git config -f .gitmodules --get submodule.${mod}.url)" path="$(git config -f .gitmodules --get submodule.${mod}.path)" branch="$(git config -f .gitmodules --get submodule.${mod}.branch)" commit="$(git submodule status $path | grep -oE '[0-9a-f]{40}')" depth_arg=("--shallow-since") [ "$mod" = "dnbd3" ] && depth_arg+=("2019-02-12") [ "$mod" = "rebash" ] && depth_arg+=("2016-11-30") [ "$mod" = "qemu-xmount" ] && depth_arg+=("2016-01-01") [ "$mod" = "xmount" ] && depth_arg+=("2015-11-05") [ "$mod" = "kernel-qcow2-linux" ] && depth_arg+=("2019-08-25") [ "$mod" = "kernel-qcow2-util-linux" ] && depth_arg+=("2019-08-15") git clone -n --no-tags "${depth_arg[@]}" --branch "$branch" "$url" "$path" pushd "$path" git checkout "$commit" popd done # apply patches for submodules git submodule foreach ' for p in $(find ${toplevel}/builder/patches/${path##*/} -type f -name "*.patch" | sort -n); do patch -p1 < $p || echo "Failed to patch $path with $p - expect errors." done 2>/dev/null ' popd ln -s "${_repo_dir}/builder/modules.d" "${_root_dir}/modules.d" fi set +o errexit ## endregion # shellcheck source=./dnbd3-rootfs/scripts/rebash/core.sh source "${_root_dir}/modules.d/dnbd3-rootfs/scripts/rebash/core.sh" core.import exceptions core.import logging core.import utils core.import change_root # endregion # region properties # shellcheck disable=SC2034 build_initramfs__doc_test_setup__='exceptions.activate' file_path='/boot/initramfs.img' dracut_parameter=(--force --no-hostonly) verbose='no' debug='no' target='' cleanup='no' full_cleanup='no' use_systemd_in_initramfs='no' # shellcheck disable=SC2034 declare -A core_dependencies=( [cat]='print messages' \ [cpio]='pack initramfs' \ ['dhclient arping']='support network connection in resulting initramfs' \ [dirname]='core logic' \ [dmsetup]='create a (temporary) writable layer during boot' \ [grep]='retrieve right boot partition during boot' \ [mktemp]='create save temporary files and dictionaries' \ [pkg-config]='retrieve information of installed development packages' \ [tee]='read from standard input and write to standard output and files' \ [readlink]="connect dracut module with dracut's module system" \ [rm]='remove (temporary) files' \ [shift]='parse command line' \ [sed]='process strings' \ ['cmake gcc make']='dynamically compile needed resources against current or given kernel') # shellcheck disable=SC2034 declare -A optional_dependencies=( [chroot]='build against a distribution other than this program runs in' \ ['curl git gzip tar']='dynamically retrieve and unpack missing application which will be compiled for current or given kernel' \ ['mkfs.ext4 fsck']='support for persistent binary diffs in image files') # shellcheck disable=SC2034 declare -A core_shared_library_pattern_dependencies=( [libz]='compile dnbd3 for given or current kernel') # shellcheck disable=SC2034 declare -A optional_shared_library_pattern_dependencies=() # shellcheck disable=SC2034 declare -A core_package_dependencies=() # shellcheck disable=SC2034 declare -A optional_package_dependencies=( ['glib-2.0 pixman-1']='support template systems in container (usually used by virtual runtime environments)') # endregion # region functions ## region command line interface print_usage_message() { # shellcheck disable=SC2016,SC2034 local __doc__=' Prints a description about how to use this program. >>> print_usage_message &>/dev/null ' logging.cat << EOF This program provides a generic way to install systemd based remote linux initramfs. EOF } print_usage_examples() { # shellcheck disable=SC2016,SC2034 local __doc__=' Prints a description about how to use this program by providing examples. >>> print_usage_examples &>/dev/null ' logging.cat << EOF Start install progress: >>> ./build_initramfs.sh EOF } print_command_line_option_description() { # shellcheck disable=SC2016,SC2034 local __doc__=' Prints descriptions about each available command line option. >>> print_command_line_option_description &>/dev/null ' logging.cat << EOF -h --help Shows this help message. -v --verbose Tells you what is going on (default: "$verbose"). -d --debug Gives you any output from all tools which are used (default: "$debug"). -p --file-path Target location for initramfs file (default: "$file_path"). -c --cleanup Removes all distribution specific compiled files. -f --full-cleanup Removes all retrieved and compiled files (usefull to retrieve latest version of all nested modules). -t --target Creates an image against given target template filesystem. If not explicitly specified current system will be used as template system (default: "$target"). -i --init Initializes the various repositories and build the required dependencies but do not build the initramfs. Use this to accelerate subsequent calls of this script. -k --kernel-version Creates an image for the given kernel version. Will require the presence of kernel headers for this version. (default: "$(uname -r)") -s --use-systemd-in-initramfs Use Systemd as init process in initramfs (improved performance but less features) (default: "$use_systemd_in_initramfs"). Additional dracut parameter and normal parameter can be added by delimiting them via a single dash (-) (default: "$dracut_parameter" with "--modules" and dynamically determined modules. Additional custom modules are concatenated.). EOF } print_help_message() { # shellcheck disable=SC2016,SC2034 local __doc__=' Provides a help message for this module. >>> print_help_message &>/dev/null ' logging.plain "\nUsage: $0 [options]\n" print_usage_message "$@" logging.plain '\nExamples:\n' print_usage_examples "$@" logging.plain -e '\nOption descriptions:\n' print_command_line_option_description "$@" logging.plain } parse_command_line() { # shellcheck disable=SC2016,SC2034 local __doc__=' Provides the command line interface and interactive questions. >>> parse_command_line -h &>/dev/null >>> echo "$verbose" no >>> logging.get_level critical >>> parse_command_line -v >>> echo "$verbose" >>> logging.get_level yes info >>> echo "$debug" no >>> parse_command_line --debug >>> echo "$debug" >>> logging.get_level yes debug >>> parse_command_line -p +doc_test_capture_stderr +doc_test_contains +doc_test_ellipsis Error with given option "-p": Traceback (most recent call first): ... >>> echo "$file_path" /boot/initramfs.img >>> parse_command_line -p /tmp/test.img >>> echo "$file_path" /tmp/test.img >>> echo "$cleanup" no >>> parse_command_line --cleanup >>> echo "$cleanup" yes >>> echo "$full_cleanup" no >>> parse_command_line --full-cleanup >>> echo "$full_cleanup" yes >>> echo "$full_cleanup" no >>> parse_command_line --full-cleanup >>> echo "$full_cleanup" yes >>> echo "$use_systemd_in_initramfs" no >>> parse_command_line -s >>> echo "$use_systemd_in_initramfs" yes >>> parse_command_line -t; echo $? +doc_test_capture_stderr +doc_test_contains +doc_test_ellipsis Error with given option "-t": Traceback (most recent call first): ... >>> [[ "$target" = "" ]] >>> parse_command_line -t /tmp/ >>> echo "$target" /tmp/ >>> echo "${dracut_parameter[@]}" --force --no-hostonly >>> parse_command_line - --test >>> echo "${dracut_parameter[@]}" --force --no-hostonly --test >>> parse_command_line - --install "vim htop" >>> echo "${dracut_parameter[3]}" vim htop >>> parse_command_line --no-available-option; echo $? +doc_test_capture_stderr +doc_test_contains +doc_test_ellipsis Error with given option "--no-available-option": Traceback (most recent call first): ... ' while true; do case "$1" in -h|--help) shift print_help_message "$0" exit 0 ;; -v|--verbose) shift verbose='yes' ;; -d|--debug) shift debug='yes' ;; -p|--file-path) local given_argument="$1" shift file_path="$1" if [[ "$file_path" == '' ]]; then logging.critical \ "Error with given option \"$given_argument\": This option needs a path to save initramfs image to." return 1 fi shift ;; -c|--cleanup) shift cleanup='yes' ;; -f|--full-cleanup) shift full_cleanup='yes' ;; -s|--use-systemd-in-initramfs) shift use_systemd_in_initramfs='yes' ;; -t|--target) local given_argument="$1" shift target="$1" if [[ "$target" == '' ]]; then logging.critical \ "Error with given option \"$given_argument\": This option needs a path create initramfs from." return 1 fi shift ;; -i|--init) shift initialize='yes' ;; -k|--kernel-version) local given_argument="$1" shift kernel_version="$1" if [ -z "$kernel_version" ]; then logging.critical \ "Error with given option \"$given_argument\": This option needs a kernel version to build the initramfs for." return 1 fi shift ;; -H|--kernel-headers) local given_argument="$1" shift kernel_headers="$1" if [ -z "$kernel_headers" ]; then logging.critical \ "Error with given option \"$given_argument\": This option needs the path to the kernel headers." return 1 fi shift ;; -q|--qcow-handler) local given_argument="$1" shift qcow_handler="$1" if [ -z "$qcow_handler" ]; then logging.critical \ "Error with given option \"$given_argument\": This options needs to be either 'xmount' or 'kernel'." return 1 fi shift ;; -u|--update) shift update='yes' ;; -) shift while [[ "$1" =~ ^.+$ ]]; do dracut_parameter+=("$1") shift done ;; '') break ;; *) logging.critical \ "Error with given option \"$1\": This argument is not available." return 1 esac done if [ "$verbose" == 'yes' ]; then logging.set_level info fi if [ "$debug" == 'yes' ]; then logging.set_level debug fi # NOTE: Remove the following line if this sanity check should be performed. return 0 if [[ "$UID" != '0' ]]; then logging.critical \ "You have to run this script as \"root\" not as \"${USER}\"." exit 2 fi return 0 } ## endregion ## region helper dependency_check() { # shellcheck disable=SC2016,SC2034 local __doc__=' Check for given dependencies with given dependency checker and log corresponding messages. Example: `dependency_check core dependencies utils_dependency_check program` ' local result=0 # shellcheck disable=SC2016,SC1004 eval 'for dependency_group in "${!'"$1"'_'"$2"'[@]}"; do # NOTE: If "dependency_check_result" would be marked as local it is # empty later. dependency_check_result="$($3 $dependency_group)" || \ local return_code=$? if [[ $return_code == 1 ]]; then echo "$dependency_check_result" return $return_code elif [[ $return_code == 2 ]]; then while read dependency; do eval "local reason=\${${1}_${2}[\"\$dependency_group\"]}" local message="Missing $1 $4 dependency \"$dependency\" needed to $reason." if [[ $1 == core ]]; then logging.critical "$message" else logging.warn "$message" fi done <<< $dependency_check_result result=2 fi return_code=0 done' return $result } initialize_dracut() { # shellcheck disable=SC2016,SC2034 local __doc__=' Downloads and compiles dracut. Example: `initialize_dracut` ' # First check what version to get # Autodetect the kmod version present on the system to decide which dracut version to get # * v47 requires kmod >= 23 (Available in Ubuntu 18.04) # * v46 works with kmod == 20 (CentOS 7.5 only provides kmod v20) if [ "$(pkg-config --modversion libkmod)" -ge 23 ]; then dracut_version="047" else dracut_version="046" fi dracut_resource_url="https://www.kernel.org/pub/linux/utils/boot/dracut/dracut-$dracut_version.tar.gz" if [[ ! -f "${_root_dir}/dracut/install/dracut-install" ]]; then mkdir --parents "${_root_dir}/dracut" logging.info "Download and extract dracut version $dracut_version" curl --location "$dracut_resource_url" | tar --extract --gzip \ --directory "${_root_dir}/dracut" --strip-components 1 pushd "${_root_dir}/dracut" # NOTE: On virtualbox shared folder symlinks are not allowed. # NOTE: make the dracut-install binary (dracut-install resolves # dependencies etc.) logging.info 'Compiling dracut.' ./configure make install/dracut-install # NOTE: We have to copy the binary to current instead of symlinking # them since this feature isn't supported in shared virtual box machine # folders. # If symlinks would be available we could simply use: # >>> make dracut-install popd fi cp "${_root_dir}/dracut/install/dracut-install" \ "${_root_dir}/dracut/dracut-install" return $? } cleanup() { # shellcheck disable=SC2016,SC2034 local __doc__=' Removes distribution specific generated files. Example: `cleanup` ' local plugin_path plugin_path="${_root_dir}/modules.d/dnbd3-rootfs" # shellcheck disable=SC1090 source "${plugin_path}/module-setup.sh" # shellcheck disable=SC2034 moddir="$(cd "$plugin_path" &>/dev/null && pwd)" clean return $? } ## endregion # endregion # region controller main() { # shellcheck disable=SC2016,SC2034 local __doc__=' Main Entry point for the build initramfs logic. Triggers command line parsing and calls sub routines depending on given command line arguments. Example: `main` ' exceptions.activate # region dependency checks and command line parsing result=0 dependency_check core dependencies utils_dependency_check program || \ result=$? dependency_check core shared_library_pattern_dependencies \ utils_dependency_check_shared_library 'shared library' || result=$? dependency_check core package_dependencies utils_dependency_check_pkgconfig \ package || result=$? [[ $result == 0 ]] || exit $result logging.set_commands_level debug logging.set_level critical if ! parse_command_line "$@"; then print_help_message "$0" exit 1 fi dependency_check optional dependencies utils_dependency_check program || \ result=$? dependency_check optional shared_library_pattern_dependencies \ utils_dependency_check_shared_library 'shared library' || result=$? dependency_check optional package_dependencies \ utils_dependency_check_pkgconfig package || result=$? [[ $result == 1 ]] && exit $result # endregion # region sanity check kernel version and qcow handler # if no kernel was specified as dracut argument, use the running kernel's version logging.info "Building for:" if [ -z "$kernel_version" ]; then kernel_version="$(uname -r)" fi logging.info " * kernel version: $kernel_version" # similar for kernel headers needed to compile dnbd3 against. if [ -z "$kernel_headers" ]; then kernel_headers="/lib/modules/${kernel_version}/build" fi if [ ! -f "${kernel_headers}/Makefile" ]; then logging.critical 'Missing core dependency "linux-headers" for version to compile against given or current kernel.' fi logging.info " * kernel headers: $kernel_headers" [ -n "$qcow_handler" ] && logging.info " * qcow2 handler: $qcow_handler" # endregion # region handle delegated operations to specified target if [[ "$target" != '' ]]; then _target="$target" if [[ -f "$target" ]]; then _target="$(mktemp --directory)" _xmount_mountpoint_target="$(mktemp --directory)" xmount --in qemu "$target" --out raw "$_xmount_mountpoint_target" _xmount_device_target="$(losetup --find)" losetup --partscan "$_xmount_device_target" \ "${_xmount_mountpoint_target}/"*.dd # TODO Use partscan mount "$_xmount_device_target" "$_target" fi if [[ -d "$_target" ]]; then _temporary_working_directory="$(chroot "$_target" mktemp --directory)" mount --bind "$(pwd)" "${_target}${_temporary_working_directory}" _parameter_skip=false _parameter_to_forward=() for _parameter; do if $_parameter_skip; then _parameter_skip=false elif [[ "$_parameter" == '-t' ]] || \ [[ "$_parameter" == '--target' ]]; then _parameter_skip=true else _parameter_to_forward+=("$_parameter") fi done # NOTE: We would have to temporary patch dracut to avoid removing the # environment variables "LD_LIBRARY_PATH" and "LD_PRELOAD" to get # "fakechroot" working with dracut. So we should remove this variables # before running the patched dracut version to follow the initial # intention. You should first do: # >>> unset LD_LIBRARY_PATH # >>> unset LD_PRELOAD # and patch "dracut.sh" temporary to comment out: # >>> unset LD_LIBRARY_PATH # >>> unset LD_PRELOAD # To avoid to broke the "fakechroot" environment pipe the ldconfig call # to the native one: # >>> FAKECHROOT_CMD_SUBST=/usr/bin/ldconfig=/usr/bin/ldconfig # shellcheck disable=SC2086 change_root "${_target}" \ "${_temporary_working_directory}/${BASH_SOURCE[0]}" \ ${_parameter_to_forward[*]} mv "${_target}/$file_path" "$file_path" # Do cleanup procedures. umount "${_target}${_temporary_working_directory}" rm --recursive --force "${_target}${_temporary_working_directory}" if [[ -f "$target" ]]; then umount "$_target" losetup --detach "$_xmount_device_target" umount "$_xmount_mountpoint_target" rm --recursive --force "$_target" "$_xmount_mountpoint_target" fi fi exit 0 fi # endregion # region handle '--update' to update all the modules in 'modules.d' if [ "$update" == "yes" ]; then pushd "${_repo_dir}" git pull popd fi # endregion # region handle dependencies which can be resolved automatically logging.info 'Checking dracut.' if [[ ! -f "${_root_dir}/dracut/dracut-install" ]]; then logging.info "Dracut isn't available yet loading it." initialize_dracut fi for _dracut_module in "${_root_dir}/modules.d/"*; do [ -d "${_dracut_module}" ] || continue _dracut_module="$(basename $_dracut_module)" # shouldn't we use absolute paths here? _dracut_module_relative_path="../../modules.d/${_dracut_module}" # TODO change to *not* always use '90' ... _dracut_module_target="${_root_dir}/dracut/modules.d/00${_dracut_module}" if [[ ! -L "$_dracut_module_target" || "$(readlink \ "$_dracut_module_target")" != "$_dracut_module_relative_path" ]]; then logging.info \ "Link ${_dracut_module} plugin into dracut modules folder ($_dracut_module_relative_path -> $_dracut_module_target)." if ! ln --symbolic --force "$_dracut_module_relative_path" \ "$_dracut_module_target"; then logging.warn \ "Linking \"$_dracut_module_relative_path\" to \"$_dracut_module_target\" failed." \ " We will copy them. So we have to recopy it every time to ensure that recompiled things take effect." cp --recursive --force --no-target-directory \ "${_root_dir}/modules.d/$_dracut_module" \ "$_dracut_module_target" fi fi done # endregion # region prepare modules and perform final dracut call _loglevel='' if [ "$verbose" == 'yes' ]; then _loglevel='--verbose' fi _modules='dnbd3-rootfs conf-tgz' logging.info "Default modules: ${_modules}" if [ "$debug" == 'yes' ]; then _loglevel="$_loglevel --stdlog 4" _modules="$_modules i18n terminfo" fi if [ "$use_systemd_in_initramfs" == 'yes' ]; then _modules="$_modules systemd systemd-initrd dracut-systemd" fi # Preprocess done - start build, cleanup or full cleanup if [[ "$full_cleanup" == 'yes' ]]; then logging.info 'Removing all modules.' rm "${_root_dir}/modules.d" \ "${_root_dir}/dracut" --recursive --force elif [[ "$cleanup" == 'yes' ]]; then logging.info 'Removing distribution specific files.' cleanup elif [[ "$initialize" == 'yes' ]]; then . "${_root_dir}/modules.d/dnbd3-rootfs/helper/build.inc" build_initialize_components else logging.info 'Build initramfs.' # NOTE: We deactivate our exception handle since dracut returns "1" if # it is launched with help parameter ("-h" or "--help"). exceptions.deactivate # NOTE: We temporary allow dracut to forward all messages since we # forward all logging configurations. _commands_log_level_backup="$(logging.get_commands_level)" _log_level_backup="$(logging.get_level)" logging.set_level debug logging.set_commands_level debug # shellcheck disable=SC2086 "${_root_dir}/dracut/dracut.sh" --local \ $_loglevel --modules "$_modules" --conf /etc/dracut.conf \ --confdir /etc/dracut.conf.d "${dracut_parameter[@]}" \ --kver "${kernel_version}" "$file_path" _return_code=$? logging.set_commands_level "$_commands_log_level_backup" logging.set_level "$_log_level_backup" if [[ "$_return_code" != 0 ]]; then logging.error 'Building initial ram file system failed.' exit 1 fi exceptions.activate # NOTE: dracut generate the initramfs with 0600 permissions chmod 0644 "${file_path}" fi # endregion exceptions.deactivate } # endregion if core.is_main; then main "$@" fi # region vim modline # vim: set tabstop=4 shiftwidth=4 expandtab: # vim: foldmethod=marker foldmarker=region,endregion: # endregion