#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2022 Patrick Spek
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# Change the working directory. In usage, this is the same as using cd,
# however, it will make additional checks to ensure everything is going fine.
chgdir() {
debug "bashtard/chgdir" "Changing workdir to $1"
cd -- "$1" || die "Failed to change directory to $1"
}
# Read a particular value from a key/value configuration file. Using this
# function introduces a dependency on awk.
config() {
config_for "${BASHTARD_PLATFORM[fqdn]}" "$@"
}
config_for() {
local host=$1 ; shift
local key=$1 ; shift
local default=$1 ; shift
local default
local file
local files
files=(
"$BASHTARD_ETCDIR/hosts.d/$host"
"$BASHTARD_ETCDIR/os.d/${BASHTARD_PLATFORM[key]}"
"$BASHTARD_ETCDIR/defaults"
)
if [[ -n "$BASHTARD_PLAYBOOK" ]]
then
debug "bashtard/config_for" "BASHTARD_PLAYBOOK=$BASHTARD_PLAYBOOK, adding etc entries"
files+=(
"$BASHTARD_ETCDIR/playbooks.d/$BASHTARD_PLAYBOOK/etc/os.d"
"$BASHTARD_ETCDIR/playbooks.d/$BASHTARD_PLAYBOOK/etc/defaults"
)
fi
files+=(
"$BASHTARD_SHAREDIR/os.d/${BASHTARD_PLATFORM[key]}"
"$BASHTARD_SHAREDIR/defaults"
)
# Check configuration files
for file in "${files[@]}"
do
debug "bashtard/config_for" "Checking for '$key' in '$file'"
[[ ! -f $file ]] && continue
value="$(awk -F= '$1 == "'"$key"'" { print $NF }' "$file")"
if [[ -n $value ]]
then
debug "bashtard/config_for" "Found $key=$value in $file"
printf "%s" "$value"
return
fi
done
# Return default value
if [[ -n $default ]]
then
printf "%s" "$default"
return
fi
# Error
alert "bashtard/config_for" "No configuration value for $key"
}
# Create a datetime stamp. This is a wrapper around the date utility, ensuring
# that the date being formatted is always in UTC and respect SOURCE_DATE_EPOCH,
# if it is set.
datetime() {
local date_opts
# Apply SOURCE_DATE_EPOCH as the date to base off of.
if [[ $SOURCE_DATE_EPOCH ]]
then
date_opts+=("-d@$SOURCE_DATE_EPOCH")
date_opts+=("-u")
fi
date "${date_opts[@]}" +"${1:-%FT%T}"
}
# Log a message as error, and exit the program. This is intended for serious
# issues that prevent the script from running correctly. The exit code can be
# specified with -i, or will default to 1.
die() {
local OPTIND
local code
while getopts ":i:" opt
do
case "$opt" in
i) code=$OPTARG ;;
*) alert "bashtard/die" "Unused argument specified: $opt" ;;
esac
done
shift $(( OPTIND -1 ))
alert "$@"
exit "${code:-1}"
}
# Fetch a file from an URL. Using this function introduces a dependency on curl.
fetch_http() {
local OPTIND
local buffer
while getopts ":o:" opt
do
case "$opt" in
o) buffer=$OPTARG ;;
*) alert "bashtard/fetch_http" "Unused argument specified: $opt" ;;
esac
done
shift $(( OPTIND -1 ))
[[ -z $buffer ]] && buffer="$(tmpfile)"
notice "bashtard/fetch_http" "Downloading $1 to $buffer"
for util in curl wget
do
command -v "$util" > /dev/null || continue
"fetch_http_$util" "$1" "$buffer" || continue
local exit_code=$?
printf "%s" "$buffer"
return $exit_code
done
die "bashtard/fetch_http" "Unable to download file over HTTP!"
}
fetch_http_curl() {
curl -Ls "$1" > "$2"
}
fetch_http_wget() {
wget --quiet --output-document "$2" "$1"
}
# Check if the first argument given appears in the list of all following
# arguments.
in_args() {
local needle="$1"
shift
for arg in "$@"
do
[[ $needle == "$arg" ]] && return 0
done
return 1
}
# Join a list of arguments into a single string. By default, this will join
# using a ",", but you can set a different character using -c. Note that this
# only joins with a single character, not a string of characters.
join_args() {
local OPTIND
local IFS=","
while getopts ":c:" opt
do
case "$opt" in
c) IFS="$OPTARG" ;;
*) warn "bashtard/join_args" "Unused opt specified: $opt" ;;
esac
done
shift $(( OPTIND - 1))
printf "%s" "$*"
}
# OS independent package management
pkg() {
local system="bashtard/pkg"
local action=$1 ; shift
local pkg
pkg="$(config "pkg.$1")" ; shift
if [[ -z $pkg ]]
then
crit "$system" "No package name for $pkg"
return 1
fi
if [[ "$(type -t "pkg_$action")" != "function" ]]
then
crit "$system" "Invalid package manager action $action"
return 1
fi
"pkg_$action" "$pkg"
}
pkg_install() {
local system="bashtard/pkg/install"
local app=$1 ; shift
case "${BASHTARD_PLATFORM[key]}" in
freebsd) set -- /usr/sbin/pkg install -y "$app" ;;
linux-debian*) set -- apt install -y "$app" ;;
linux-gentoo) set -- emerge --ask=n --update "$app" ;;
*)
crit "$system" "No package manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
;;
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
pkg_uninstall() {
local system="bashtard/pkg/uninstall"
local app=$1 ; shift
case "${BASHTARD_PLATFORM[key]}" in
freebsd) set -- /usr/sbin/pkg uninstall -y "$app" ;;
linux-debian*) set -- apt remove -y "$app" ;;
linux-gentoo) set -- emerge --ask=n --unmerge "$app" ;;
*)
crit "$system" "No package manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
;;
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
# OS independent service management.
svc() {
local system="bashtard/svc"
local service
local action
action=$1 ; shift
service="$(config "svc.$1")" ; shift
if [[ -z $service ]]
then
crit "$system" "No service name for $service"
return 1
fi
if [[ "$(type -t "svc_$action")" != "function" ]]
then
crit "$system" "Invalid service manager action $action"
return 1
fi
"svc_$action" "$service"
}
svc_disable() {
local system="bashtard/svc/disable"
local service=$1
case "${BASHTARD_PLATFORM[key]}" in
linux-gentoo) set -- /sbin/rc-update del "$service" ;;
linux-*) set -- systemctl disable "$service" ;;
*)
crit "$system" "No service manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
svc_enable() {
local system="bashtard/svc/enable"
local service=$1
case "${BASHTARD_PLATFORM[key]}" in
linux-gentoo) set -- /sbin/rc-update add "$service" ;;
linux-*) set -- systemctl enable "$service" ;;
*)
crit "$system" "No service manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
svc_reload() {
local system="bashtard/svc/reload"
local service=$1
case "${BASHTARD_PLATFORM[key]}" in
freebsd) set -- service "$service" reload ;;
linux-gentoo) set -- /sbin/rc-service "$service" reload ;;
linux-*) set -- systemctl reload "$service" ;;
*)
crit "$system" "No service manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
svc_restart() {
local system="bashtard/svc/restart"
local service=$1
case "${BASHTARD_PLATFORM[key]}" in
freebsd) set -- service "$service" restart ;;
linux-gentoo) set -- /sbin/rc-service "$service" restart ;;
linux-*) set -- systemctl restart "$service" ;;
*)
crit "$system" "No service manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
svc_start() {
local system="bashtard/svc/start"
local service=$1
case "${BASHTARD_PLATFORM[key]}" in
freebsd) set -- service "$service" start ;;
linux-gentoo) set -- /sbin/rc-service "$service" start ;;
linux-*) set -- systemctl start "$service" ;;
*)
crit "$system" "No service manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
svc_stop() {
local system="bashtard/svc/stop"
local service=$1
case "${BASHTARD_PLATFORM[key]}" in
freebsd) set -- service "$service" stop ;;
linux-gentoo) set -- /sbin/rc-service "$service" stop ;;
linux-*) set -- systemctl stop "$service" ;;
*)
crit "$system" "No service manager configured for ${BASHTARD_PLATFORM[key]}"
return 1
esac
notice "$system" "$*"
# shellcheck disable=SC2068
$@
}
file_template()
{
local file="$BASHTARD_ETCDIR/playbooks.d/$BASHTARD_PLAYBOOK/share/$1" ; shift
local sedfile
sedfile="$(tmpfile)"
if [[ ! -f $file ]]
then
crit "bashtard/template" "Tried to render template from $file, but it doesn't exist"
return
fi
for kv in "$@"
do
debug "bashtard/template" "Adding $kv to sedfile at $sedfile"
key="$(awk -F= '{ print $1 }' <<< "$kv")"
if [[ -z "$key" ]]
then
crit "bashtard/template" "Empty key in '$kv' while rendering $file?"
fi
value="$(awk -F= '{ print $NF }' <<< "$kv")"
if [[ -z "$value" ]]
then
crit "bashtard/template" "Empty key in '$kv' while rendering $file?"
fi
# shellcheck disable=SC2016
printf 's@${%s}@%s@g\n' "$key" "$value" >> "$sedfile"
done
sed -f "$sedfile" "$file"
}
# Create a temporary directory. Similar to tempfile, but you'll get a directory
# instead.
tmpdir() {
local dir
dir="$(mktemp -d)"
# Ensure the file was created succesfully
if [[ ! -d "$dir" ]]
then
die "bashtard/tmpdir" "Failed to create a temporary directory at $dir"
fi
debug "bashtard/tmpdir" "Temporary file created at $dir"
printf "%s" "$dir"
}
# Create a temporary file. In usage, this is no different from mktemp itself,
# however, it will apply additional checks to ensure everything is going
# correctly, and the files will be cleaned up automatically at the end.
tmpfile() {
local file
file="$(mktemp)"
# Ensure the file was created succesfully
if [[ ! -f "$file" ]]
then
die "bashtard/tmpfile" "Failed to create a temporary file at $file"
fi
debug "bashtard/tmpfile" "Temporary file created at $file"
printf "%s" "$file"
}