#!/usr/bin/env bash # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. set -o pipefail ## Set common variables # declare -r arkdep_dir="$(readlink -m $ARKDEP_ROOT/arkdep)" # Override arkdep_boot if set, if not assume located inside of root if [[ -n $ARKDEP_BOOT ]]; then declare -r arkdep_boot="$(readlink -m $ARKDEP_BOOT)" else declare -r arkdep_boot="$(readlink -m $ARKDEP_ROOT/boot)" fi # if ARKDEP_BOOT is set also set ARKDEP_NO_BOOTCTL [[ -n $ARKDEP_BOOT ]] && declare -r ARKDEP_NO_BOOTCTL=1 if [[ ! -d $arkdep_dir ]] && [[ ! $1 == 'init' ]]; then printf "\e[1;31m<#>\e[0m\e[1m Arkep does not seem to be managing this system or the provided file path is incorrect for $arkdep_dir was not found\e[0m\n" exit 1 fi ## Load config file, unless we are running init # if [[ ! $1 == 'init' ]]; then source $arkdep_dir/config # Set default variables if config variables are undefined [[ -z ${enable_overlay+x} ]] && enable_overlay=1 && printf '\e[1;33m\e[0m\e[1m enable_overlay not defined in config, using default\e[0m\n' [[ -z ${repo_url+x} ]] && repo_url='https://arkanelinux.org/arkdep' && printf '\e[1;33m\e[0m\e[1m repo_url not defined in config, using default\e[0m\n' [[ -z ${repo_default_image+x} ]] && repo_default_image='arkanelinux' && printf '\e[1;33m\e[0m\e[1m repo_default_image not defined in config, using default\e[0m\n' [[ -z ${deploy_keep+x} ]] && deploy_keep=3 && printf '\e[1;33m\e[0m\e[1m deploy_keep not defined in config, using default\e[0m\n' [[ -z ${clean_cache_on_remove+x} ]] && clean_cache_on_remove=1 && printf '\e[1;33m\e[0m\e[1m clean_cache_on_remove not defined in config, using default\e[0m\n' [[ -z ${always_healthcheck+x} ]] && always_healthcheck=1 && printf '\e[1;33m\e[0m\e[1m always_healthcheck not defined in config, using default\e[0m\n' [[ -z ${gpg_signature_check+x} ]] && gpg_signature_check=1 && printf '\e[1;33m\e[0m\e[1m gpg_signature_check not defined in config, using default\e[0m\n' [[ -z ${minimum_available_boot_storage+x} ]] && minimum_available_boot_storage=153600 && printf '\e[1;33m\e[0m\e[1m minimum_available_boot_storage not defined in config, using default\e[0m\n' [[ -z ${minimum_available_root_storage+x} ]] && minimum_available_root_storage=12582912 && printf '\e[1;33m\e[0m\e[1m minimum_available_root_storage not defined in config, using default\e[0m\n' [[ -z ${update_cpu_microcode+x} ]] && update_cpu_microcode=1 && printf '\e[1;33m\e[0m\e[1m update_cpu_microcode not defined in config, using default\e[0m\n' [[ -z ${backup_user_accounts+x} ]] && backup_user_accounts=1 && printf '\e[1;33m\e[0m\e[1m backup_user_accounts not defined in config, using default\e[0m\n' [[ -z ${latest_image_always_default+x} ]] && latest_image_always_default=0 && printf '\e[1;33m\e[0m\e[1m latest_image_always_default not defined in config, using default\e[0m\n' [[ -z ${var_migrate_files+x} ]] && var_migrate_files=('/usrlocal' '/usrliblocale' '/opt' '/srv' '/nm-system-connections' '/lib/AccountsService' '/lib/bluetooth' '/lib/NetworkManager' '/lib/arkane') && printf '\e[1;33m\e[0m\e[1m var_migrate_files not defined in config, using default\e[0m\n' [[ -z ${load_extensions+x} ]] && load_extensions=0 && printf '\e[1;33m\e[0m\e[1m load_extensions not defined in config, using default\e[0m\n' [[ -z ${remove_tar_after_deployment+x} ]] && remove_tar_after_deployment=1 && printf '\e[1;33m\e[0m\e[1m remove_tar_after_deployment not defined in config, using default\e[0m\n' fi ## Common functions # # Cleanup and quit if error cleanup_and_quit () { # If any paramters are passed we will assume it to be an error message [[ -n $1 ]] && printf "\e[1;31m<#>\e[0m $*\e[0m\n" >&2 # Ensure we do not try to remove our current deployment if [[ ! -z ${data[0]+x} ]]; then if grep -q ${data[0]} /proc/cmdline; then printf '\e[1;33m\e[0m\e[1m Cleanup target is current active deployment, skipping\e[0m\n' exit 1 fi fi # Remove the subvolume we were working on # TODO: Make this a generic function and share with the removal of old images? if [[ -n ${data[0]} ]]; then btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs/etc ro false btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs/var ro false btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs ro false btrfs subvolume delete $arkdep_dir/deployments/${data[0]}/rootfs/etc btrfs subvolume delete $arkdep_dir/deployments/${data[0]}/rootfs/var btrfs subvolume delete $arkdep_dir/deployments/${data[0]}/rootfs rm -rfv $arkdep_dir/deployments/${data[0]} \ $arkdep_boot/arkdep/${data[0]} rm -v $arkdep_dir/cache/${data[0]}-*.img \ $arkdep_boot/loader/entries/${data[0]}.conf fi exit 1 } ## Healthcheck # # Set common variables for healthcheck and cleanup, # only set all these vars if they will actually be used if [[ always_healthcheck -eq 1 ]] || [[ $1 =~ ^(healthcheck|cleanup) ]]; then # Gather tracked deployments declare -r tracker=($(cat $arkdep_dir/tracker)) declare -r deployed=($(ls $arkdep_dir/deployments/)) declare untracked=${deployed[@]} # Check for hanging cache files declare -r cached=($(ls $arkdep_dir/cache/)) declare hanging_cache=() # Generate grep regex for cache check declare cache_regex=$(printf "|%s" "${tracker[@]}") cache_regex=${cache_regex:1} # Compare items in tracker to actual deployed for tracked in ${tracker[@]}; do untracked=("${untracked[@]/$tracked}") done for cached_item in ${cached[@]}; do hanging_cache+=($(echo $cached_item | grep -v -E "$cache_regex")) done # Clean whitespaces untracked=($(echo ${untracked[@]} | xargs)) fi # Check for and report on any issues such as untracked deployments or hanging files in cache healthcheck () { if [[ -n $untracked ]]; then printf '\e[1;33m\e[0m\e[1m The following deployments were found but are untracked\n\e[0m' for t in ${untracked[@]}; do printf "$t\n" done fi if [[ -n $hanging_cache ]]; then printf '\e[1;33m\e[0m\e[1m The following hanging images were found in cache\n\e[0m' for t in ${hanging_cache[@]}; do printf "$t\n" done fi # Warn if gpg check is enabled but no keys are installed if [[ ! $gpg_signature_check -eq 0 ]] && [[ ! -s $arkdep_dir/keys/trusted-keys ]]; then printf "\e[1;33m\e[0m\e[1m gpg_signature_check is enabled but $arkdep_dir/keys/trusted-keys does not exist or is empty\n\e[0m" fi # If $1 is healthcheck it was manually called by the user [[ $1 == 'healthcheck' ]] && exit 0 } cleanup () { if [[ -n $untracked ]]; then printf '\e[1;34m-->\e[0m\e[1m Cleaning up untracked deployments\e[0m\n' for target in ${untracked[@]}; do if [[ $target == *recovery* ]]; then printf '\e[1;33m\e[0m\e[1m Detected untracked recovery entry, ignoring\n\e[0m' continue fi # Ensure deployment is not currently active if grep -q "$arkdep_dir/deployments/$target/rootfs" /proc/cmdline; then printf '\e[1;33m\e[0m\e[1m Target is currently active deployment\n\e[0m' continue fi # Remove bootloader entry rm -rfv $arkdep_boot/loader/entries/$target.conf rm -rfv $arkdep_boot/arkdep/$target # Ensure the deployment and all sub-volumes are writable for volume in $(btrfs subvolume list / | grep -oE '[^ ]+$' | grep $target); do btrfs property set -f -ts $(readlink -m $ARKDEP_ROOT/$volume) ro false || printf "failed to make subvol $volume writable\n" done # Remove the deployment rm -rf $arkdep_dir/deployments/$target done fi if [[ -n $hanging_cache ]]; then printf '\e[1;34m-->\e[0m\e[1m Cleaning up hanging cache\e[0m\n' for target in ${hanging_cache[@]}; do rm -v $arkdep_dir/cache/$target done fi exit 0 } # Always healthcheck on run if requested in config, unless the user explicitely called it [[ $always_healthcheck -eq 1 ]] && [[ ! $1 == 'healthcheck' ]] && healthcheck ## Error checking # # Quit if not root, only run if required if [[ ! $1 =~ ^(get-available|healthcheck) ]]; then if [[ ! $EUID -eq 0 ]]; then printf '\e[1;31m<#>\e[0m\e[1m This program has to be run as root\n\e[0m' && exit 1 fi fi # Check if all dependencies are installed, quit if not for prog in btrfs wget dracut bootctl curl gpg gpgv; do # If ARKDEP_NO_BOOTCTL defined do not enforce bootctl requirement if [[ $prog == 'bootctl' ]] && [[ $ARKDEP_NO_BOOTCTL -eq 1 ]]; then break fi if ! command -v $prog > /dev/null; then printf "\e[1;31m<#>\e[0m\e[1m Failed to locate $prog, ensure it is installed\e[0m\n" # Do not immediately exit to log all missing programs err=1 fi [[ $err ]] && exit 1 done # Ensure minimum required storage is available, only run if new deployment will be made if [[ $1 == 'deploy' ]]; then declare boot_storage_available=($(df --output=avail $arkdep_boot)) boot_storage_available=${boot_storage_available[1]} declare root_storage_available=($(df --output=avail $ARKDEP_ROOT/)) root_storage_available=${root_storage_available[1]} # Check amount of available boot storage, do not run if set to 0 if [[ $boot_storage_available -lt $minimum_available_boot_storage ]] && [[ $minimum_available_boot_storage -ne 0 ]]; then printf "\e[1;31m<#>\e[0m\e[1m Less than ${minimum_available_boot_storage}Kib available on boot partition\e[0m\n" exit 1 fi # Check amount of available root storage, do not run if set to 0 if [[ $root_storage_available -lt $minimum_available_root_storage ]] && [[ $minimum_available_root_storage -ne 0 ]] ; then printf "\e[1;31m<#>\e[0m\e[1m Less than ${minimum_available_root_storage}Kib available on root partition\e[0m\n" exit 1 fi fi ## Core functions # # Initialize the system for arkdep init () { # Ensure systemd-boot is installed before continuing, for it is the only thing we support # Do not run if ARKDEP_NO_BOOTCTL defined if [[ ! $ARKDEP_NO_BOOTCTL -eq 1 ]]; then bootctl -q is-installed || cleanup_and_quit 'systemd-boot seems to not be installed' else printf '\e[1;33m\e[0m\e[1m Not running bootctl is-installed because overwritten with ARKDEP_NO_BOOTCTL\e[0m\n' fi printf '\e[1;34m-->\e[0m\e[1m Initializing arkdep\e[0m\n' [[ -d $arkdep_dir ]] && cleanup_and_quit "$arkdep_dir already exists" # Create the /arkdep subvolume printf "\e[1;34m-->\e[0m\e[1m Creating $arkdep_dir subvolume\e[0m\n" btrfs subvolume create $arkdep_dir || cleanup_and_quit "Failed to create btrfs subvolume" # Create directory structure printf '\e[1;34m-->\e[0m\e[1m Creating directory structure\e[0m\n' mkdir -pv $arkdep_dir/deployments \ $arkdep_dir/deployments \ $arkdep_dir/cache \ $arkdep_dir/templates \ $arkdep_dir/overlay \ $arkdep_dir/keys \ $arkdep_dir/extensions \ $arkdep_dir/shared || cleanup_and_quit "Failed to create $arkdep_dir and related directories" # Create empty database files touch $arkdep_dir/tracker touch $arkdep_dir/keys/trusted-keys # Add home shared subvolume and make writable btrfs subvolume create $arkdep_dir/shared/home || cleanup_and_quit "Failed to create home subvolume" btrfs subvolume create $arkdep_dir/shared/root || cleanup_and_quit "Failed to create root subvolume" btrfs subvolume create $arkdep_dir/shared/flatpak || cleanup_and_quit "Failed to create flatpak subvolume" btrfs property set -f -ts $arkdep_dir/shared/home ro false btrfs property set -f -ts $arkdep_dir/shared/root ro false btrfs property set -f -ts $arkdep_dir/shared/flatpak ro false # Ensure permissions on root home directory are set properly chmod 700 $arkdep_dir/shared/root # Write default config file printf '\e[1;34m-->\e[0m\e[1m Adding default config file\e[0m\n' cat <<- END > $arkdep_dir/config # Write /arkdep/overlay overlay to new deployments enable_overlay=1 # URL to image repository, do not add trailing slash repo_url='https://repo.arkanelinux.org/arkdep' # Default image pulled from repo if nothing defined repo_default_image='arkanelinux' # Keep the latest N deployments, remove anything older deploy_keep=3 # Remove images from the cache when their deployments are removed clean_cache_on_remove=1 # Check for untracked deployments and other issues on run always_healthcheck=1 # Perform a GPG signature check on remote sources # 1 = enabled but optional, 2 = required gpg_signature_check=1 # Minimum amount of storage which needs to be available on /boot in Kib minimum_available_boot_storage=153600 # Minimum amount of storage which needs to be available on / in Kib minimum_available_root_storage=12582912 # Update CPU firmware if newer version available update_cpu_microcode=1 # Automatically make a copy of passwd, shadow and group files if they differ from overlay backup_user_accounts=1 # Ensure latest image as defined in the external database is always the default systemd-boot boot entry latest_image_always_default=0 # List of files and folders to be recursively copied over from var to new var, path should start with / var_migrate_files=('/usrlocal' '/usrliblocale' '/opt' '/srv' '/nm-system-connections' '/lib/AccountsService' '/lib/bluetooth' '/lib/NetworkManager' '/lib/arkane') # Load script extensions from /arkdep/extensions load_extensions=0 # Remove tarball from cache once deployment is finished remove_tar_after_deployment=1 END # Add default bootloader config file cat <<- END > $arkdep_dir/templates/systemd-boot title Arkane GNU/Linux - Arkdep linux /arkdep/%target%/vmlinuz initrd /amd-ucode.img initrd /intel-ucode.img initrd /arkdep/%target%/initramfs-linux.img options root="LABEL=arkane_root" rootflags=subvol=/arkdep/deployments/%target%/rootfs rw END exit 0 } teardown () { cat <<- END WARNING: Removing arkdep may leave your system in an unbootable state and you may have to manually reconfigure your bootloader etc.. Only proceed if you know what you are doing! The following changes will be made to your system; - All subvolumes under $arkdep_dir will be deleted - All systemd-boot bootloader entries containing the word "arkdep" will be removed - Kernel and initramfs storage location /boot/arkdep will be removed END # Ensure user knows what they are doing read -p 'Type "I KNOW WHAT I AM DOING" in uppercase to confirm that you know what you are doing: ' input_confirm if [[ $input_confirm == 'I KNOW WHAT I AM DOING' ]]; then printf '\e[1;34m-->\e[0m\e[1m Tearing down arkdep\e[0m\n' # Quit with error if $arkdep_dir does not exist if [[ ! -d $arkdep_dir ]]; then printf "\e[1;31m<#>\e[0m $arkdep_dir does not exist, there is nothing to tear down\n\e[0m" exit 1 fi # Remove all bootloader entries rm -v $(grep -ril arkdep $arkdep_boot/loader/entries) # Remove kernels and initramfs deployed by Arkdep rm -rfv $arkdep_boot/arkdep # Ensure all nested volumes in arkdep are writable and remove for volume in $(btrfs subvolume list / | grep -oE '[^ ]+$' | grep "^$arkdep_dir" | tac); do btrfs property set -f -ts $(readlink -m /$volume) ro false btrfs subvolume delete $volume done else printf '\e[1;33m\e[0m\e[1m Teardown canceled, no changes made to system\e[0m\n' fi exit 0 } remove_deployment () { # Ensure required vars are set if [[ -z $1 ]]; then printf '\e[1;31m<#>\e[0m\e[1m No deployment defined\n\e[0m' exit 1 fi for deployment in ${@:2}; do # Ensure requested deployment is tracked declare hits=($(grep $deployment $arkdep_dir/tracker)) if [[ ${#hits[@]} -gt 1 ]]; then # Check if there is an exact match for hit in ${hits[@]}; do if [[ $1 == $hit ]]; then declare -r exact_match_found=1 # Set first hit to exact match hits[0]=$hit fi done if [[ ! $exact_match_found -eq 1 ]]; then printf '\e[1;33m\e[0m\e[1m Multiple deployments match target, be more specific or provide an exact match\e[0m\n' continue fi elif [[ ${#hits[@]} -lt 1 ]]; then printf '\e[1;33m\e[0m\e[1m No deployments match target\e[0m\n' continue fi declare target=${hits[0]} printf "Removing $target\n" # Ensure deployment is not currently active if grep -q "$arkdep_dir/deployments/$target/rootfs" /proc/cmdline; then printf '\e[1;33m\e[0m\e[1m Target is current active deployment\e[0m\n' continue fi # Remove bootloader entry rm -rfv $arkdep_boot/loader/entries/$target.conf rm -rfv $arkdep_boot/arkdep/$target # Ensure the deployment and all sub-volumes are writable for volume in $(btrfs subvolume list / | grep -oE '[^ ]+$' | grep $target); do btrfs property set -f -ts $(readlink -m $ARKDEP_ROOT/$volume) ro false \ || printf "\e[1;33m\e[0m\e[1m Failed to make subvolume $volume writable\e[0m\n" done # Remove the deployment rm -rf $arkdep_dir/deployments/$target # Remove from tracker grep -v $target $arkdep_dir/tracker > $arkdep_dir/tracker_tmp declare tracker_write_exit_code=$? # Grep may return a 1 if the file is empty if [[ $tracker_write_exit_code -eq 1 ]]; then # No matches, this means file is now empty truncate -s 0 $arkdep_dir/tracker elif [[ $tracker_write_exit_code -eq 2 ]]; then # An error occured in grep cleanup_and_quit 'Failed to update tracker file' fi mv $arkdep_dir/tracker_tmp $arkdep_dir/tracker \ || cleanup_and_quit 'Failed to move tracker_tmp file to tracker' # Remove images from cache if requested if [[ $clean_cache_on_remove -eq 1 ]]; then # Only attempt remove if file exists if ls $arkdep_dir/cache/ | grep $target; then rm -v $arkdep_dir/cache/$target.tar.* fi fi done if [[ $remove_no_quit -ne 1 ]]; then exit 0 fi } # Scrape all variants from from the repo index page get_available () { printf "\e[1;34m-->\e[0m\e[1m Scraping index from $repo_url/\e[0m\n" # Lets first check for server errors, curl does not return an non-zero exit code on server errors declare -r status_code=$(curl -s -o /dev/null --write-out "%{http_code}" $repo_url/) # Error if server returned a status code other than 200 if [[ $status_code -ne 200 ]]; then printf "\e[1;31m<#>\e[0m Server returned a $status_code status code instead of the expected 200 indicating some type of server error\e[0m\n" exit 1 fi # Assumes indexing is available and provided by the webserver is in a non-weird format declare index=($(curl -s $repo_url/ | grep -o 'href=".*"' | # Match only anchor tags grep -v '"/' | # Exclude root and parent directories grep '/"')) # Exclude files # Ensure we had at least a single match if [[ ${#index} -lt 1 ]]; then printf '\e[1;31m<#>\e[0m Index found no matches\e[0m\n' exit 1 fi # Extract variant names from index hits and print for i in ${index[*]}; do i=${i#*\"} echo ${i%/*} done exit 0 } # Deploy a new or update an existing deployment deploy () { # Allow for a clean shutdown, later this is blocked once deployment starts trap 'echo "User interupt received, interupting download"; exit 4' INT TERM # target and version are optional, if not defined default to primary as defined in # /arkdep/config and latest if [[ -n $1 ]] && [[ $1 != '-' ]]; then declare -r deploy_target=$1 else declare -r deploy_target=$repo_default_image fi if [[ -n $2 ]]; then declare -r deploy_version=$2 else declare -r deploy_version='latest' fi # If cache requested version may not be latest if [[ $1 == 'cache' ]] && [[ $deploy_version == 'latest' ]]; then cleanup_and_quit '"latest" and undefined are not a valid version definitions for a cache source' fi printf "\e[1;34m-->\e[0m\e[1m Deploying $deploy_target $deploy_version\e[0m\n" # Split latest_version at the delimiter, creating an array with data.0=package ver, data.1=compression method, data.2=sha1 hash # only run if request target is not cache if [[ $1 != 'cache' ]]; then # If latest is requested grab database and get first line printf '\e[1;34m-->\e[0m\e[1m Downloading database\e[0m\n' if [[ $deploy_version == 'latest' ]]; then declare curl_data=$(curl -sf "${repo_url}/${deploy_target}/database" | head -n 1) elif [[ $deploy_target != 'cache' ]]; then # Only return first hit declare curl_data=$(curl -sf "${repo_url}/${deploy_target}/database" | grep -E "^$2" | head -1) else declare curl_data='cache' fi readarray -d : -t data <<< "$curl_data" # If target is cache else # Find full name in cache, exclude sig files, if no hit quit with error declare cache_hits=($(ls $arkdep_dir/cache | grep -E "^$deploy_version" | grep -v '.sig$')) # Temporary var to store the delimited file found in cache declare data_inter=() # Check if none or more than a single hit, we only expect a single item to match [[ ${#cache_hits[@]} -gt 1 ]] && cleanup_and_quit 'More than a single item in cache matched requested version' [[ ${#cache_hits[@]} -lt 1 ]] && cleanup_and_quit 'No item in cache matched requested version' # Split filename at delimiter readarray -d . -t data_inter <<< "$cache_hits" # Set expected vars for remainder of script data[0]=${data_inter[0]} data[1]=${data_inter[2]} data[2]='-' fi # Ensure none of the vars contain whitespaces data[0]=${data[0]//[$'\t\r\n']} data[1]=${data[1]//[$'\t\r\n']} data[2]=${data[2]//[$'\t\r\n']} # Lets do a bunch of checks to ensure the data is all present if [[ -z ${data[0]+x} ]] || [[ ! -n ${data[0]} ]]; then printf '\e[1;31m<#>\e[0m\e[1m No target found\n\e[0m' exit 1 fi if [[ -z ${data[1]+x} ]] || [[ ! -n ${data[1]} ]]; then printf '\e[1;31m<#>\e[0m\e[1m No compression method found\n\e[0m' exit 1 fi if [[ -z ${data[2]+x} ]] || [[ ! -n ${data[2]} ]]; then # Do not trigger if hash is -, is used for cache deployments if [[ $deploy_target != '-' ]]; then printf '\e[1;31m<#>\e[0m\e[1m No checksum found\n\e[0m' exit 1 fi fi # Lets ensure the requested image is not already deployed if [[ -e $arkdep_dir/deployments/${data[0]} ]]; then # Lets ensure the latest deployment is active, even if it is already deployed if [[ latest_image_always_default -eq 1 ]] && [[ ! $ARKDEP_NO_BOOTCTL -eq 1 ]]; then # Allow it to error, it is no big deal if it does bootctl set-default ${data[0]}.conf || printf '\e[1;33m\e[0m\e[1m Failed to set default bootloader entry on latest_image_always_default\e[0m\n' fi printf "\e[1;33m\e[0m\e[1m ${data[0]} is already deployed, canceling deployment\e[0m\n" exit 0 fi # Check if requested version is already downloaded if [[ -e $arkdep_dir/cache/${data[0]}.tar.${data[1]} ]] && [[ ! -e $arkdep_dir/cache/${data[0]}.tar.${data[1]}.run ]]; then printf "\e[1;34m-->\e[0m\e[1m ${data[0]} already in cache, skipping download\e[0m\n" else printf "\e[1;34m-->\e[0m\e[1m Downloading disk image\e[0m\n" # Download the tarball if not yet downloaded # Write .run file to indicate process is ongoing and not yet finished, can be used to resume download later touch $arkdep_dir/cache/${data[0]}.tar.${data[1]}.run # Start the download systemd-inhibit --who='arkdep deploy' --what='idle:sleep:shutdown' --why='Arkdep is downloading an image' \ wget -c -q --show-progress -P $arkdep_dir/cache/ "$repo_url/$deploy_target/${data[0]}.tar.${data[1]}" || cleanup_and_quit 'Failed to download tarball' # Download GPG signature, only perform check if not disabled by user and keychain exists if [[ ! $gpg_signature_check -eq 0 ]] && [[ -s $arkdep_dir/keys/trusted-keys ]]; then # Download gpg signature if not yet in cache if [[ ! -s $arkdep_dir/cache/${data[0]}.tar.${data[1]}.sig ]]; then wget -c -q --show-progress -P $arkdep_dir/cache/ "$repo_url/$deploy_target/${data[0]}.tar.${data[1]}.sig" sig_exitcode=$? fi if [[ ! $sig_exitcode -eq 0 ]] && [[ $gpg_signature_check -eq 1 ]]; then # Sig download is allowed to fail printf "\e[1;33m\e[0m\e[1m Failed to download GPG signature, signature check will be skipped\e[0m\n" elif [[ ! $sig_exitcode -eq 0 ]] && [[ $gpg_signature_check -eq 2 ]]; then # gpg_signature_check = 2, error and quit the program on fail cleanup_and_quit 'GPG signature check configured to quit on download failure' fi fi # Remove the .run file rm $arkdep_dir/cache/${data[0]}.tar.${data[1]}.run fi if [[ $gpg_signature_check -eq 2 ]] && [[ ! -s $arkdep_dir/cache/${data[0]}.tar.${data[1]}.sig ]]; then # if GPG check required but file not present error and quit cleanup_and_quit 'GPG signature expected but none were provided' elif [[ ! -s $arkdep_dir/cache/${data[0]}.tar.${data[1]}.sig ]]; then skip_gpg_check=1 fi # If not configured to skip by previous error handeling check the signature to the downloaded image if [[ ! $skip_gpg_check -eq 1 ]]; then printf '\e[1;34m-->\e[0m\e[1m Checking GPG signature\e[0m\n' # Perform GPG signature check gpgv --keyring $arkdep_dir/keys/trusted-keys $arkdep_dir/cache/${data[0]}.tar.${data[1]}.sig $arkdep_dir/cache/${data[0]}.tar.${data[1]} || cleanup_and_quit 'gpg check failed' elif [[ ${data[2]} != '-' ]]; then # If GPG check not triggered instead check hash, unless defined as - printf '\e[1;34m-->\e[0m\e[1m Validating integrity\e[0m\n' # Identify used checksum method if [[ ${#data[2]} -eq 40 ]]; then # If it is a sha-1 sha1sum $arkdep_dir/cache/${data[0]}.tar.${data[1]} | grep "${data[2]}" || cleanup_and_quit 'SHA-1 checksum does not match the one defined in database\e[0m\n' elif [[ ${#data[2]} -eq 56 ]]; then # If it is sha-224 sha224sum $arkdep_dir/cache/${data[0]}.tar.${data[1]} | grep "${data[2]}" || cleanup_and_quit 'SHA-224 checksum does not match the one defined in database\e[0m\n' elif [[ ${#data[2]} -eq 64 ]]; then # If it is sha-256 sha256sum $arkdep_dir/cache/${data[0]}.tar.${data[1]} | grep "${data[2]}" || cleanup_and_quit 'SHA-256 checksum does not match the one defined in database\e[0m\n' elif [[ ${#data[2]} -eq 96 ]]; then # If it is sha-384 sha384sum $arkdep_dir/cache/${data[0]}.tar.${data[1]} | grep "${data[2]}" || cleanup_and_quit 'SHA-384 checksum does not match the one defined in database\e[0m\n' elif [[ ${#data[2]} -eq 128 ]]; then # If it is a sha-512 sha512sum $arkdep_dir/cache/${data[0]}.tar.${data[1]} | grep "${data[2]}" || cleanup_and_quit 'SHA-512 Checksum does not match the one defined in database\e[0m\n' else cleanup_and_quit 'Failed to identify SHA checksum type' fi fi # We will now start writing the images to disk, prevent the user from shutting down the script trap '' INT TERM # Check if there is a migration script available if tar -xf $arkdep_dir/cache/${data[0]}.tar.${data[1]} -C $arkdep_dir/cache/ ./${data[0]}-migration.sh 2> /dev/null; then printf '\e[1;34m-->\e[0m\e[1m Running migration script\e[0m\n' # Run the migration script if provided (source $arkdep_dir/cache/${data[0]}-migration.sh) # TODO: Evaluate if this is how we would really like to do this # # Cancel the update once migration is finished, we are assuming the migration script did all the work for us exit 0 fi # Extract the root image if not yet extracted printf '\e[1;34m-->\e[0m\e[1m Writing root\e[0m\n' # Create directory using unique deployment name mkdir -p $arkdep_dir/deployments/${data[0]} || cleanup_and_quit 'Failed to create deployment directory' # Extra image from tarball to stdin and receive tar -xOf $arkdep_dir/cache/${data[0]}.tar.${data[1]} "./${data[0]}-rootfs.img" | btrfs receive $arkdep_dir/deployments/${data[0]} || cleanup_and_quit 'Failed to receive root' # Extract the etc image if not yet extracted printf '\e[1;34m-->\e[0m\e[1m Writing etc\e[0m\n' # Write the etc image and create var directory, we have to unlock rootfs temporarily to do this btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs ro false || cleanup_and_quit 'Failed to unlock root to write etc' # Extra image from tarball to stdin and receive tar -xOf $arkdep_dir/cache/${data[0]}.tar.${data[1]} "./${data[0]}-etc.img" | btrfs receive $arkdep_dir/deployments/${data[0]}/rootfs/ || cleanup_and_quit 'Failed to receive etc' # Unlock the etc deployment btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs/etc ro false || cleanup_and_quit 'Failed to unlock root to write etc' # Write the var image printf '\e[1;34m-->\e[0m\e[1m Writing var\e[0m\n' # Extra image from tarball to stdin and receive tar -xOf $arkdep_dir/cache/${data[0]}.tar.${data[1]} "./${data[0]}-var.img" | btrfs receive $arkdep_dir/deployments/${data[0]}/rootfs/ || cleanup_and_quit 'Failed to receive var' # Make var writable btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs/var ro false || cleanup_and_quit 'Failed to unlock var' # Lock the root volume again btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs ro true || cleanup_and_quit 'Failed to lock root' # Add overlay if enabled if [[ $enable_overlay -eq 1 ]]; then # If backup_user_accounts is enabled automatically perform a backup, do not run if custom root is defined if [[ $backup_user_accounts -eq 1 ]] && [[ ! -n $ARKDEP_ROOT ]]; then printf '\e[1;34m-->\e[0m\e[1m Copying user account files to overlay if changed\e[0m\n' for file in passwd shadow group; do if ! cmp --silent $arkdep_dir/overlay/etc/$file /etc/$file; then cp -v /etc/$file $arkdep_dir/overlay/etc/$file fi done # Ensure shadow file permissions are set properly chmod 600 $arkdep_dir/overlay/etc/shadow fi printf '\e[1;34m-->\e[0m\e[1m Copying overlay to deployment\e[0m\n' declare -r overlay_files=($(ls $arkdep_dir/overlay/)) # Check if only /etc is present, if it is we do not have to unlock the root volume for file in ${overlay_files[*]}; do if [[ $file != 'etc' ]]; then printf "\e[1;33m\e[0m\e[1m Non /etc file or directory detected, root will be temporarily unlocked\e[0m\n" overlay_unlock_root=1 fi done # Unlock root if required if [[ $overlay_unlock_root -eq 1 ]]; then btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs ro false fi cp -rv $arkdep_dir/overlay/* $arkdep_dir/deployments/${data[0]}/rootfs/ # Lock root again if required if [[ $overlay_unlock_root -eq 1 ]]; then btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs ro true fi fi # Migrate specified files and directories if [[ ${#var_migrate_files[@]} -ge 1 ]] && [[ ! -n $ARKDEP_ROOT ]]; then printf '\e[1;34m-->\e[0m\e[1m Migrating local files to new deployment\e[0m\n' for file in ${var_migrate_files[@]}; do printf "Copying /var$file\n" cp -r /var/$file $arkdep_dir/deployments/${data[0]}/rootfs/var/${file%/*} done fi printf '\e[1;34m-->\e[0m\e[1m Installing kernel image to boot\e[0m\n' # Get list of all available kernels kernels_installed=($(ls $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/modules/)) mkdir -p $arkdep_boot/arkdep/${data[0]} # Deploy kernel to /boot, deploy first hit of kernels_installed printf "Copying ${kernels_installed[0]}/vmlinuz\n" cp $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/modules/${kernels_installed[0]}/vmlinuz $arkdep_boot/arkdep/${data[0]}/ || cleanup_and_quit 'Failed to copy kernel image' # Deploy CPU firmware to boot if [[ $update_cpu_microcode -eq 1 ]]; then printf '\e[1;34m-->\e[0m\e[1m Checking for CPU microcode updates\e[0m\n' for ucode in $(ls $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/ | grep ucode); do # If CPU firmware present in both image and install if ! cmp --silent $arkdep_boot/$ucode $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/$ucode; then printf "Copying $ucode\n" cp $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/$ucode $arkdep_boot/$ucode || cleanup_and_quit 'Failed to copy microcode' fi done fi # Install kernel and generate initramfs printf '\e[1;34m-->\e[0m\e[1m Generating initramfs\e[0m\n' dracut -q -k $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/modules/${kernels_installed[0]} \ -c $arkdep_dir/deployments/${data[0]}/rootfs/etc/dracut.conf \ --confdir $arkdep_dir/deployments/${data[0]}/rootfs/etc/dracut.conf.d \ --kernel-image $arkdep_boot/arkdep/${data[0]}/vmlinuz \ --kver ${kernels_installed[0]} \ --force \ $arkdep_boot/arkdep/${data[0]}/initramfs-linux.img || cleanup_and_quit 'Failed to generate initramfs' if [[ $load_extensions -eq 1 ]]; then printf '\e[1;34m-->\e[0m\e[1m Running extensions\e[0m\n' extensions=($(ls $arkdep_dir/extensions/)) for extension in ${extensions[@]}; do (source $arkdep_dir/extensions/$extension) done fi # Add to database printf '\e[1;34m-->\e[0m\e[1m Updating database\e[0m\n' printf "${data[0]}\n$(cat $(readlink -m $arkdep_dir/tracker))" | tee $arkdep_dir/tracker_tmp mv $arkdep_dir/tracker_tmp $arkdep_dir/tracker # Deploy bootloader configuration # also insert newline printf '\n\e[1;34m-->\e[0m\e[1m Adding bootloader entry\e[0m\n' sed "s/%target%/${data[0]}/" $arkdep_dir/templates/systemd-boot > $arkdep_boot/loader/entries/${data[0]}.conf # Set new deployment as default bootloader entry printf '\e[1;34m-->\e[0m\e[1m Setting new bootloader entry as default\e[0m\n' # Do not set default boot entry if ARKDEP_NO_BOOTCTL is set if [[ ! $ARKDEP_NO_BOOTCTL -eq 1 ]]; then bootctl set-default ${data[0]}.conf || cleanup_and_quit "Failed to set default bootloader entry" else printf '\e[1;33m\e[0m\e[1m Not running bootctl set-default because overwritten with ARKDEP_NO_BOOTCTL\e[0m\n' fi # Image deployment finished, allow for interupts again trap 'echo "User interupt received, canceling cleanup step"; exit 4' INT TERM # Check if there is an update script available if tar -xf $arkdep_dir/cache/${data[0]}.tar.${data[1]} -C $arkdep_dir/cache/ ./${data[0]}-update.sh 2> /dev/null && [[ ! -n $ARKDEP_ROOT ]]; then printf '\e[1;34m-->\e[0m\e[1m Running update script\e[0m\n' # Run the migration script if provided (source $arkdep_dir/cache/${data[0]}-update.sh) fi # Remove tarball if configured to remove if [[ $remove_tar_after_deployment -eq 1 ]]; then printf '\e[1;34m-->\e[0m\e[1m Removing tarball from cache\e[0m\n' rm $arkdep_dir/cache/${data[0]}.tar.* fi # Remove entries outside of keep declare -r remove_deployments=($(tail -n +$deploy_keep $arkdep_dir/tracker)) # Remove old deployments if [[ ${#remove_deployments[@]} -ge 1 ]]; then printf '\e[1;34m-->\e[0m\e[1m Cleaning up old deployments\e[0m\n' declare -r remove_no_quit=1 remove_deployment ${remove_deployments[@]} fi exit 0 } [[ $1 == 'init' ]] && init $2 [[ $1 == 'teardown' ]] && teardown [[ $1 == 'get-available' ]] && get_available [[ $1 == 'deploy' ]] && deploy $2 $3 [[ $1 == 'remove' ]] && remove_deployment $@ [[ $1 == 'healthcheck' ]] && healthcheck $1 [[ $1 == 'cleanup' ]] && cleanup # No valid params were provided printf '\e[1;31m<#>\e[0m\e[1m No valid parameters provided\e[0m\n' exit 3