#!/usr/bin/bash # global variables FUNC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export FUNC_DIR TEST_ROOT_DIR="$(pwd)" export TEST_ROOT_DIR TEST_FILENAME=$(basename "$BATS_TEST_FILENAME") export TEST_FILENAME export TEST_NAME=${TEST_FILENAME%.bats} export THEME_DIRNAME="$BATS_TEST_DIRNAME" export TEST_NAME_SHORT="$TEST_NAME" export SWUPD_DIR="$FUNC_DIR/../.." # 3rd-party variables export THIRD_PARTY_DIR="opt/3rd-party" export THIRD_PARTY_BUNDLES_DIR="$THIRD_PARTY_DIR/bundles" export THIRD_PARTY_BIN_DIR="$THIRD_PARTY_DIR/bin" export THIRD_PARTY_SCRIPT_TEMPLATE="script.template" # formatting variables export SPACE=" " export TAB=" " # detect where the swupd binary is if [ -e "$SWUPD" ]; then # nothing to be done, variable already set up export SWUPD elif [ -e "$SWUPD_DIR"/swupd ]; then # using the path relative to the test dir export SWUPD="$SWUPD_DIR"/swupd elif [ -e "$(pwd)"/swupd ]; then # using the current path SWUPD="$(pwd)"/swupd export SWUPD fi # Exit codes export SWUPD_OK=0 export SWUPD_NO=1 export SWUPD_REQUIRED_BUNDLE_ERROR=2 # a required bundle is missing or was attempted to be removed export SWUPD_INVALID_BUNDLE=3 # the specified bundle is invalid export SWUPD_COULDNT_LOAD_MOM=4 # MoM cannot be loaded into memory (this could imply network issue) export SWUPD_COULDNT_REMOVE_FILE=5 # couldn't delete a file which must be deleted export SWUPD_COULDNT_RENAME_DIR=6 # couldn't rename a directory export SWUPD_COULDNT_CREATE_FILE=7 # couldn't create a file export SWUPD_RECURSE_MANIFEST=8 # error while recursing a manifest export SWUPD_LOCK_FILE_FAILED=9 # cannot get the lock export SWUPD_COULDNT_RENAME_FILE=10 # couldn't rename a file export SWUPD_INIT_GLOBALS_FAILED=12 # cannot initialize globals export SWUPD_BUNDLE_NOT_TRACKED=13 # bundle is not tracked on the system export SWUPD_COULDNT_LOAD_MANIFEST=14 # cannot load manifest into memory export SWUPD_INVALID_OPTION=15 # invalid command option export SWUPD_SERVER_CONNECTION_ERROR=16 # no net connection to swupd server export SWUPD_COULDNT_DOWNLOAD_FILE=17 # file download problem export SWUPD_COULDNT_UNTAR_FILE=18 # couldn't untar a file export SWUPD_COULDNT_CREATE_DIR=19 # Cannot create required dir export SWUPD_CURRENT_VERSION_UNKNOWN=20 # Cannot determine current OS version export SWUPD_SIGNATURE_VERIFICATION_FAILED=21 # Cannot initialize signature verification export SWUPD_BAD_TIME=22 # System time is bad export SWUPD_COULDNT_DOWNLOAD_PACK=23 # Pack download failed export SWUPD_BAD_CERT=24 # unable to verify server SSL certificate export SWUPD_DISK_SPACE_ERROR=25 # not enough disk space left (or it cannot be determined) export SWUPD_PATH_NOT_IN_MANIFEST=26 # the required path is not in any manifest export SWUPD_UNEXPECTED_CONDITION=27 # an unexpected condition was found export SWUPD_SUBPROCESS_ERROR=28 # failure to execute another program in a subprocess export SWUPD_COULDNT_LIST_DIR=29 # couldn't list the content of a directory export SWUPD_COMPUTE_HASH_ERROR=30 # there was an error computing the hash of the specified file export SWUPD_TIME_UNKNOWN=31 # couldn't get current system time export SWUPD_COULDNT_WRITE_FILE=32 # couldn't write to a file export SWUPD_MIX_COLLISIONS=33 # collisions were found between mix and upstream export SWUPD_OUT_OF_MEMORY_ERROR=34 # swupd ran out of memory export SWUPD_VERIFY_FAILED=35 # verify could not fix/replace/delete one or more files export SWUPD_INVALID_BINARY=36 # binary to be executed is missing or invalid export SWUPD_INVALID_REPOSITORY=37 # the specified 3rd-party repository is invalid export SWUPD_INVALID_FILE=38 # file is missing or invalid # global constant export zero_hash="0000000000000000000000000000000000000000000000000000000000000000" export SCRIPT_TEMPLATE="\ #!/bin/bash\n\n\ export PATH=%s:\$PATH\n\ export LD_LIBRARY_PATH=%s:\$LD_LIBRARY_PATH\n\ export XDG_DATA_DIRS=%s:\$XDG_DATA_DIRS\n\ export XDG_CONFIG_DIRS=%s:\$XDG_CONFIG_DIRS\n\ \n\ %s \"\$@\"\n" generate_random_content() { # swupd_function show_help "$(cat <<-EOM Generates random characters that can be used as content for test files. Usage: generate_random_content EOM )" "$@" local bottom_range=${1:-5} local top_range=${2:-100} local range=$((top_range - bottom_range + 1)) local number_of_lines=$((RANDOM%range + bottom_range)) local total_bytes=$((number_of_lines * 400)) < /dev/urandom head -c "$total_bytes" | tr -dc 'a-zA-Z0-9-_!@#$%^&*()_+{}|:<>?=' | fold -w 100 | head -n $number_of_lines } generate_random_name() { # swupd_function show_help "$(cat <<-EOM Generates a random name for test resources. The name consists of the prefix followed by 8 random alphanumeric characters. Usage: generate_random_name [prefix] Arguments: - prefix: the prefix to be used for the generated name (default: 'test-') EOM )" "$@" local prefix=${1:-test-} local uuid # generate random 8 character alphanumeric string (lowercase only) uuid=$(< /dev/urandom head -c 100 | tr -dc 'a-f0-9' | fold -w 8 | head -n 1) echo "$prefix$uuid" } generate_certificate() { # swupd_function show_help "$(cat <<-EOM Generates public and private keys. Usage: generate_certificate Arguments: - key_path: path to private key - cert_path: path to public key EOM )" "$@" local key_path=$1 local cert_path=$2 local config=$3 validate_param "$key_path" validate_param "$cert_path" debug_msg "Generating test certificate" # generate self-signed public and private key if [ -z "$config" ]; then sudo openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \ -keyout "$key_path" -out "$cert_path" \ -subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=localhost" > /dev/null 2>&1 else validate_item "$config" openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \ -keyout "$key_path" -out "$cert_path" \ -subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=localhost" \ -config "$config" > /dev/null 2>&1 fi debug_msg "Generated private key: $key_path" debug_msg "Generated public key: $cert_path" debug_msg "Keys generated successfully" } create_trusted_cacert() { # swupd_function show_help "$(cat <<-EOM Creates the trusted certificate store and adds one certificate. Usage: create_trusted_cacert Arguments: - certificate_path: path of certificate to add to trust store EOM )" "$@" local cert_path=$1 local counter=0 local subj_hash # only one test can use the trusted certificate store at the same time to # avoid certificate contamination from other tests and race conditions. until mkdir "$CACERT_DIR"; do sleep 1 if [ "$counter" -eq "1000" ]; then echo "Timeout: creating trusted certificate" return 1 fi counter=$((counter + 1)) done # only allow the test that create CACERT_DIR to erase it echo '1' > "$CACERT_DIR/$TEST_NAME.lock" # the public key names in the CACERT_DIR must use the following format to # be included by swupd in the trust store: . subj_hash=$(openssl x509 -subject_hash -noout -in "$cert_path") ln -s "$cert_path" "$CACERT_DIR/$subj_hash.0" } # shellcheck disable=SC2120 destroy_trusted_cacert() { # swupd_function show_help "$(cat <<-EOM Deletes the trusted certificate store. Usage: destroy_trusted_cacert EOM )" "$@" # only the test that created CACERT_DIR can erase it which stops # tests from erasing CACERT_DIR when they fail before creating it if [ -f "$CACERT_DIR/$TEST_NAME.lock" ]; then rm -rf "$CACERT_DIR" fi } print() { # swupd_function show_help "$(cat <<-EOM Prints a message from a test, the message will be displayed only when running the test using the bats -t flag. Usage: print Arguments: - msg: the message to be printed EOM )" "$@" local msg=$1 # if file descriptor 3 is not available (for example when sourcing the library # from the command line) use std output instead if { true >&3; } 2> /dev/null; then echo -e "$msg" >&3 else echo -e "$msg" fi } # shellcheck disable=SC2120 show_target() { # swupd_function show_help "$(cat <<-EOM Shows the file system of the target directory in a tree view. Usage: show_target EOM )" "$@" print "\n$(tree "$TARGETDIR")\n" } debug_msg() { #swupd_function show_help "$(cat <<-EOM Messages printed with debug_msg will only be shown when the user sets the environment variable DEBUG_TEST=true. Usage: debug_msg Arguments: - msg: the message to be printed EOM )" "$@" local msg=$1 if [ "$DEBUG_TEST" = true ]; then if [[ $msg == '\n'* ]]; then print "" fi print "(${FUNCNAME[1]}:${BASH_LINENO[0]}) DEBUG: ${msg/'\n'}" if [[ $msg == *'\n' ]]; then print "" fi fi } terminate() { show_help "$(cat <<-EOM Exits if the function is called from a script, or forces an error to stop execution if the function is called from an interactive shell. Usage: terminate Arguments: - msg: the message to be printed EOM )" "$@" # since the library could be sourced and run from an interactive shell # not only from a script, we cannot use exit in interactive shells since it # would terminate the shell, so we are using parameter expansion with a fake # parameter "param" to force an error when running interactive shells local msg=$1 local param case "$-" in *i*) print_stack : "${param:?"$msg, exiting..."}" ;; *) print_stack echo "$msg, exiting..." exit 1;; esac } validate_path() { show_help "$(cat <<-EOM Validates the existance of the path (directory), if it doesn't exist, terminates execution. Usage: validate_path Arguments: - path: the relative or absolute path to the directory EOM )" "$@" local path=$1 if [ -z "$path" ] || [ ! -d "$path" ]; then terminate "Please provide a valid path" fi } validate_item() { show_help "$(cat <<-EOM Validates the existance of the file, symlink or directory, if it doesn't exist, terminates execution. Usage: validate_item Arguments: - path: the relative or absolute path to the file or directory EOM )" "$@" local vfile=$1 if sudo sh -c "[ -z $vfile ] || [ ! -e $vfile ]"; then terminate "Please provide a valid file" fi } validate_param() { show_help "$(cat <<-EOM Validates the presence of the parameter, if it is not present, terminates execution. Usage: validate_param Arguments: - param: parameter to be validated EOM )" "$@" local param=$1 if [ -z "$param" ]; then terminate "Mandatory parameter missing" fi } validate_number() { show_help "$(cat <<-EOM Validates the parameter is a valid number. Usage: validate_number Arguments: - param: parameter to be validated EOM )" "$@" local param=$1 if ! [[ $param =~ ^[0-9]+([.][0-9]+)?$ ]]; then terminate "Bad parameter provided, expecting a number" fi } wait_for_deletion() { show_help "$(cat <<-EOM Waits up to a specific timeout for a file or directory to be deleted. The function returns as soon as the file no longer exists or the timeout is reached. Usage: wait_for_deletion Arguments: - path: relative or absolute path to the file or directory to wait for EOM )" "$@" local param=$1 local timeout=5 # arbitrary timeout local wait_milisecs=0 until [ $((wait_milisecs / 1000)) -eq "$timeout" ] || [ ! -e "$param" ]; do sleep 0.1 wait_milisecs=$((wait_milisecs + 100)) done } get_latest_version() { show_help "$(cat <<-EOM Gets the latest version of the update content in the test environment. Usage: get_latest_version Arguments: - content_dir: path to the content directory (e.g. webdir) EOM )" "$@" local content_dir=$1 # shellcheck disable=SC2010 # SC2010: Don't use ls | grep. Use a glob. # Exception: ls has sorting options that are tricky # to get right with other commands. ls "$content_dir" | grep -E '^[0-9]+$' | sort -rn | head -n1 } get_system_version() { show_help "$(cat <<-EOM Gets the current version of content installed in the test target system. Usage: get_system_version Arguments: - env_name: name of the test environment EOM )" "$@" local env_name=$1 awk -F = '/VERSION_ID/{ print $NF }' "$env_name"/testfs/target-dir/usr/lib/os-release } get_manifest_previous_version() { show_help "$(cat <<-EOM Gets the previous version where a bundle manifest is found. Usage: get_manifest_previous_version [init_version] Arguments: - content_dir: the path to the content directory (e.g. webdir) - bundle_name: name of the bundle of the manifest to be found - init_version: the starting version that will be used to start looking backwards (default: the latest version). EOM )" "$@" local content_dir=$1 local bundle=$2 local init_version=$3 local prev_version if [ -z "$init_version" ]; then init_version=$(get_latest_version "$content_dir") fi if [ ! -e "$content_dir"/"$init_version" ]; then terminate "The initial version provided '$init_version' does not exist." fi # if a manifest for the specified bundle exist in the initial version, # use the manifest to find its previous manifest if [ -e "$content_dir"/"$init_version"/Manifest."$bundle" ]; then awk '/^previous/ { print $2 }' "$content_dir"/"$init_version"/Manifest."$bundle" return fi # look backwards through versions strating from init_version # until we find the previous version of the bundle manifest prev_version="$init_version" while [ "$prev_version" -gt 0 ] && [ ! -e "$content_dir"/"$prev_version"/Manifest."$bundle" ]; do prev_version=$(awk '/^previous/ { print $2 }' "$content_dir"/"$prev_version"/Manifest.MoM) done echo "$prev_version" } copy_manifest() { show_help "$(cat <<-EOM Copies a manifest from one version to another in a test environment. Usage: copy_manifest Arguments: - content_dir: the path to the content directory (e.g. webdir) - bundle_name: name of the bundle of the manifest to be copied - from_version: the version where the manifest will be copied from. - to_version: the version where the manifest will be copied to. EOM )" "$@" local content_dir=$1 local bundle=$2 local from_version=$3 local to_version=$4 local from_path local to_path local manifest local files local bundle_file local file_tar local format from_path="$content_dir"/"$from_version" to_path="$content_dir"/"$to_version" if [ ! -e "$to_path"/Manifest."$bundle" ]; then sudo cp "$from_path"/Manifest."$bundle" "$to_path"/Manifest."$bundle" fi # update the manifest's header format=$(cat "$to_path"/format) update_manifest -p "$to_path"/Manifest."$bundle" version "$to_version" update_manifest -p "$to_path"/Manifest."$bundle" previous "$from_version" update_manifest "$to_path"/Manifest."$bundle" format "$format" # untar the zero pack into the files directory in the to_version sudo tar --preserve-permissions -xf "$to_path"/pack-"$bundle"-from-0.tar --strip-components 1 --directory "$to_path"/files # copy the tar file of each fullfile from a previous version files=("$(ls -I "*.tar" "$to_path"/files)") for bundle_file in ${files[*]}; do if [ ! -e "$to_path"/files/"$bundle_file".tar ]; then # find the existing tar in previous versions and copy # it to the current directory file_tar=$(sudo find "$content_dir" -name "$bundle_file".tar | head -n 1) sudo cp "$file_tar" "$to_path"/files/"$bundle_file".tar fi done } write_to_protected_file() { # swupd_function show_help "$(cat <<-EOM Writes to a file that is owned by root. Usage: write_to_protected_file [-a] Options: -a If used the text will be appended to the file, otherwise it will be overwritten Arguments: - file: the path to the file to be written to - stream: the content to be written EOM )" "$@" local arg [ "$1" = "-a" ] && { arg=-a ; shift ; } local file=${1?Missing output file in write_to_protected_file} shift printf '%b' "$@" | sudo tee $arg "$file" >/dev/null } set_env_variables() { # swupd_function show_help "$(cat <<-EOM Exports the environment variables that are dependent on the test environment. Usage: set_env_variables Arguments: - env_name: the name of the test environment EOM )" "$@" local env_name=$1 local path local testfs_path validate_path "$env_name" path=$(dirname "$(realpath "$env_name")") testfs_path="$path"/"$env_name"/testfs debug_msg "Exporting environment variables for $env_name" export TEST_DIRNAME="$path"/"$env_name" debug_msg "TEST_DIRNAME: $TEST_DIRNAME" export WEBDIR="$env_name"/web-dir debug_msg "WEBDIR: $WEBDIR" export TARGETDIR="$env_name"/testfs/target-dir debug_msg "TARGETDIR: $TARGETDIR" export STATEDIR="$env_name"/testfs/state debug_msg "STATEDIR: $STATEDIR" export PATH_PREFIX="$TEST_DIRNAME"/testfs/target-dir debug_msg "PATH_PREFIX: $PATH_PREFIX" # different options for swupd export SWUPD_OPTS="-S $testfs_path/state -p $testfs_path/target-dir -F staging -C $TEST_DIRNAME/Swupd_Root.pem -I --no-progress" export SWUPD_OPTS_PROGRESS="-S $testfs_path/state -p $testfs_path/target-dir -F staging -C $TEST_DIRNAME/Swupd_Root.pem -I" export SWUPD_OPTS_KEEPCACHE="$SWUPD_OPTS --keepcache" export SWUPD_OPTS_NO_CERT="-S $testfs_path/state -p $testfs_path/target-dir -F staging -I --no-progress" export SWUPD_OPTS_NO_FMT="-S $testfs_path/state -p $testfs_path/target-dir -C $TEST_DIRNAME/Swupd_Root.pem -I --no-progress" export SWUPD_OPTS_NO_FMT_NO_CERT="-S $testfs_path/state -p $testfs_path/target-dir -I --no-progress" export SWUPD_OPTS_NO_PATH="-S $testfs_path/state -F staging -C $TEST_DIRNAME/Swupd_Root.pem -I --no-progress" export CLIENT_CERT_DIR="$testfs_path/target-dir/etc/swupd" export CLIENT_CERT="$CLIENT_CERT_DIR/client.pem" export CACERT_DIR="$SWUPD_DIR/swupd_test_certificates" # trusted key store path export PORT_FILE="$path/$env_name/port_file.txt" # stores web server port export SERVER_PID_FILE="$path/$env_name/pid_file.txt" # stores web server pid # Add environment variables for PORT and SERVER_PID when web server used if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE") export PORT fi if [ -f "$SERVER_PID_FILE" ]; then SERVER_PID=$(cat "$SERVER_PID_FILE") export SERVER_PID fi } create_dir() { # swupd_function show_help "$(cat <<-EOM Creates a directory with a hashed name in the specified path, if a directory already exists it returns the name. Usage: create_dir Arguments: - path: the path where the directory will be created EOM )" "$@" local path=$1 local hashed_name local directory # most directories have the same hash, so we only need one directory # in the files directory, if there is already one just return the path/name directory=$(find "$path"/* -type d 2> /dev/null) if [ ! "$directory" ]; then sudo mkdir "$path"/testdir hashed_name=$(sudo "$SWUPD" hashdump --quiet "$path"/testdir) sudo mv "$path"/testdir "$path"/"$hashed_name" # since tar is all we use, create a tar for the new dir create_tar "$path"/"$hashed_name" directory="$path"/"$hashed_name" fi echo "$directory" } create_file() { # swupd_function show_help "$(cat <<-EOM Creates a file with a hashed name in the specified path. Usage: create_file [-x] [-u] [-g] [size] Options: -x If set, the file is created as executable -u If set, the file is created with the SETUID flag -g If set, the file is created with the SETGID flag Arguments: - path: the path where the file will be created - size: the size of the file (in bytes), if nothing is specified the size will be random but fairly small EOM )" "$@" local OPTIND local opt local hashed_name local executable=false local setuid=false local setgid=false while getopts :xug opt; do case "$opt" in x) executable=true ;; u) setuid=true ;; g) setgid=true ;; *) return ;; esac done shift $((OPTIND-1)) local path=$1 local size=$2 validate_path "$path" if [ -n "$size" ]; then < /dev/urandom head -c "$size" | sudo tee "$path/testfile" > /dev/null else generate_random_content | sudo tee "$path/testfile" > /dev/null fi if [ "$executable" = true ]; then sudo chmod +x "$path/testfile" fi if [ "$setuid" = true ]; then sudo chmod u+s "$path/testfile" fi if [ "$setgid" = true ]; then sudo chmod g+s "$path/testfile" fi hashed_name=$(sudo "$SWUPD" hashdump --quiet "$path"/testfile) sudo mv "$path"/testfile "$path"/"$hashed_name" # since tar is all we use, create a tar for the new file create_tar "$path"/"$hashed_name" echo "$path/$hashed_name" } # Parameters: # - PATH: the path where the symbolic link will be created # - FILE: the path to the file to point to create_link() { # swupd_function show_help "$(cat <<-EOM Creates a symbolic link with a hashed name to the specified file in the specified path. If no existing file is specified to point to, a new file will be created and pointed to by the link. If a file is provided but doesn't exist, then a dangling file will be created. Usage: create_link [file_to_point_to] Arguments: - path: the path where the symbolic link will be created - file_to_point_to: the path to the file to point to EOM )" "$@" local path=$1 local pfile=$2 local hashed_name validate_path "$path" # if no file is specified, create one if [ -z "$pfile" ]; then pfile=$(create_file "$path") fi sudo ln -rs "$pfile" "$path"/testlink hashed_name=$(sudo "$SWUPD" hashdump --quiet "$path"/testlink) sudo mv "$path"/testlink "$path"/"$hashed_name" create_tar --skip-validation "$path"/"$hashed_name" echo "$path/$hashed_name" } create_tar() { # swupd_function show_help "$(cat <<-EOM Creates a tar for the specified item in the same location. Usage: create_tar [--skip-validation] Options: --skip-validation If set, the function parameters will not be validated Arguments: - path: the path where the file will be created - size: the relative path to the item (file, directory, link, manifest) EOM )" "$@" local path local item_name local skip_param_validation=false [ "$1" = "--skip-validation" ] && { skip_param_validation=true ; shift ; } local item=$1 if [ "$skip_param_validation" = false ]; then validate_item "$item" fi # do not follow symlinks path=$(dirname "$(realpath --no-symlinks "$item")") item_name=$(basename "$item") # if the item is a directory exclude its content when taring if [ -d "$item" ]; then sudo tar --preserve-permissions -C "$path" -cf "$path"/"$item_name".tar --exclude="$item_name"/* "$item_name" else sudo tar --preserve-permissions -C "$path" -cf "$path"/"$item_name".tar "$item_name" fi } set_as_minversion() { # swupd_function show_help "$(cat <<-EOM Sets an existing version as minversion. Usage: set_as_minversion Arguments: - version_path: the path of the version to be set as minversion (e.g. webdir/10) EOM )" "$@" local minversion_path=$1 local webdir_path local minversion local previous_version local bundles local bundle local mom local manifest local files local bundle_file validate_path "$minversion_path" minversion="$(basename "$minversion_path")" webdir_path="$(dirname "$minversion_path")" # copy all manifests in MoM to the minversion mom="$minversion_path"/Manifest.MoM mapfile -t bundles < <(awk '/^M\.\.\./ { print $4 }' "$mom") IFS=$'\n' for bundle in ${bundles[*]}; do # search for the latest manifest version for the bundle manifest="$minversion_path"/Manifest."$bundle" if [ ! -e "$manifest" ]; then previous_version="$minversion" while [ "$previous_version" -gt 0 ] && [ ! -e "$manifest" ]; do previous_version="$(awk '/previous/ { print $2 }' "$mom")" manifest="$webdir_path"/"$previous_version"/Manifest."$bundle" mom="$webdir_path"/"$previous_version"/Manifest.MoM done sudo cp "$manifest" "$webdir_path"/"$minversion"/Manifest."$bundle" manifest="$webdir_path"/"$minversion"/Manifest."$bundle" mom="$webdir_path"/"$minversion"/Manifest.MoM # copy the zero pack also from the old version to the new one sudo cp "$webdir_path"/"$previous_version"/pack-"$bundle"-from-0.tar "$webdir_path"/"$minversion"/pack-"$bundle"-from-0.tar # extract the content of the zero pack in the files directory so we have # all files available to create future delta packs sudo tar --preserve-permissions -xf "$webdir_path"/"$minversion"/pack-"$bundle"-from-0.tar --strip-components 1 --directory "$webdir_path"/"$minversion"/files files=("$(ls -I "*.tar" "$webdir_path"/"$minversion"/files)") for bundle_file in ${files[*]}; do if [ ! -e "$webdir_path"/"$minversion"/files/"$bundle_file".tar ]; then create_tar "$webdir_path"/"$minversion"/files/"$bundle_file" fi done update_manifest -p "$manifest" version "$minversion" update_manifest -p "$manifest" previous "$previous_version" fi # update manifest content with latest version sudo sed -i "/....\\t.*\\t.*\\t.*$/s/\\(....\\t.*\\t\\).*\\(\\t\\)/\\1$minversion\\2/g" "$manifest" sudo rm -rf "$manifest".tar create_tar "$manifest" done unset IFS # update the MoM update_manifest "$mom" minversion "$minversion" update_hashes_in_mom "$mom" } create_manifest() { # swupd_function show_help "$(cat <<-EOM Creates an empty manifest in the specified path. Usage: create_manifest [format] [previous_version] Arguments: - path: the path where the manifest will be created (e.g. webdir/10) - bundle_name: the name of the bundle which this manifest belongs to - format: the format of the manifest (default: 1) - previous_version: the previous version of the manifest (default: 0) EOM )" "$@" local path=$1 local name=$2 local format=${3:-1} local previous_version=${4:-0} local minversion="0" local version validate_path "$path" validate_param "$name" version=$(basename "$path") { printf 'MANIFEST\t%s\n' "$format" printf 'version:\t%s\n' "$version" if [ "$name" = "MoM" ]; then printf 'minversion:\t%s\n' "$minversion" fi printf 'previous:\t%s\n' "$previous_version" printf 'filecount:\t0\n' printf 'timestamp:\t%s\n' "$(date +"%s")" printf 'contentsize:\t0\n' printf '\n' } | sudo tee "$path"/Manifest."$name" > /dev/null echo "$path/Manifest.$name" } retar_manifest() { # swupd_function show_help "$(cat <<-EOM Regenerates a manifest's tar, updates the hashes in the MoM and signs it. Usage: retar_manifest Arguments: - manifest: the path to the manifest file to have its tar regenerated EOM )" "$@" local manifest=$1 validate_item "$manifest" sudo rm -f "$manifest".tar create_tar "$manifest" # if the modified manifest is the MoM, sign it again if [ "$(basename "$manifest")" = Manifest.MoM ]; then sudo rm -f "$manifest".sig sign_manifest "$manifest" else # update hashes in MoM, re-creates tar and re-signs MoM update_hashes_in_mom "$(dirname "$manifest")"/Manifest.MoM fi } add_to_manifest() { # swupd_function show_help "$(cat <<-EOM Adds the specified item to an existing bundle manifest. Usage: add_to_manifest [-s] [-p] [-e] Options: -s If set (skip), the validation of parameters will be skipped -p If set (partial), the item will be added to the manifest, but the manifest's tar won't be re-created. If the manifest being updated is the MoM, it won't be re-signed either. This is useful if more updates are to be done in the manifest to avoid extra processing -e If set (experimental), the bundle will be added to the manifest as experimental. This should only be used when is a MoM Arguments: - manifest: the relative path to the manifest file (e.g. webdir/10/Manifest.test) - item: the relative path to the item (file, directory, symlink) to be added - item_path_in_fs: the relative path of the item in the target system when installed EOM )" "$@" local OPTIND local opt local item_type local item_size local name local version local filecount local contentsize local file_type="." local exported_type="." local skip_param_validation=false local partial=false local experimental="." # If no parameters are received show usage if [ $# -eq 0 ]; then add_to_manifest_usage return fi while getopts :spe opt; do case "$opt" in s) skip_param_validation=true ;; p) partial=true ;; e) experimental="e" ;; *) add_to_manifest_usage return ;; esac done shift $((OPTIND-1)) local manifest=$1 local item=$2 local item_path=$3 if [ "$skip_param_validation" = false ]; then validate_item "$manifest" validate_item "$item" validate_param "$item_path" fi item_size=$(stat -c "%s" "$item") name=$(basename "$item") version=$(basename "$(dirname "$manifest")") # add to filecount filecount=$(awk '/^filecount/ { print $2}' "$manifest") filecount=$((filecount + 1)) sudo sed -i "s/^filecount:.*/filecount:\\t$filecount/" "$manifest" # add to contentsize contentsize=$(awk '/^contentsize/ { print $2}' "$manifest") contentsize=$((contentsize + item_size)) # get the item type if [ "$(basename "$manifest")" = Manifest.MoM ]; then item_type=M # MoM has a contentsize of 0, so don't increase this for MoM contentsize=0 # files, directories and links are stored already hashed, but since # manifests are not stored hashed, we need to calculate the hash # of the manifest before adding it to the MoM name=$(sudo "$SWUPD" hashdump --quiet "$item") elif [ -L "$item" ]; then item_type=L # when adding a link to a bundle we may want to also add the file the # link is pointin too to the manifest too, if this is the case this has # to be done explicitely elif [ -f "$item" ]; then item_type=F elif [ -d "$item" ]; then item_type=D fi # apply heuristics to the file and select the appropriate flag case "$item_path" in /data/* | /dev/* | /home/* | /proc/* | /root/* | /run/* | /sys/* | /tmp/* | /usr/src* | /var/* | /lost+found/* ) file_type="s";; /boot/* | /usr/lib/modules/* | /usr/lib/kernel/* | /usr/bin/bootctl* | /usr/lib/systemd/boot* ) file_type="b";; /etc/* ) file_type="C";; esac # if the file is in any of these paths: /bin, /usr/bin. /usr/local/bin, then it is an exported file if [ "$(dirname "$item_path")" = "/bin" ] || [ "$(dirname "$item_path")" = "/usr/bin" ] || [ "$(dirname "$item_path")" = "/usr/local/bin" ]; then exported_type="x" fi sudo sed -i "s/^contentsize:.*/contentsize:\\t$contentsize/" "$manifest" # add to manifest content write_to_protected_file -a "$manifest" "$item_type$experimental$file_type$exported_type\\t$name\\t$version\\t$item_path\\n" # If a manifest tar already exists for that manifest, renew the manifest tar unless specified otherwise if [ "$partial" = false ]; then retar_manifest "$manifest" fi } add_dependency_to_manifest() { # swupd_function show_help "$(cat <<-EOM Adds the specified bundle dependency to an existing bundle manifest. Usage: add_dependency_to_manifest [-p] [-o] Options: -p If set (partial), the dependency will be added to the manifest, but the manifest's tar won't be re-created, nor the hash in the MoM will be updated either. This is useful if more updates are to be done in the manifest to avoid extra processing -o If set (optional), the dependency will be added to the manifest as an optional dependency (also-add flag). Arguments: - manifest: the relative path to the manifest file (e.g. webdir/10/Manifets.test) - dependency: the name of the bundle to be included as a dependency Note: if both options -p and -o are to be used, they must be specified in that order or one option will be ignored. EOM )" "$@" local partial=false local flag=includes [ "$1" = "-p" ] && { partial=true ; shift ; } [ "$1" = "-o" ] && { flag="also-add"; shift ; } local manifest=$1 local dependency=$2 local path local manifest_name local version local pre_version validate_item "$manifest" validate_param "$dependency" path=$(dirname "$(dirname "$manifest")") version=$(basename "$(dirname "$manifest")") manifest_name=$(basename "$manifest") # if the provided manifest does not exist in the current version, it means # we need to copy it from a previous version. # this could happen for example if a manifest is created in one version (e.g. 10), # but the dependency should be added in a different, future version (e.g. 20) if [ ! -e "$path"/"$version"/"$manifest_name" ]; then pre_version="$version" while [ "$pre_version" -gt 0 ] && [ ! -e "$path"/"$pre_version"/"$manifest_name" ]; do pre_version=$(awk '/^previous/ { print $2 }' "$path"/"$pre_version"/Manifest.MoM) done sudo cp "$path"/"$pre_version"/"$manifest_name" "$path"/"$version"/"$manifest_name" update_manifest -p "$manifest" version "$version" update_manifest -p "$manifest" previous "$pre_version" fi update_manifest -p "$manifest" timestamp "$(date +"%s")" sudo sed -i "/^contentsize:.*/a $flag:\\t$dependency" "$manifest" # If a manifest tar already exists for that manifest, renew the manifest tar # unless specified otherwise if [ "$partial" = false ]; then retar_manifest "$manifest" fi } remove_from_manifest() { # swupd_function show_help "$(cat <<-EOM Removes the specified item from an existing bundle manifest. Usage: remove_from_manifest Options: -p If set (partial), the item will be removed from the manifest, but the manifest's tar won't be re-created, nor the hash in the MoM will be updated either. This is useful if more updates are to be done in the manifest to avoid extra processing Arguments: - manifest: the relative path to the manifest file (e.g. webdir/10 Manifest.test) - item: either the hash, or filename of the item to be removed EOM )" "$@" local partial=false [ "$1" = "-p" ] && { partial=true ; shift ; } local manifest=$1 local item=$2 local filecount local contentsize local item_size local item_hash validate_item "$manifest" validate_param "$item" # replace every / with \/ in item (if any) item="${item////\\/}" # decrease filecount and contentsize filecount=$(awk '/^filecount/ { print $2}' "$manifest") filecount=$((filecount - 1)) update_manifest -p "$manifest" filecount "$filecount" if [ "$(basename "$manifest")" != Manifest.MoM ]; then contentsize=$(awk '/^contentsize/ { print $2}' "$manifest") item_hash=$(get_hash_from_manifest "$manifest" "$item") item_size=$(stat -c "%s" "$(dirname "$manifest")"/files/"$item_hash") contentsize=$((contentsize - item_size)) update_manifest -p "$manifest" contentsize "$contentsize" fi # remove the lines that match from the manifest sudo sed -i "/\\t$item$/d" "$manifest" sudo sed -i "/\\t$item\\t/d" "$manifest" # If a manifest tar already exists for that manifest, renew the manifest tar # unless specified otherwise if [ "$partial" = false ]; then retar_manifest "$manifest" fi } update_manifest() { # swupd_function show_help "$(cat <<-EOM Updates fields in an existing manifest. Usage: update_manifest [-p] update_manifest [-p] Options: -p if the p flag is set (partial), the function skips updating the MoM's hashes, creating a tar for the MoM and signing it. It also skips creating the tar for the modified manifest, this is useful if more updates are to be done in the manifest to avoid extra processing Arguments: - manifest: the relative path to the manifest file (e.g. webdir/10/Manifest.test) - key: the element to be updated (e.g. format, minversion, file-status, etc.) - hash_or_filename: the file name or hash of the record to be updated (if applicable) - new_value: the value to be used for updating the record EOM )" "$@" local partial=false [ "$1" = "-p" ] && { partial=true ; shift ; } local manifest=$1 local key=$2 local var=$3 local value=$4 local manifest_version validate_item "$manifest" validate_param "$key" validate_param "$var" var="${var////\\/}" value="${value////\\/}" case "$key" in format) sudo sed -i "s/^MANIFEST.*/MANIFEST\\t$var/" "$manifest" ;; version | previous | filecount | timestamp | contentsize) sudo sed -i "s/^$key:.*/$key:\\t$var/" "$manifest" ;; minversion) # Update minversion field which should only exist in MoM sudo sed -i "s/^$key:.*/$key:\\t$var/" "$manifest" # Replace manifest version when older than minversion manifest_version=$(awk '/^version:/ {print $2}' "$manifest") if [ "$manifest_version" -lt "$var" ]; then sudo sed -i "/^version:\\t/ s/[0-9]\\+/$var/" "$manifest" fi # Replace file versions with minversion when less than minversion sudo gawk -i inplace -v var="$var" '{if($3 != "" && $3 Arguments: - manifest: the relative path to the manifest file (e.g. webdir/10/Manifest.test) - hash_or_filename: the hash or the filename of the item to be retrieved EOM )" "$@" local manifest=$1 local entry_key=$2 local value validate_item "$manifest" # if no key is provided, return a list with all # files/entries from the manifest if [ -z "$entry_key" ]; then sudo cat "$manifest" | grep -E -x "(D|F|L)..."$'\t'".*"$'\t'".*"$'\t'"/.*" return fi # a key was provided, try with the key as a file # path first which should be unique in a Manifest value=$(sudo cat "$manifest" | grep "$entry_key"$) # if an entry was not found using the file path, try with the hash next if [ -z "$value" ]; then value=$(sudo cat "$manifest" | grep ^"...."$'\t'"$entry_key") fi echo "$value" } update_hashes_in_mom() { # swupd_function show_help "$(cat <<-EOM Recalculate the hashes of the elements in the specified MoM and updates with the new values. Usage: update_hashes_in_mom Arguments: - manifest: the relative path to the MoM manifest file (e.g. webdir/10/Manifest.MoM) EOM )" "$@" local manifest=$1 local path local bundles local bundle local bundle_old_hash local bundle_new_hash validate_item "$manifest" path=$(dirname "$manifest") IFS=$'\n' if [ "$(basename "$manifest")" = Manifest.MoM ]; then bundles=("$(sudo cat "$manifest" | grep -x "M.\\.\\..*" | awk '{ print $4 }')") for bundle in ${bundles[*]}; do # if the hash of the manifest changed, update it bundle_old_hash=$(get_hash_from_manifest "$manifest" "$bundle") bundle_new_hash=$(sudo "$SWUPD" hashdump --quiet "$path"/Manifest."$bundle") if [ "$bundle_old_hash" != "$bundle_new_hash" ] && [ "$bundle_new_hash" != "$zero_hash" ]; then # replace old hash with new hash sudo sed -i "/\\t$bundle_old_hash\\t/s/\\(....\\t\\).*\\(\\t.*\\t\\)/\\1$bundle_new_hash\\2/g" "$manifest" # replace old version with new version sudo sed -i "/\\t$bundle_new_hash\\t/s/\\(....\\t.*\\t\\).*\\(\\t\\)/\\1$(basename "$path")\\2/g" "$manifest" fi done # re-order items on the manifest so they are in the correct order based on version sudo sort -t$'\t' -k3 -s -h -o "$manifest" "$manifest" # since the MoM has changed, sign it again and update its tar retar_manifest "$manifest" else echo "The provided manifest is not the MoM" return 1 fi unset IFS } bump_format() { # swupd_function show_help "$(cat <<-EOM Updates all manifests after a format bump. Usage: bump_format Arguments: - env_name: the name of the test environment Note: bump_format can only be used in an environment that has the os-core bundle and release files (create_test_environment -r MY_ENV) EOM )" "$@" local env_name=$1 local format local version local previous_version local new_version local middle_version local new_version_path local middle_version_path local mom local manifest local bundles validate_path "$env_name" # find the latest version and MoM version=$(get_latest_version "$env_name"/web-dir) middle_version="$((version+10))" middle_version_path="$env_name"/web-dir/"$middle_version" new_version="$((version+20))" new_version_path="$env_name"/web-dir/"$new_version" format="$(< "$env_name"/web-dir/"$version"/format)" # if the environment doesn't have os-core or was not created with release files exit manifest="$env_name"/web-dir/"$version"/Manifest.os-core if [ ! -e "$manifest" ] || ! grep -qx "F\\.\\.\\..*/usr/lib/os-release$" "$manifest"; then terminate "bump_format can only be used in an environment that has the os-core bundle and release files" fi # create a +10 and a +20 versions for the format bump create_version "$env_name" "$middle_version" "$version" "$format" create_version -r "$env_name" "$new_version" "$middle_version" "$((format+1))" # Update the +10 version mom="$middle_version_path"/Manifest.MoM # +10 and +20 versions should have matching content sudo rm -rf "$middle_version_path" sudo cp -r "$new_version_path" "$middle_version_path" # Update +10 format and versions sudo sh -c "echo $format > $middle_version_path/format" update_manifest -p "$mom" format "$format" update_manifest -p "$mom" version "$middle_version" update_manifest -p "$mom" previous "$version" # The +20 version updates the os-release and format files. Change # the versions of these files in the +10 to the +10's version. update_manifest -p "$mom" file-version os-core "$middle_version" update_manifest -p "$middle_version_path"/Manifest.os-core file-version /usr/lib/os-release "$middle_version" update_manifest -p "$middle_version_path"/Manifest.os-core file-version /usr/share/defaults/swupd/format "$middle_version" mapfile -t bundles < <(awk '/^M\.\.\./ { print $4 }' "$mom") IFS=$'\n' for bundle in ${bundles[*]}; do manifest="$middle_version_path"/Manifest."$bundle" update_manifest -p "$manifest" format "$format" update_manifest -p "$manifest" version "$middle_version" update_manifest "$manifest" previous "$version" done unset IFS update_hashes_in_mom "$mom" # update the minversion mom="$new_version_path"/Manifest.MoM update_manifest -p "$new_version_path"/Manifest.os-core previous "$middle_version" set_as_minversion "$new_version_path" } sign_manifest() { # swupd_function show_help "$(cat <<-EOM Signs a manifest with a PEM key and generates the signed manifest in the same location. Usage: sign_manifest Arguments: - manifest: the relative path to the manifest to be signed (e.g. webdir/10/Manifest.test) EOM )" "$@" local manifest=$1 validate_item "$manifest" sudo openssl smime -sign -binary -in "$manifest" \ -signer "$TEST_DIRNAME"/Swupd_Root.pem \ -inkey "$TEST_DIRNAME"/private.pem \ -outform DER -out "$(dirname "$manifest")"/Manifest.MoM.sig } sign_version() { # swupd_function show_help "$(cat <<-EOM Signs the version file with a PEM key and generates the signature in the same location. Usage: sign_version Arguments: - version_file: the relative path to the version file to be signed EOM )" "$@" local version_file=$1 validate_item "$version_file" sudo openssl smime -sign -binary -in "$version_file" \ -signer "$TEST_DIRNAME"/Swupd_Root.pem \ -inkey "$TEST_DIRNAME"/private.pem \ -outform DER -out "$version_file".sig } get_hash_from_manifest() { # swupd_function show_help "$(cat <<-EOM Retrieves the hash value of a file or directory in a manifest. Usage: get_hash_from_manifest Arguments: - manifest: the relative path to the manifest file to be looked at (e.g. webdir/10/Manifest.test) - name_in_fs: the filename to look for in the manifest EOM )" "$@" local manifest=$1 local item=$2 validate_item "$manifest" validate_param "$item" hash=$(sudo cat "$manifest" | grep $'\t'"$item"$ | awk '{ print $2 }') echo "$hash" } set_current_version() { # swupd_function show_help "$(cat <<-EOM Sets the current version of the target system to the desired version. Usage: set_current_version [3rd_party_repo] Arguments: - environment_name: the name of the test environmnt to act upon - new_version: the version for the target to be set to (e.g. 20) - 3rd_party_repo: the name of the 3rd-party repository (if applicable) EOM )" "$@" local env_name=$1 local new_version=$2 local repo_name=$3 local os_release validate_path "$env_name" if [ -n "$repo_name" ]; then os_release="$env_name"/testfs/target-dir/"$THIRD_PARTY_BUNDLES_DIR"/"$repo_name"/usr/lib/os-release else os_release="$env_name"/testfs/target-dir/usr/lib/os-release fi sudo sed -i "s/VERSION_ID=.*/VERSION_ID=$new_version/" "$os_release" } set_latest_version() { # swupd_function show_help "$(cat <<-EOM Sets the latest version of the update content to the desired version. Usage: set_latest_version Arguments: - env_name: the name of the test environmnt to act upon - new_version: the version for the target to be set to (e.g. 20) EOM )" "$@" local env_name=$1 local new_version=$2 validate_path "$env_name" write_to_protected_file "$env_name"/web-dir/version/formatstaging/latest "$new_version" sign_version "$env_name"/web-dir/version/formatstaging/latest } create_third_party_repo() { #swupd_function show_help "$(cat <<-EOM Creates a new version of the update content in the specified location to serve as a 3rd-party repository. Usage: create_third_party_repo [-a] [format] [repo_name] Options: -a if the a flag is set (add), the 3rd-party repository is also added to the repo.ini file as if a user already added it to swupd Arguments: - env_name: the name of the test environment - new_version: the version of the server side content (e.g. 20) - format: the format to use for the new version (e.g. 1, default: staging) - repo_name: the name of the 3rd-party repo (default: test-repo) EOM )" "$@" local add=false [ "$1" = "-a" ] && { add=true ; shift ; } local env_name=$1 local version=$2 local format=${3:-staging} local repo_name=${4:-test-repo} local path local hashed_name local cert validate_item "$env_name" validate_param "$version" path=$(dirname "$(realpath "$env_name")") debug_msg "Creating 3rd-party repo $repo_name..." # create the state dir for the 3rd-party repo TPSTATEDIR="$STATEDIR"/3rd-party/"$repo_name" debug_msg "Creating a state directory for repo $repo_name at $TPSTATEDIR..." sudo mkdir -p "$TPSTATEDIR"/{staged,download,delta,telemetry,bundles} sudo chmod -R 0700 "$STATEDIR" # create the basic content for the 3rd-party repo create_version -r "$env_name" "$version" 0 "$format" "$repo_name" debug_msg "3rd-party repo $repo_name created successfully" export TPSTATEDIR export TPWEBDIR="$env_name"/3rd-party/"$repo_name" export TPTARGETDIR="$env_name"/testfs/target-dir/"$THIRD_PARTY_BUNDLES_DIR"/"$repo_name" export TPURL="$path"/"$env_name"/3rd-party/"$repo_name" debug_msg "3rd-party repo state dir: $TPSTATEDIR" debug_msg "3rd-party repo content dir: $TPWEBDIR" debug_msg "3rd-party repo target dir: $TPTARGETDIR" debug_msg "3rd-party repo URL: $TPURL" # if requested, add the new repo to the repo.ini file # and the template file sudo mkdir -p "$env_name"/testfs/target-dir/"$THIRD_PARTY_DIR" if [ "$add" = true ]; then { printf '[%s]\n\n' "$repo_name" printf 'URL=%s\n\n' "file://$TPURL" printf 'VERSION=%s\n\n' "$version" } | sudo tee -a "$env_name"/testfs/target-dir/"$THIRD_PARTY_DIR"/repo.ini > /dev/null write_to_protected_file "$env_name"/testfs/target-dir/"$THIRD_PARTY_DIR"/"$THIRD_PARTY_SCRIPT_TEMPLATE" "$SCRIPT_TEMPLATE" fi # every 3rd-party repo needs to have at least the special os-core bundle, # this should be added by default and should include: # - os-release # - Swupd_Root.pem # - format debug_msg "Adding bundle os-core to the 3rd-party repo..." hashed_name=$(sudo "$SWUPD" hashdump --quiet "$TEST_DIRNAME"/Swupd_Root.pem) sudo cp -p "$TEST_DIRNAME"/Swupd_Root.pem "$env_name"/3rd-party/"$repo_name"/"$version"/files/"$hashed_name" create_tar "$env_name"/3rd-party/"$repo_name"/"$version"/files/"$hashed_name" cert="$env_name"/3rd-party/"$repo_name"/"$version"/files/"$hashed_name" if [ "$add" = true ]; then create_bundle -L -n os-core -v "$version" -f /usr/lib/os-release:"$OS_RELEASE",/usr/share/clear/update-ca/Swupd_Root.pem:"$cert",/usr/share/defaults/swupd/format:"$FORMAT" -u "$repo_name" "$env_name" else create_bundle -n os-core -v "$version" -f /usr/lib/os-release:"$OS_RELEASE",/usr/share/clear/update-ca/Swupd_Root.pem:"$cert",/usr/share/defaults/swupd/format:"$FORMAT" -u "$repo_name" "$env_name" fi if [ "$TEST_ENV_ONLY" = true ]; then print "\nVariables for 3rd-party repo $repo_name:\n" print "TPURL=$TPURL" print "TPWEBDIR=$TPWEBDIR" print "TPTARGETDIR=$TPTARGETDIR" print "TPSTATEDIR=$TPSTATEDIR\n" fi } create_version() { # swupd_function show_help "$(cat <<-EOM Creates a new version of the update content. Usage: create_version [-p] [-r] [from_version] [format] [content_dir] Options: -p if the p flag is set (partial), the function skips creating the MoM's tar and signing it, this is useful if more changes are to be done in the new version in order to avoid extra processing -r if the r flag is set (release), the version is created with hashed os-release and format files that can be used for creating updates Arguments: - env_name: the name of the test environment - new_version: the new version of the server side content (e.g. 20) - from_version: the previous version (default: 0) - format: the format to use for the version (e.g. 1, default: staging) - content_dir: the directory where all the content is to be created (default: web-dir) Note: if both options -p and -r are to be used, they must be specified in that order or one option will be ignored. EOM )" "$@" local partial=false local release_files=false # since this function is called with every test environment, some times multiple # times, use simple parsing of arguments instead of using getopts to keep light weight # with the caveat that arguments need to be provided in a specific order [ "$1" = "-p" ] && { partial=true ; shift ; } [ "$1" = "-r" ] && { release_files=true ; shift ; } local env_name=$1 local version=$2 local from_version=${3:-0} local format=${4:-staging} local repo_name=$5 local mom local hashed_name local content_dir validate_item "$env_name" validate_param "$version" # if a content_dir is specified it means we are using a 3rd-party repo if [ -n "$repo_name" ]; then content_dir=3rd-party/"$repo_name" else content_dir=web-dir fi debug_msg "Creating content for version $version at $content_dir..." # if the requested version already exists do nothing if [ -d "$env_name"/"$content_dir"/"$version" ]; then echo "the requested version $version already exists" return fi sudo mkdir -p "$env_name"/"$content_dir"/"$version"/{files,delta} sudo mkdir -p "$env_name"/"$content_dir"/version/format"$format" write_to_protected_file "$env_name"/"$content_dir"/version/format"$format"/latest "$version" sign_version "$env_name"/"$content_dir"/version/format"$format"/latest write_to_protected_file "$env_name"/"$content_dir"/version/latest_version "$version" sign_version "$env_name"/"$content_dir"/version/latest_version if [ "$format" = staging ]; then format=1 fi write_to_protected_file "$env_name"/"$content_dir"/"$version"/format "$format" # create a new os-release file per version { printf 'NAME="Swupd Test Distro"\n' printf 'VERSION=1\n' printf 'ID=clear-linux-os\n' printf 'VERSION_ID=%s\n' "$version" printf 'PRETTY_NAME="Swupd Test Distro"\n' printf 'ANSI_COLOR="1;35"\n' printf 'HOME_URL="https://clearlinux.org"\n' printf 'SUPPORT_URL="https://clearlinux.org"\n' printf 'BUG_REPORT_URL="https://bugs.clearlinux.org/jira"\n' } | sudo tee "$env_name"/"$content_dir"/"$version"/os-release > /dev/null # copy hashed versions of os-release and format to the files directory if [ "$release_files" = true ]; then debug_msg "Creating release files" hashed_name=$(sudo "$SWUPD" hashdump --quiet "$env_name"/"$content_dir"/"$version"/os-release) sudo cp "$env_name"/"$content_dir"/"$version"/os-release "$env_name"/"$content_dir"/"$version"/files/"$hashed_name" create_tar "$env_name"/"$content_dir"/"$version"/files/"$hashed_name" OS_RELEASE="$env_name"/"$content_dir"/"$version"/files/"$hashed_name" export OS_RELEASE hashed_name=$(sudo "$SWUPD" hashdump --quiet "$env_name"/"$content_dir"/"$version"/format) sudo cp "$env_name"/"$content_dir"/"$version"/format "$env_name"/"$content_dir"/"$version"/files/"$hashed_name" create_tar "$env_name"/"$content_dir"/"$version"/files/"$hashed_name" FORMAT="$env_name"/"$content_dir"/"$version"/files/"$hashed_name" export FORMAT fi # if the previous version is 0 then create a new MoM, otherwise copy the MoM # from the previous version if [ "$from_version" = 0 ]; then debug_msg "This is the first version in the content directory, creating a new MoM..." mom=$(create_manifest "$env_name"/"$content_dir"/"$version" MoM) if [ "$partial" = false ]; then create_tar "$mom" sign_manifest "$mom" fi debug_msg "MoM created successfully" else # copy the packs from the previous version debug_msg "Creating a new MoM for version $version based on the MoM from version $from_version" sudo cp "$env_name"/"$content_dir"/"$from_version"/pack-*.tar "$env_name"/"$content_dir"/"$version" sudo cp "$env_name"/"$content_dir"/"$from_version"/Manifest.MoM "$env_name"/"$content_dir"/"$version" mom="$env_name"/"$content_dir"/"$version"/Manifest.MoM # update MoM info and create the tars update_manifest -p "$mom" format "$format" update_manifest -p "$mom" version "$version" update_manifest -p "$mom" previous "$from_version" update_manifest -p "$mom" timestamp "$(date +"%s")" if [ "$partial" = false ]; then create_tar "$mom" sign_manifest "$mom" fi # when creating a new version we need to make an update to os-release and format # files in the os-core bundle (unless otherwise specified or if bundle does not exist) if [ "$release_files" = true ]; then oldversion="$version" while [ "$oldversion" -gt 0 ] && [ ! -e "$env_name"/"$content_dir"/"$oldversion"/Manifest.os-core ]; do oldversion=$(awk '/^previous/ { print $2 }' "$env_name"/"$content_dir"/"$oldversion"/Manifest.MoM) done # if no os-core manifest was found, nothing else needs to be done if [ -e "$env_name"/"$content_dir"/"$oldversion"/Manifest.os-core ]; then update_bundle -p "$env_name" os-core --header-only "$repo_name" if [ -n "$(get_hash_from_manifest "$env_name"/"$content_dir"/"$oldversion"/Manifest.os-core /usr/lib/os-release)" ]; then remove_from_manifest -p "$env_name"/"$content_dir"/"$version"/Manifest.os-core /usr/lib/os-release fi update_bundle -p "$env_name" os-core --add /usr/lib/os-release:"$OS_RELEASE" "$repo_name" if [ -n "$(get_hash_from_manifest "$env_name"/"$content_dir"/"$oldversion"/Manifest.os-core /usr/share/defaults/swupd/format)" ]; then remove_from_manifest -p "$env_name"/"$content_dir"/"$version"/Manifest.os-core /usr/share/defaults/swupd/format fi # update without -p flag to refresh tar and MoM update_bundle "$env_name" os-core --add /usr/share/defaults/swupd/format:"$FORMAT" "$repo_name" fi fi debug_msg "MoM created successfully" fi } create_test_environment() { # swupd_function show_help "$(cat <<-EOM Creates a test environment with the basic directory structure needed to validate the swupd client. Usage: create_test_environment [-e|-r] [-s ] [initial_version] [format] Options: -e If set, the test environment is created empty, otherwise it will have bundle os-core in the web-dir and installed by default. -r If set, the test environment is created with a more complete version of the os-core bundle, a version that includes the os-release and format files, so it is more useful for some tests, like update tests. -s If used, specifies the maximum size the test environment has in MB, this can be useful when testing scenarios bound to the disk size. Arguments: - env_name: the name of the test environment, this should be typically the test name - initial_version: the version to use for the test environment (default: 10) - format: the format number to use initially in the environment (e.g. 1, default: staging) Note: options -e and -r are mutually exclusive, so you can only use one at a time. EOM )" "$@" local OPTIND local opt local empty=false local release_files=false local size=0 local path local targetdir local statedir while getopts :ers: opt; do case "$opt" in e) empty=true release_files=false ;; r) release_files=true empty=false ;; s) size="$OPTARG" ;; *) return ;; esac done shift $((OPTIND-1)) local env_name=$1 local version=${2:-10} local format=${3:-staging} validate_param "$env_name" validate_number "$size" debug_msg "Creating test environment: $env_name" # clean test environment when test interrupted trap 'destroy_test_environment $TEST_NAME' INT # create all the files and directories needed # web-dir files & dirs mkdir -p "$env_name" touch "$env_name"/.test_env # export environment variables that are dependent of the test env set_env_variables "$env_name" # Generate certificates generate_certificate "$env_name"/private.pem "$env_name"/Swupd_Root.pem "$FUNC_DIR"/certattributes.cnf # Create version if [ "$release_files" = true ]; then create_version -p -r "$env_name" "$version" "0" "$format" else create_version -p "$env_name" "$version" "0" "$format" fi # if a size was selected, create the FS, # if not just use a normal directory if [ "$size" -gt 0 ]; then print "Creating a test file system..." print "Created: $(create_test_fs "$env_name" "$size") ($size MB)" sudo touch "$env_name"/.testfs else debug_msg "No size restrictions were selected for the environment, using a common directory as FS" sudo mkdir -p "$env_name"/testfs fi targetdir="$env_name"/testfs/target-dir statedir="$env_name"/testfs/state # target-dir files & dirs debug_msg "Creating target-dir files and diretories" path=$(dirname "$(realpath "$env_name")") sudo mkdir -p "$targetdir"/usr/lib sudo cp "$env_name"/web-dir/"$version"/os-release "$targetdir"/usr/lib/os-release sudo mkdir -p "$targetdir"/usr/share/clear/bundles sudo mkdir -p "$targetdir"/usr/share/defaults/swupd sudo cp "$env_name"/web-dir/"$version"/format "$targetdir"/usr/share/defaults/swupd/format write_to_protected_file "$targetdir"/usr/share/defaults/swupd/versionurl "file://$path/$env_name/web-dir" write_to_protected_file "$targetdir"/usr/share/defaults/swupd/contenturl "file://$path/$env_name/web-dir" sudo mkdir -p "$targetdir"/etc/swupd # state files & dirs debug_msg "Creating a state dir" sudo mkdir -p "$statedir"/{staged,download,delta,telemetry,bundles,3rd-party} sudo chmod -R 0700 "$statedir" # every environment needs to have at least the os-core bundle so this should be # added by default to every test environment unless specified otherwise if [ "$empty" = false ]; then debug_msg "Adding bundle os-core to the test environment" if [ "$release_files" = true ]; then debug_msg "Including the release files in bundle os-core" create_bundle -L -n os-core -v "$version" -d /usr/share/clear/bundles -f /core,/usr/lib/os-release:"$OS_RELEASE",/usr/share/defaults/swupd/format:"$FORMAT" "$env_name" else create_bundle -L -n os-core -v "$version" -f /core "$env_name" fi else debug_msg "The -e flag was specified, the os-core bundle was not included in the environment" create_tar "$env_name"/web-dir/"$version"/Manifest.MoM sign_manifest "$env_name"/web-dir/"$version"/Manifest.MoM fi debug_msg "Test environment \"$env_name\" created successfully" } destroy_test_environment() { # swupd_function show_help "$(cat <<-EOM Destroys a test environment. Usage: destroy_test_environment [--force] Options: --force If set, the environment will be destroyed even if KEEP_ENV=true Arguments: - env_name: the name of the test environment to be deleted Note: if you want your test environment to be preserved for debugging purposes, set KEEP_ENV=true before running your test. EOM )" "$@" local force=false [ "$1" = "--force" ] && { force=true ; shift ; } local env_name=$1 validate_param "$env_name" debug_msg "Destroying environment $env_name" if [ -z "$KEEP_ENV" ]; then KEEP_ENV=false fi debug_msg "The KEEP_ENV environment variable is set to $KEEP_ENV" if [ "$force" = true ]; then debug_msg "The --force option was used" fi destroy_web_server destroy_trusted_cacert # if the test environment doesn't exist warn the user but don't terminate script # execution, this function could be called from a test teardown and terminating # at that point will cause incorrect test reporting if [ ! -d "$env_name" ]; then echo "Warning: test environment \"$env_name\" doesn't exist, nothing to destroy." return 1 fi if [ "$KEEP_ENV" = true ] && [ "$force" = false ]; then local msg="Warning: KEEP_ENV is set to true, the test environment will be preserved" print "$msg\\n" 2>/dev/null || echo "$msg" return fi # since the action to be performed is very destructive, at least # make sure the directory does look like a test environment if [ ! -e "$env_name"/.test_env ]; then echo "The name provided \"$env_name\" doesn't seem to be a valid test environment" return 1 fi # if a test fs was created, destroy it if [ -e "$env_name"/.testfs ]; then destroy_test_fs "$env_name" fi sudo rm -rf "$env_name" if [ ! -e "$env_name" ]; then debug_msg "Environment $env_name deleted successfully" else debug_msg "The environment $env_name failed to be deleted" fi } add_os_core_update_bundle() { # swupd_function show_help "$(cat <<-EOM Adds a test os-core-update bundle to the test environment (as installed). Usage: add_os_core_update_bundle Arguments: - env_name: the name of the test environment EOM )" "$@" local env_name=$1 validate_path "$env_name" # move the versionurl file to the content directory versionurl_hash=$(sudo "$SWUPD" hashdump --quiet "$TARGETDIR"/usr/share/defaults/swupd/versionurl) sudo cp "$TARGETDIR"/usr/share/defaults/swupd/versionurl "$WEBDIR"/10/files/"$versionurl_hash" versionurl="$WEBDIR"/10/files/"$versionurl_hash" # move the contenturl file to the content directory contenturl_hash=$(sudo "$SWUPD" hashdump --quiet "$TARGETDIR"/usr/share/defaults/swupd/contenturl) sudo cp "$TARGETDIR"/usr/share/defaults/swupd/contenturl "$WEBDIR"/10/files/"$contenturl_hash" contenturl="$WEBDIR"/10/files/"$contenturl_hash" # create the os-core-update bundle create_bundle -L -n os-core-update -f /usr/share/defaults/swupd/versionurl:"$versionurl",/usr/share/defaults/swupd/contenturl:"$contenturl" "$env_name" } create_test_fs() { # swupd_function show_help "$(cat <<-EOM Creates a test file system of a given size. Usage: create_test_fs Arguments: - env_name: the name of the test environment - size: the size in MB of the test filesystem to be created for the environment Note: this filesystem has to be deleted using destroy_test_fs() once no longer required EOM )" "$@" local env_name=$1 local size=$2 local fsfile validate_path "$env_name" validate_param "$size" fsfile=/tmp/"$env_name" # create a file of the appropriate size dd if=/dev/zero of="$fsfile" bs=1M count="$size" >& /dev/null # create loop device and format it sudo losetup -f "$fsfile" mkfs.ext4 "$fsfile" >& /dev/null # mount the fs sudo mkdir "$env_name"/testfs sudo mount "$fsfile" "$env_name"/testfs -o sync # return the fs echo "$env_name"/testfs } destroy_test_fs() { # swupd_function show_help "$(cat <<-EOM Deletes the specified test file system. Usage: destroy_test_fs Arguments: - env_name: the name of the test environment EOM )" "$@" local env_name=$1 local fsfile validate_path "$env_name" fsfile=/tmp/"$env_name" sudo umount "$env_name"/testfs sudo losetup -d "$(losetup -j "$fsfile" -nO NAME)" rm "$fsfile" } create_config_file() { # swupd_function show_help "$(cat <<-EOM Creates an empty configuration file for swupd. Usage: create_config_file EOM )" "$@" if [ ! -d "$TEST_NAME" ]; then terminate "The TEST_NAME needs to be specified to create a config file" fi # these values have to be exported in this function because the # TEST_NAME changes during the setup export SWUPD_CONFIG_DIR="$TEST_NAME"/testconfig export SWUPD_CONFIG_FILE="$SWUPD_CONFIG_DIR"/config debug_msg "Creating config file $SWUPD_CONFIG_FILE..." if [ "$("$SWUPD" --version | grep "config file path" | awk '{print $4}')" != "./testconfig" ]; then skip "Tests that use a config file require swupd to be built with '--with-config-file-path=./testconfig'" fi if [ -e "$SWUPD_CONFIG_FILE" ]; then debug_msg "A config file already existed in that location, deleting it..." destroy_config_file fi sudo mkdir -p "$SWUPD_CONFIG_DIR" sudo touch "$SWUPD_CONFIG_FILE" debug_msg "Config file created successfully" } # shellcheck disable=SC2120 destroy_config_file() { # swupd_function show_help "$(cat <<-EOM Deletes the test swupd configuration file. Usage: destroy_config_file Note: the configuration file to be deleted is the one at SWUPD_CONFIG_DIR EOM )" "$@" if [ ! -d "$SWUPD_CONFIG_DIR" ]; then print "There was no directory $SWUPD_CONFIG_DIR, nothing was deleted" return fi debug_msg "Deleting config file directory $SWUPD_CONFIG_DIR" sudo rm -rf "$SWUPD_CONFIG_DIR" # we need to make sure the config file is deleted before # another test is run so it does not interfere with it since # the config file is common and shared by all commands wait_for_deletion "$SWUPD_CONFIG_DIR" debug_msg "Config file directory deleted successfully" } add_option_to_config_file() { # swupd_function show_help "$(cat <<-EOM Add a key/value in the specified section of the swupd configuration file. Usage: add_option_to_config_file