#!/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 ## Distributor configurables # # These are the default settings utilized as both a fallback in case a # setting is not configured and as the default settings a new system will # be initialized with. declare -r dist_enable_overlay=1 declare -r dist_repo_url='https://repo.arkanelinux.org/arkdep' declare -r dist_repo_default_image='arkanelinux' declare -r dist_deploy_keep=3 declare -r dist_clean_cache_on_remove=1 declare -r dist_always_healthcheck=1 declare -r dist_gpg_signature_check=1 declare -r dist_minimum_available_boot_storage=153600 declare -r dist_minimum_available_root_storage=12582912 declare -r dist_update_cpu_microcode=1 declare -r dist_backup_user_accounts=1 declare -r dist_migrate_files="'var/usrlocal' 'var/opt' 'var/srv' 'var/lib/AccountsService' 'var/lib/bluetooth' 'var/lib/NetworkManager' 'var/lib/arkane' 'var/lib/power-profiles-daemon' 'var/db' 'etc/localtime' 'etc/locale.gen' 'etc/locale.conf' 'etc/NetworkManager/system-connections' 'etc/ssh'" declare -r dist_load_extensions=0 declare -r dist_remove_tar_after_deployment=1 declare -r dist_update_diff_style='list' declare -r dist_interactive_mode=1 declare -r dist_package_layer_command='pacman -Syy --needed --noconfirm' declare -r dist_package_unlayer_command='pacman -Rsn --noconfirm' # systemd-boot configuration declare -r boot_entry_title='Arkane Linux - Arkdep' declare -r kernel_params='root="LABEL=arkane_root" rootflags=subvol=/arkdep/deployments/%target%/rootfs rw' ## Check if program is locked, if yes do not run # if [[ -f /tmp/arkdep.lock ]]; then printf '\e[1;31m<#>\e[0m\e[1m /tmp/arkdep.lock exists, another instance of the program might be running\e[0m\n' exit 5 fi ## 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_ROOT is defined we imply confirm [[ -n $ARKDEP_ROOT ]] && declare -r ARKDEP_CONFIRM=1 # Override database name if ARKDEP_DATABASE is defined if [[ -n $ARKDEP_DATABASE ]]; then declare -r database_name=$ARKDEP_DATABASE else declare -r database_name='database' fi if [[ ! -d $arkdep_dir ]] && [[ ! $1 == 'init' ]]; then printf "\e[1;31m<#>\e[0m\e[1m Arkdep 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 if [[ -z ${ARKDEP_CONFIG+x} ]]; then # Ensure file exists if [[ ! -f $arkdep_dir/config ]]; then printf "\e[1;31m<#>\e[0m\e[1m $arkdep_dir/config configuration file does not exist\e[0m\n" exit 1 fi source $arkdep_dir/config else # Ensure file exists if [[ ! -f $ARKDEP_CONFIG ]]; then printf "\e[1;31m<#>\e[0m\e[1m $ARKDEP_CONFIG configuration file does not exist\e[0m\n" exit 1 fi source $ARKDEP_CONFIG fi # Set default variables if config variables are undefined [[ -z ${enable_overlay+x} ]] && enable_overlay=$dist_enable_overlay && 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=$dist_repo_url && 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=$dist_repo_default_image && 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=$dist_deploy_keep && 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=$dist_clean_cache_on_remove && 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=$dist_always_healthcheck && 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=$dist_gpg_signature_check && 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=$dist_minimum_available_boot_storage && 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=$dist_minimum_available_root_storage && 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=$dist_update_cpu_microcode && 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=$dist_backup_user_accounts && printf '\e[1;33m\e[0m\e[1m backup_user_accounts not defined in config, using default\e[0m\n' [[ -z ${migrate_files+x} ]] && migrate_files=($(printf "$dist_migrate_files")) && printf '\e[1;33m\e[0m\e[1m migrate_files not defined in config, using default\e[0m\n' [[ -z ${load_extensions+x} ]] && load_extensions=$dist_load_extensions && 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=$dist_remove_tar_after_deployment && printf '\e[1;33m\e[0m\e[1m remove_tar_after_deployment not defined in config, using default\e[0m\n' [[ -z ${update_diff_style+x} ]] && update_diff_style=$dist_update_diff_style && printf '\e[1;33m\e[0m\e[1m update_diff_style not defined in config, using default\e[0m\n' [[ -z ${interactive_mode+x} ]] && interactive_mode=$dist_interactive_mode && printf '\e[1;33m\e[0m\e[1m interactive_mode not defined in config, using default\e[0m\n' [[ -z ${package_layer_command+x} ]] && package_layer_command=$dist_package_layer_command && printf '\e[1;33m\e[0m\e[1m package_layer_command not defined in config, using default\e[0m\n' [[ -z ${package_unlayer_command+x} ]] && package_unlayer_command=$dist_package_unlayer_command && printf '\e[1;33m\e[0m\e[1m package_unlayer_command not defined in config, using default\e[0m\n' fi ## Common functions # # Cleanup and quit if error cleanup_and_quit () { # If any parameters 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' unlock_and_quit 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 2> /dev/null btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs/var ro false 2> /dev/null btrfs property set -f -ts $arkdep_dir/deployments/${data[0]}/rootfs ro false 2> /dev/null btrfs subvolume delete $arkdep_dir/deployments/${data[0]}/rootfs/etc 2> /dev/null btrfs subvolume delete $arkdep_dir/deployments/${data[0]}/rootfs/var 2> /dev/null btrfs subvolume delete $arkdep_dir/deployments/${data[0]}/rootfs 2> /dev/null rm -rfv $arkdep_dir/deployments/${data[0]} \ $arkdep_boot/arkdep/${data[0]} 2> /dev/null rm -v $arkdep_dir/cache/${data[0]}-*.img \ $arkdep_boot/loader/entries/*${data[0]}*.conf 2> /dev/null fi unlock_and_quit 1 } # Exit and release lock file # Takes exit code as $1 unlock_and_quit () { rm /tmp/arkdep.lock exit $1 } touch /tmp/arkdep.lock # Script-wide trap interupt trap 'echo "User interupt received"; unlock_and_quit 4' INT TERM ## 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' ]] && unlock_and_quit 1 } cleanup () { # Run healthcheck, if always_healthcheck is enabled it will have previously not run healthcheck # Ensure there is actually something to clean up if [[ ${#untracked[@]} -eq 0 ]] && [[ ${#hanging_cache[@]} -eq 0 ]]; then printf '\e[1;33m\e[0m\e[1m There is nothing to clean up\n\e[0m' unlock_and_quit 1 fi if [[ $interactive_mode -eq 1 ]] && [[ $ARKDEP_CONFIRM -ne 1 ]]; then printf "The above listed items will be removed.\n\n" read -p 'Proceed with removal? [Y/n] ' remove_confirm if [[ ! $remove_confirm =~ ^(y|Y|yes|YES|)$ ]]; then unlock_and_quit 1 fi fi 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 [[ -f $arkdep_boot/loader/entries/*$target*.conf ]] && printf "Removing $target bootloader entry\n" && rm -rf $arkdep_boot/loader/entries/*$target*.conf [[ -f $arkdep_boot/arkdep/$target ]] && printf "Removing $arkdep_boot/arkdep/$target\n" && rm -rf $arkdep_boot/arkdep/$target # Ensure the deployment and all sub-volumes are writable for volume in $(btrfs subvolume list / | grep -oE '[^ ]+$' | grep $target); do printf "Unlocking $volume\n" btrfs property set -f -ts $(readlink -m $ARKDEP_ROOT/$volume) ro false || printf "failed to make subvol $volume writable\n" done # Remove the deployment printf "Removing $arkdep_dir/deployments/$target\n" && 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 printf "Removing $arkdep_dir/cache/$target\n" && rm $arkdep_dir/cache/$target done fi unlock_and_quit 0 } # Always healthcheck on run if requested in config, unless the user explicitely called it or the program is going to call it [[ $always_healthcheck -eq 1 ]] && [[ ! $1 =~ ^(healthcheck|cleanup)$ ]] && healthcheck ## Error checking # # Quit if not root, only run if required if [[ ! $1 =~ ^(get-available|diff|healthcheck|layer-ls)$ ]]; 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' && unlock_and_quit 1 fi fi # Check if all dependencies are installed, quit if not for prog in btrfs wget dracut curl gpgv; do 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 ]] && unlock_and_quit 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" unlock_and_quit 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" unlock_and_quit 1 fi fi ## Core functions # # Initialize the system for arkdep, either on a new Linux install # when using ARKDEP_BOOT and ARKDEP_ROOT, or the current system # if called without said variables defined init_new_system () { printf '\e[1;34m-->\e[0m\e[1m Initializing arkdep\e[0m\n' # Ensure Arkdep is not already installed, unless we are installing # to a new system [[ -d $arkdep_dir ]] && [[ -z ${ARKDEP_ROOT+x} ]] && 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=$dist_enable_overlay # URL to image repository, do not add trailing slash repo_url='$dist_repo_url' # Default image pulled from repo if nothing defined repo_default_image='$dist_repo_default_image' # Keep the latest N deployments, remove anything older deploy_keep=$dist_deploy_keep # Remove images from the cache when their deployments are removed clean_cache_on_remove=$dist_clean_cache_on_remove # Check for untracked deployments and other issues on run always_healthcheck=$dist_always_healthcheck # Perform a GPG signature check on remote sources # 1 = enabled but optional, 2 = required gpg_signature_check=$dist_gpg_signature_check # Minimum amount of storage which needs to be available on /boot in Kib minimum_available_boot_storage=$dist_minimum_available_boot_storage # Minimum amount of storage which needs to be available on / in Kib minimum_available_root_storage=$dist_minimum_available_root_storage # Update CPU firmware if newer version available update_cpu_microcode=$dist_update_cpu_microcode # Automatically make a copy of passwd, shadow and group files if they differ from overlay backup_user_accounts=$dist_backup_user_accounts # List of files and folders to be recursively copied over from root tree to new root filesystem migrate_files=($(printf "$dist_migrate_files")) # Load script extensions from /arkdep/extensions load_extensions=$dist_load_extensions # Remove tarball from cache once deployment is finished remove_tar_after_deployment=$dist_remove_tar_after_deployment # Update diff styling, available styles: 'list' update_diff_style='$dist_update_diff_style' # Before making changes to the system show diff and ask for confirmation interactive_mode=$dist_interactive_mode END # Add default bootloader config file cat <<- END > $arkdep_dir/templates/systemd-boot title $boot_entry_title linux /arkdep/%target%/vmlinuz initrd /amd-ucode.img initrd /intel-ucode.img initrd /arkdep/%target%/initramfs-linux.img options $kernel_params END unlock_and_quit 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" unlock_and_quit 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 unlock_and_quit 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' unlock_and_quit 1 fi # Check if remove is called in scriptmode if [[ $remove_scriptmode -eq 1 ]]; then declare -r remove_targets=(${@:1}) else declare -r remove_targets=(${@:2}) fi for deployment in ${remove_targets[@]}; 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 if [[ $interactive_mode -eq 1 ]] && [[ $cleanup_no_confirm -ne 1 ]] && [[ $ARKDEP_CONFIRM -ne 1 ]]; then printf "$target will be removed.\n\n" read -p 'Proceed with removal? [Y/n] ' remove_confirm if [[ ! $remove_confirm =~ ^(y|Y|yes|YES|)$ ]]; then unlock_and_quit 1 fi 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 # Do not exit if run in scriptmode if [[ $remove_scriptmode -ne 1 ]]; then unlock_and_quit 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" # Assumes indexing is available and provided by the webserver is in a non-weird format declare index=($(curl -sf $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' unlock_and_quit 1 fi # Extract variant names from index hits and print for i in ${index[*]}; do i=${i#*\"} echo ${i%/*} done unlock_and_quit 0 } # Process .pkgs files in provided by server and generate update diff between current and another deployment version diff () { # TODO: Very basic implementation, expand later # Set default new variant as diff_target unless param provided if [[ -n $1 ]] && [[ $1 != '-' ]]; then declare -r diff_target=$1 else declare -r diff_target=$repo_default_image fi # Set default variant version if not provided, otherwise override of provided if [[ -n $2 ]] && [[ $2 != '-' ]]; then # Param was provided, set target to param # Get ID of (partially) defined entry declare curl_data=($(curl -sf "$repo_url/$diff_target/$database_name" || printf 'ERROR')) if [[ $curl_data == 'ERROR' ]]; then printf '\e[1;31m<#>\e[0m Failed to download database\e[0m\n' unlock_and_quit 1 fi for db_entry in ${curl_data[@]}; do if [[ $db_entry == $2* ]]; then declare -r database_hit=$db_entry fi done else # No param was provided, set first db entry as target # Get ID of latest entry declare curl_data=($(curl -sf "$repo_url/$diff_target/$database_name" || printf 'ERROR')) if [[ $curl_data == 'ERROR' ]]; then printf '\e[1;31m<#>\e[0m Failed to download database\e[0m\n' unlock_and_quit 1 fi database_hit=${curl_data[0]} fi # Ensure data properly received if [[ ${#database_hit} -eq 0 ]]; then printf 'Failed to find any matching database entries\n' unlock_and_quit 1 fi # Process returned data from database readarray -d : -t data <<< "$database_hit" declare -r deployment_id_new=${data[0]} # Process mountinfo to determine current deployment ID declare mountinfo=($(cat /proc/self/mountinfo | head -n 1)) mountinfo=${mountinfo[3]} # Get subvol location mountinfo=${mountinfo%/*} # Remove everything after ID declare -r deployment_id_old=${mountinfo##*/} # Remove everything before ID # Check if we are already running the latest update if [[ $deployment_id_old == $deployment_id_new ]]; then printf 'Already on latest version\n' unlock_and_quit 0 fi # Get new package list mapfile new_pkgs < <(curl -sf $repo_url/$diff_target/$deployment_id_new.pkgs || printf 'ERROR') # Get old package list mapfile old_pkgs < <(curl -sf $repo_url/$repo_default_image/$deployment_id_old.pkgs || printf 'ERROR') if [[ $new_pkgs == 'ERROR' ]]; then printf '\e[1;31m<#>\e[0m Failed to download new pkgs file\e[0m\n' unlock_and_quit 1 fi if [[ $old_pkgs == 'ERROR' ]]; then printf '\e[1;31m<#>\e[0m Failed to download old pkgs file\e[0m\n' unlock_and_quit 1 fi declare changed=() declare old_ver=() declare new_ver=() declare removed=() declare new=() # Find updated packages for pkg in "${new_pkgs[@]}"; do # Split package name and package versions in to list declare spaced=($pkg) # Find new packages, if a package from the new image is not in the # package list of the current version it is new if [[ ! "${old_pkgs[@]}" =~ "${spaced[0]}" ]]; then new+=("${spaced[0]}") continue fi # Compare new pkgs to old ones for old_pkg in "${old_pkgs[@]}"; do # Split package name and package versions in to list declare old_spaced=($old_pkg) # Find matchings packages, compare versions # If versions do not match they have been updated/changed/downgraded if [[ ${spaced[0]} == ${old_spaced[0]} ]]; then if [[ ${spaced[1]} != ${old_spaced[1]} ]]; then changed+=("${spaced[0]}") old_ver+=("${old_spaced[1]}") new_ver+=("${spaced[1]}") break fi fi done done # Find removed packages for old_pkg in "${old_pkgs[@]}"; do declare old_spaced=($old_pkg) # Compare all packages from new image to current package version, # current package is not in the new_pkgs list it has been removed if [[ ! "${new_pkgs[@]}" =~ "${old_spaced[0]}" ]]; then removed+=("${old_spaced[0]}") fi done # Print changed packages and diff if [[ $update_diff_style == 'list' ]]; then if [[ ${#changed} -ne 0 ]]; then declare num=0 printf 'Changed:\n' while [[ $num -lt ${#changed[@]} ]]; do printf " ${changed[$num]} \e[34m${old_ver[$num]}\e[0m -> \e[32m${new_ver[$num]}\e[0m\n" num=$(($num + 1)) done fi # Print new packages if [[ ${#new[@]} -ne 0 ]]; then printf '\nNew:\n' for n in "${new[@]}"; do printf " $n\n" done fi # Print removed packages if [[ ${#removed[@]} -ne 0 ]]; then printf '\nRemoved:\n' for rem in "${removed[@]}"; do printf " $rem\n" done fi else printf "Update diff style \"$update_diff_style\" does not exist\n" unlock_and_quit 1 fi unlock_and_quit 0 } # Deploy a new or update an existing deployment deploy () { # 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_name" || printf 'ERROR')) [[ $curl_data == 'ERROR' ]] && cleanup_and_quit 'Failed to download database file' declare -r database_hit=${curl_data[0]} elif [[ $deploy_target != 'cache' ]]; then # Only return first hit declare curl_data=($(curl -sf "$repo_url/$deploy_target/$database_name" || printf 'ERROR')) [[ $curl_data == 'ERROR' ]] && cleanup_and_quit 'Failed to download database file' # Find matching database entry for db_entry in ${curl_data[@]}; do if [[ $db_entry == $2* ]]; then declare -r database_hit=$db_entry fi done else declare database_hit='cache' fi readarray -d : -t data <<< "$database_hit" # 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' unlock_and_quit 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' unlock_and_quit 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' unlock_and_quit 1 fi fi # Lets ensure the requested image is not already deployed if [[ -e $arkdep_dir/deployments/${data[0]} ]]; then printf "\e[1;33m\e[0m\e[1m ${data[0]} is already deployed, canceling deployment\e[0m\n" unlock_and_quit 0 fi if [[ $interactive_mode -eq 1 ]] && [[ $ARKDEP_CONFIRM -ne 1 ]]; then printf "${data[0]} from $deploy_target will be deployed.\n\n" read -p 'Proceed with deployment? [Y/n] ' remove_confirm # To skip confirmation on the cleanup step later declare -r cleanup_no_confirm=1 if [[ ! $remove_confirm =~ ^(y|Y|yes|YES|)$ ]]; then unlock_and_quit 1 fi 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) # It is assumed that the migration script will do its own cleanup on migration related files printf '\e[1;34m-->\e[0m\e[1m Removing migration script\e[0m\n' rm $arkdep_dir/cache/${data[0]}-migration.sh unlock_and_quit 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' # 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 subuid subgid; do # Hash old and new file to compare # No need to handle file not exist scenario, we can assume /etc/$file to always exist declare checksum_old=($(sha256sum $arkdep_dir/overlay/etc/$file 2> /dev/null)) declare checksum_current=($(sha256sum /etc/$file 2> /dev/null)) if [[ ! ${checksum_old[0]} == ${checksum_current[0]} ]]; then printf "Copying $file\n" cp /etc/$file $arkdep_dir/overlay/etc/$file || printf "Failed to copy $file\n" 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' cp -rv $arkdep_dir/overlay/* $arkdep_dir/deployments/${data[0]}/rootfs/ fi # Layer addition packages if defined declare -r layer_list=($(cat $arkdep_dir/layer)) if [[ ${#layer_list[@]} -ne 0 ]]; then printf '\e[1;34m-->\e[0m\e[1m Layering additional packages\e[0m\n' # Bind mind, otherwise arch-chroot complains mount --bind $arkdep_dir/deployments/${data[0]}/rootfs $arkdep_dir/deployments/${data[0]}/rootfs # Install the layer packages arch-chroot $arkdep_dir/deployments/${data[0]}/rootfs pacman -S --noconfirm ${layer_list[@]} || printf "\e[1;33m\e[0m\e[1m Error during package installation, validate your package layer configuration\e[0m\n" # Unmount the installation target again unmount $arkdep_dir/deployments/${data[0]}/rootfs fi # Migrate specified files and directories if [[ ${#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 ${migrate_files[@]}; do [[ ! -e /$file ]] && continue printf "Copying $file\n" cp -r /$file $arkdep_dir/deployments/${data[0]}/rootfs/${file%/*} done fi printf '\e[1;34m-->\e[0m\e[1m Locking new root partition\e[0m\n' # 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' 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/)) # Error if no kernels installed if [[ ${#kernels_installed[@]} -eq 0 ]]; then cleanup_and_quit 'Provided image has no kernel installed' fi 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 # Hash current and new file to compare # No need to handle file not exist scenario, we can assume /etc/$file to always exist declare checksum_new=($(sha256sum $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/$ucode)) declare checksum_current=($(sha256sum $arkdep_boot/$ucode)) if [[ ! ${checksum_new[0]} == ${checksum_current[0]} ]]; then printf "Copying $ucode\n" cp $arkdep_dir/deployments/${data[0]}/rootfs/usr/lib/$ucode $arkdep_boot/$ucode || printf "Failed to copy $ucode\n" 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 deployment tracker\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' # Ensure bootloader entry is not already present, if it us replace it if [[ -f $arkdep_boot/loader/entries/${data[0]}.conf ]]; then printf "\e[1;33m\e[0m\e[1m A bootloader entry for ${data[0]} is already installed, bootloader entry will be regenerated\e[0m\n" rm $arkdep_boot/loader/entries/${data[0]}.conf fi # Load systemd-boot template in to list mapfile systemd_boot_template < $arkdep_dir/templates/systemd-boot || cleanup_and_quit 'Failed to read systemd-boot template file' # Write bootloader entry for line in "${systemd_boot_template[@]}"; do echo ${line/\%target\%/${data[0]}} >> $arkdep_boot/loader/entries/$(date +%Y%m%d-%H%M%S)-${data[0]}+3.conf || cleanup_and_quit 'Failed to write systemd-boot entry' done # Check if there is an update script available if [[ ! -n $ARKDEP_ROOT ]]; then if tar -xf $arkdep_dir/cache/${data[0]}.tar.${data[1]} -C $arkdep_dir/cache/ ./${data[0]}-update.sh 2> /dev/null; 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) printf '\e[1;34m-->\e[0m\e[1m Removing update script\e[0m\n' rm $arkdep_dir/cache/${data[0]}-update.sh fi fi # Image deployment finished, allow for interupts again trap 'echo "User interupt received"; unlock_and_quit 4' INT TERM # Get list of all tarballs in cache declare -r tarball_hits=($(ls $arkdep_dir/cache/ | grep -E '.*.tar..*')) # Remove tarball if configured to remove we have hits if [[ $remove_tar_after_deployment -eq 1 ]] && [[ ${#tarball_hits[@]} -gt 0 ]]; then printf '\e[1;34m-->\e[0m\e[1m Removing tarball from cache\e[0m\n' for tarball in ${tarball_hits[@]}; do printf "Removing $tarball\n" rm $arkdep_dir/cache/$tarball done fi # Remove entries outside of keep, ignore first hit to allow N in config file # instead of requiring N+1 to be configured declare remove_deployments=($(tail -n +$deploy_keep $arkdep_dir/tracker)) remove_deployments=(${remove_deployments[@]:1}) # 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_scriptmode=1 remove_deployment ${remove_deployments[@]} fi unlock_and_quit 0 } # Install a package and add it to the layer tracker layer () { # If no installation candidates provided if [[ $# -eq 1 ]]; then cleanup_and_quit 'No layer candidates provided' fi # By default, as it is discouraged, the layer tracker file is only created upon calling this function # Create layer tracker file if it does not yet exist if [[ ! -f $arkdep_dir/layer ]]; then printf "\e[1;33m\e[0m\e[1m Creating new layer tracker file\e[0m\n" touch $arkdep_dir/layer fi # Index layer tracker list declare -r layered=($(cat $arkdep_dir/layer)) # Program list with duplicates removed declare progs_clean=() # Check if prog is already in list, if not add to progs_clean if [[ ${#layered[@]} -gt 0 ]]; then for prog in ${@:2}; do for layer in ${layered[@]}; do # If program is already present in layer tracker it can be ignored if [[ $layer == $prog ]]; then printf "\e[1;33m\e[0m\e[1m $prog is already in layer tracker file and will be ignored\e[0m\n" declare layer_matched=1 break fi done # Add program to layer list unless duplicate if [[ $layer_matched -ne 1 ]]; then progs_clean+=($prog) fi unset layer_matched done else progs_clean=(${@:2}) fi # If progs_clean is zero, error and quit if [[ ${#progs_clean[@]} -eq 0 ]]; then printf "\e[1;33m\e[0m\e[1m All provided layer candidates are already layered\e[0m\n" unlock_and_quit 0 fi # Notify user of changes and ask for permission if interactive_mode is enabled printf '\e[1;34m-->\e[0m\e[1m The following packages will be layered\e[0m\n' echo "${progs_clean[@]}" | column if [[ $interactive_mode -eq 1 ]]; then read -p 'Proceed with installation? [Y/n] ' remove_confirm if [[ ! $remove_confirm =~ ^(y|Y|yes|YES|)$ ]]; then unlock_and_quit 1 fi fi # Check if root should be locked again after running if btrfs property get / 2> /dev/null | grep -q 'ro=true'; then declare -r lock_when_done=1 fi # Unlock root if [[ $lock_when_done -eq 1 ]]; then btrfs property set -f -ts / ro false || cleanup_and_quit 'Failed to unlock root partition' fi # Install the packages $package_layer_command ${progs_clean[@]} || cleanup_and_quit 'Failed to install requested packages' # Lock root if it was previously locked if [[ $lock_when_done -eq 1 ]]; then btrfs property set -f -ts / ro true || cleanup_and_quit 'Failed to unlock root partition' fi # If installation was successful add to layer tracker file printf "%s\n" "${progs_clean[@]}" >> $arkdep_dir/layer unlock_and_quit 0 } layer-ls () { cat $arkdep_dir/layer | column unlock_and_quit 0 } unlayer () { # Index layer tracker list declare -r layered=($(cat $arkdep_dir/layer)) # Program list with duplicates removed declare progs_matched=() declare progs_nomatch=() # Check if prog is already in list, if they are add to progs_matched if [[ ${#layered[@]} -gt 0 ]]; then for prog in ${@:2}; do for layer in ${layered[@]}; do # If prog matched add to matched list if [[ $layer == $prog ]]; then progs_matched+=($prog) declare layer_matched=1 break fi done # Add program to nomatch if no match was found if [[ $layer_matched -ne 1 ]]; then progs_nomatch+=($prog) fi unset layer_matched done else printf "\e[1;33m\e[0m\e[1m No packages are layered, thus there are none to remove from the layer\e[0m\n" unlock_and_quit 0 fi # Notify user if provided target progs are not layered if [[ ${#progs_nomatch[@]} -ne 0 ]]; then printf "\e[1;33m\e[0m\e[1m The following packages were provided by are not currently layered\e[0m\n" echo "${progs_nomatch[@]}" | column fi # Confirm if user wishes to remove packages from layer printf '\e[1;34m-->\e[0m\e[1m The following packages will be removed from the layered\e[0m\n' echo "${progs_matched[@]}" | column if [[ $interactive_mode -eq 1 ]]; then read -p 'Proceed with removal? [Y/n] ' remove_confirm if [[ ! $remove_confirm =~ ^(y|Y|yes|YES|)$ ]]; then unlock_and_quit 1 fi fi # Check if root should be locked again after running if btrfs property get / 2> /dev/null | grep -q 'ro=true'; then declare -r lock_when_done=1 fi # Unlock root if [[ $lock_when_done -eq 1 ]]; then btrfs property set -f -ts / ro false || cleanup_and_quit 'Failed to unlock root partition' fi $package_unlayer_command ${progs_matched[@]} || cleanup_and_quit 'failed to remove packages from layer' # Lock root if it was previously locked if [[ $lock_when_done -eq 1 ]]; then btrfs property set -f -ts / ro true || cleanup_and_quit 'Failed to unlock root partition' fi # Remove package from layer tracker declare -r grep_string=$(printf %s\| ${progs_matched[@]}) declare -r new_layer_tracker=$(grep -Ev "${grep_string::-1}" $arkdep_dir/layer) printf "%s\n" "$new_layer_tracker" > $arkdep_dir/layer unlock_and_quit 0 } [[ $1 == 'init' ]] && init_new_system $2 [[ $1 == 'teardown' ]] && teardown [[ $1 == 'get-available' ]] && get_available [[ $1 == 'diff' ]] && diff $2 $3 [[ $1 == 'deploy' ]] && deploy $2 $3 [[ $1 == 'remove' ]] && remove_deployment $@ [[ $1 == 'healthcheck' ]] && healthcheck $1 [[ $1 == 'cleanup' ]] && cleanup [[ $1 == 'layer' ]] && layer $@ [[ $1 == 'layer-ls' ]] && layer-ls [[ $1 == 'unlayer' ]] && unlayer $@ # No valid params were provided printf '\e[1;31m<#>\e[0m\e[1m No valid parameters provided\e[0m\n' unlock_and_quit 3