arkdep/arkdep
2024-06-25 15:43:41 +02:00

899 lines
33 KiB
Bash
Executable file

#!/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=0 && 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=0
# 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
}
# List all available packages defined in the repo's list file
get_available () {
printf "\e[1;34m-->\e[0m\e[1m Downloading list file from $repo_url\e[0m\n"
curl -sf "${repo_url}/list" || printf '\e[1;33m<!>\e[0m\e[1m Failed to download list file\e[0m\n'
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