aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPatrick Spek <p.spek@tyil.nl>2022-04-15 16:32:43 +0200
committerPatrick Spek <p.spek@tyil.nl>2022-04-15 16:32:43 +0200
commitd8a2f732b300cdbb892e0878fe87dbb7a0ef6d03 (patch)
treed364845506af8f3080c79df9a91bb3e32cc4b4d8 /lib
Initial commit
Diffstat (limited to 'lib')
-rw-r--r--lib/logging.bash22
-rw-r--r--lib/main.bash163
-rw-r--r--lib/subcommands/bootstrap.bash72
-rw-r--r--lib/subcommands/ssh.bash39
-rw-r--r--lib/subcommands/sysinfo.bash11
-rw-r--r--lib/util.bash365
6 files changed, 672 insertions, 0 deletions
diff --git a/lib/logging.bash b/lib/logging.bash
new file mode 100644
index 0000000..05b95c6
--- /dev/null
+++ b/lib/logging.bash
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+# The base function to output logging information. This should *not* be used
+# directly, but the helper functions can be used safely.
+log() {
+ local system=$1 ; shift
+
+ printf "\e[32m[%s]\e[m \e[33m%s[\e[m%s\e[0;33m]\e[m: %s\e[m\n" \
+ "$(date +%FT%T)" \
+ "$system" \
+ "$$" \
+ "$*" \
+ >&2
+}
+
+debug() { [[ -n $BASHTARD_DEBUG ]] && log "$1" "$(printf "\e[0;37m%s" "${@:2}")" ; }
+info() { log "$1" "$(printf "\e[m%s" "${@:2}")" ; }
+notice() { log "$1" "$(printf "\e[0;34m%s" "${@:2}")" ; }
+warn() { log "$1" "$(printf "\e[1;39m%s" "${@:2}")" ; }
+crit() { log "$1" "$(printf "\e[0;33m%s" "${@:2}")" ; }
+alert() { log "$1" "$(printf "\e[0;31m%s" "${@:2}")" ; }
+emerg() { log "$1" "$(printf "\e[1;31m%s" "${@:2}")" ; }
diff --git a/lib/main.bash b/lib/main.bash
new file mode 100644
index 0000000..8075b59
--- /dev/null
+++ b/lib/main.bash
@@ -0,0 +1,163 @@
+#!/usr/bin/env bash
+
+# shellcheck source=lib/util.bash
+source "$(dirname "${BASH_SOURCE[0]}")/util.bash"
+
+# shellcheck source=lib/logging.bash
+source "$(dirname "${BASH_SOURCE[0]}")/logging.bash"
+
+main() {
+ debug "$BASHTARD_NAME/main" "Running from $BASHTARD_BASEDIR"
+ debug "$BASHTARD_NAME/main" "Configuration dir is at $BASHTARD_ETCDIR"
+ debug "$BASHTARD_NAME/main" "> $0 $*"
+
+ [[ -z $1 ]] && usage && exit 2
+
+ local subcommand="$1"
+ shift
+
+ debug "$BASHTARD_NAME/main" "Handling subcommand '$subcommand'"
+
+ subcommand_src="$BASHTARD_BASEDIR/lib/subcommands/$subcommand.bash"
+
+ debug "$BASHTARD_NAME/main" "Checking $subcommand_src"
+
+ if [[ ! -f $subcommand_src ]]
+ then
+ debug "$BASHTARD_NAME/main" "No script found to handle action, showing usage"
+ usage
+ exit 2
+ fi
+
+ # Declare some global variables
+ declare -A BASHTARD_PLATFORM
+
+ # Figure out system details
+ debug "$BASHTARD_NAME/main" "Discovering system information"
+ discover_system
+
+ # Export BASHTARD_ variables
+ export BASHTARD_PLATFORM
+ export BASHTARD_SYSTEM="${subcommand%%/*}"
+
+ # Source the file defining the subcommand.
+ debug "$BASHTARD_NAME/main" "Sourcing $subcommand_src"
+ source "$subcommand_src"
+
+ # Maintain our own tempdir
+ export TMPDIR="$BASEDIR/tmp/$RANDOM"
+ mkdir -p -- "$TMPDIR"
+ debug "$BASHTARD_NAME/main" "\$TMPDIR set to $TMPDIR"
+
+ # Actually perform the subcommand
+ debug "$BASHTARD_NAME/main" "Running subcommand"
+ subcommand "$@"
+ local subcommand_exit=$?
+
+ # Clean up if necessary
+ if [[ -z $BASHTARD_MESSY ]]
+ then
+ debug "$BASHTARD_NAME/main" "Cleaning up tempfiles at $TMPDIR"
+ rm -rf -- "$TMPDIR"
+ fi
+
+ ## Use the subcommand's exit code
+ exit $subcommand_exit
+}
+
+usage() {
+ cat <<EOF
+Usage:
+ $BASHTARD_NAME -h
+ $BASHTARD_NAME add <playbook>
+ $BASHTARD_NAME bootstrap
+ $BASHTARD_NAME del <playbook>
+ $BASHTARD_NAME ssh <command>
+ $BASHTARD_NAME sync
+ $BASHTARD_NAME sysinfo
+
+Perform maintenance on your infra.
+
+Commands:
+ add Add a configuration playbook to this machine.
+ bootstrap Bootstrap the $BASHTARD_NAME configuration system.
+ del Remove a configuration playbook from this machine.
+ ssh Run a given command on all known hosts.
+ sync Pull latest changes through git, and synchronize all added
+ playbooks.
+ sysinfo Show gathered information about this system.
+
+Playbooks:
+EOF
+}
+
+# Discover information about the system. If any bugs are reported and you want
+# more information about the system the user is running on, additional checks
+# can be added here, and the user will simply have to include the output of the
+# sysinfo command in their message to you.
+discover_system() {
+ BASHTARD_PLATFORM["os"]="$(discover_system_os)"
+ BASHTARD_PLATFORM["arch"]="$(discover_system_arch)"
+ BASHTARD_PLATFORM["version"]="$(discover_system_version)"
+ BASHTARD_PLATFORM["term"]="$TERM"
+ BASHTARD_PLATFORM["fqdn"]="$(hostname -f)"
+
+ # When on a Linux-using OS, check for the specific distribution in use.
+ if [[ ${BASHTARD_PLATFORM[os]} == *"linux"* ]]
+ then
+ BASHTARD_PLATFORM["distro"]="$(discover_system_distro)"
+ fi
+
+ BASHTARD_PLATFORM[key]="$(discover_system_key)"
+}
+
+discover_system_arch() {
+ uname -m
+}
+
+discover_system_distro() {
+ if [[ -f /etc/os-release ]]
+ then
+ (
+ source /etc/os-release
+ printf "%s" "$NAME" \
+ | awk '{print tolower($0)}' \
+ | sed 's@[/+ ]@_@g'
+ )
+ return
+ fi
+
+ crit "No /etc/os-release found. Are you sure you're on a sane GNU+Linux distribution?"
+
+ if command -v pacman > /dev/null
+ then
+ warn "$BASHTARD_NAME/main" "Found pacman, assuming Archlinux as distro."
+ printf "%s" "archlinux"
+ return
+ fi
+}
+
+discover_system_version() {
+ printf "%s" "$(uname -r | awk '{print tolower($0)}')"
+}
+
+discover_system_key() {
+ key+="${BASHTARD_PLATFORM[os]}"
+
+ if [[ ${BASHTARD_PLATFORM[distro]} ]]
+ then
+ key+="-${BASHTARD_PLATFORM[distro]}"
+ fi
+
+ printf "%s" "$key"
+}
+
+discover_system_os() {
+ if command -v uname > /dev/null
+ then
+ printf "%s" "$(uname -s | awk '{print tolower($0)}' | sed 's@[/+ ]@_@g')"
+ return
+ fi
+}
+
+main "$@"
diff --git a/lib/subcommands/bootstrap.bash b/lib/subcommands/bootstrap.bash
new file mode 100644
index 0000000..b2e540b
--- /dev/null
+++ b/lib/subcommands/bootstrap.bash
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+
+subcommand()
+{
+ remote="$1" ; shift
+
+ if [[ -z "$remote" ]]
+ then
+ info "bootstrap" "No remote given, bootstrapping from scratch"
+ bootstrap_local
+ return
+ fi
+
+ bootstrap_remote
+}
+
+bootstrap_local()
+{
+ local dirs=(
+ "$BASHTARD_ETCDIR"
+ "$BASHTARD_ETCDIR/hosts.d"
+ "$BASHTARD_ETCDIR/os.d"
+ "$BASHTARD_ETCDIR/playbooks.d"
+ "$BASHTARD_ETCDIR/playbook-registry.d/"
+ )
+
+ local files=(
+ "$BASHTARD_ETCDIR/defaults"
+ "$BASHTARD_ETCDIR/playbooks"
+ "$BASHTARD_ETCDIR/playbook-registry.d/${BASHTARD_PLATFORM[fqdn]}"
+ "$BASHTARD_ETCDIR/hosts.d/${BASHTARD_PLATFORM[fqdn]}"
+ "$BASHTARD_ETCDIR/os.d/${BASHTARD_PLATFORM[key]}"
+ )
+
+ for dir in "${dirs[@]}"
+ do
+ notice "bootstrap" "Creating $dir"
+ mkdir -p -- "$dir"
+ done
+
+ for file in "${files[@]}"
+ do
+ notice "bootstrap" "Creating $file"
+ touch -- "$file"
+ done
+}
+
+bootstrap_remote()
+{
+ notice "bootstrap" "Cloning $1 to $BASHTARD_ETCDIR"
+ git clone "$1" "$BASHTARD_ETCDIR"
+
+ local files=(
+ "$BASHTARD_ETCDIR/hosts.d/${BASHTARD_PLATFORM[fqdn]}"
+ "$BASHTARD_ETCDIR/os.d/${BASHTARD_PLATFORM[key]}"
+ "$BASHTARD_ETCDIR/playbook-registry.d/${BASHTARD_PLATFORM[fqdn]}"
+ )
+
+ for file in "${files[@]}"
+ do
+ [[ -f "$file" ]] && continue
+
+ notice "bootstrap" "Creating $file"
+ touch -- "$file"
+ done
+
+ while read -r playbook url
+ do
+ notice bootstrap "Cloning $playbook from $url"
+ git clone "$url" "$BASHTARD_ETCDIR/playbooks.d"
+ done < "$BASHTARD_ETCDIR/playbooks"
+}
diff --git a/lib/subcommands/ssh.bash b/lib/subcommands/ssh.bash
new file mode 100644
index 0000000..19ead9a
--- /dev/null
+++ b/lib/subcommands/ssh.bash
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+subcommand()
+{
+ local ssh="$(config app.ssh)"
+
+ if [[ ! -d "$BASHTARD_ETCDIR/hosts.d" ]]
+ then
+ crit "$BASHTARD_NAME/ssh" "Could not find hosts file at $BASHTARD_ETCDIR/hosts.d"
+ return 3
+ fi
+
+ chgdir "$BASHTARD_ETCDIR/hosts.d"
+
+ for node in *
+ do
+ local user
+ local host
+ local ip
+
+ user="$(config_for "$node" "ssh.user" "root")"
+
+ # Try IPv6 first
+ host="$(config_for "$node" "ssh.host" "$(config_for "$node" "vpn.ipv6")")"
+
+ if [[ -z "$host" ]]
+ then
+ # Otherwise try IPv4
+ host="$(config_for "$node" "ssh.host" "$(config_for "$node" "vpn.ipv4")")"
+ fi
+
+ notice "ssh" "$user@$node ($host) > $*"
+
+ $ssh "$user@$host" "$@"
+
+ unset user
+ unset host
+ done
+}
diff --git a/lib/subcommands/sysinfo.bash b/lib/subcommands/sysinfo.bash
new file mode 100644
index 0000000..5010798
--- /dev/null
+++ b/lib/subcommands/sysinfo.bash
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+subcommand() {
+ printf "%-15s %s\n" "basedir" "$BASHTARD_BASEDIR"
+ printf "%-15s %s\n" "etcdir" "$BASHTARD_ETCDIR"
+
+ for key in "${!BASHTARD_PLATFORM[@]}"
+ do
+ printf "%-15s %s\n" "$key" "${BASHTARD_PLATFORM[$key]}"
+ done
+}
diff --git a/lib/util.bash b/lib/util.bash
new file mode 100644
index 0000000..2c10ef4
--- /dev/null
+++ b/lib/util.bash
@@ -0,0 +1,365 @@
+#!/usr/bin/env bash
+
+# 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 "tyilnet/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 default
+ local file
+ local files
+ local host
+ local value
+ local key
+
+ host=$1 ; shift
+ key=$1 ; shift
+ default=$1 ; shift
+
+ files=(
+ "$BASHTARD_ETCDIR/hosts.d/$host"
+ "$BASHTARD_ETCDIR/os.d/${BASHTARD_PLATFORM[key]}"
+ "$BASHTARD_ETCDIR/defaults"
+ "$BASHTARD_BASEDIR/etc/os.d/${BASHTARD_PLATFORM[key]}"
+ "$BASHTARD_BASEDIR/etc/defaults"
+ )
+
+ # Check configuration files
+ for file in "${files[@]}"
+ do
+ [[ ! -f $file ]] && continue
+
+ value="$(awk -F= '$1 == "'"$key"'" { print $NF }' "$file")"
+
+ if [[ -n $value ]]
+ then
+ debug "tyilnet/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 "tyilnet/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 "tyilnet/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 "tyilnet/fetch_http" "Unused argument specified: $opt" ;;
+ esac
+ done
+
+ shift $(( OPTIND -1 ))
+
+ [[ -z $buffer ]] && buffer="$(tmpfile)"
+
+ notice "tyilnet/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 "tyilnet/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 "tyilnet/join_args" "Unused opt specified: $opt" ;;
+ esac
+ done
+
+ shift $(( OPTIND - 1))
+
+ printf "%s" "$*"
+}
+
+# OS independent package management
+pkg() {
+ local system="tyilnet/pkg"
+
+ local action=$1 ; shift
+ local app="$(config "pkg.$1" "$(config "app.$1")")" ; shift
+
+ local type
+
+ if [[ -z $app ]]
+ then
+ crit "$system" "No package name for $app"
+ return 1
+ fi
+
+ if [[ "$(type -t pkg_$action)" != "function" ]]
+ then
+ crit "$system" "Invalid package manager action $action"
+ return 1
+ fi
+
+ "pkg_$action" "$app"
+}
+
+pkg_install() {
+ local system="tyilnet/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 "$app" ;;
+ *)
+ crit "$system" "No package manager configured for ${BASHTARD_PLATFORM[key]}"
+ return 1
+ ;;
+ esac
+
+ notice "$system" "$*"
+ $@
+}
+
+# OS independent service management.
+svc() {
+ local system="tyilnet/svc"
+
+ local service
+ local action
+
+ service="$(config svc.$1)" ; shift
+ action=$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_enable() {
+ local system="tyilnet/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" "$*"
+ $@
+}
+
+svc_restart() {
+ local system="tyilnet/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" "$*"
+ $@
+}
+
+svc_start() {
+ local system="tyilnet/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" "$*"
+ $@
+}
+
+template()
+{
+ local file="$BASEDIR/etc/templates/$1" ; shift
+ local sedfile="$(tmpfile)"
+
+ if [[ ! -f $file ]]
+ then
+ crit "tyilnet/template" "Tried to render template from $file, but it doesn't exist"
+ return
+ fi
+
+ for kv in "$@"
+ do
+ debug "tyilnet/template" "Adding $kv to sedfile at $sedfile"
+
+ key="$(awk -F= '{ print $1 }' <<< $kv)"
+
+ if [[ -z "$key" ]]
+ then
+ crit "tyilnet/template" "Empty key in '$kv' while rendering $file?"
+ fi
+
+ value="$(awk -F= '{ print $NF }' <<< $kv)"
+
+ if [[ -z "$value" ]]
+ then
+ crit "tyilnet/template" "Empty key in '$kv' while rendering $file?"
+ fi
+
+ 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 "tyilnet/tmpdir" "Failed to create a temporary directory at $dir"
+ fi
+
+ debug "tyilnet/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 "tyilnet/tmpfile" "Failed to create a temporary file at $file"
+ fi
+
+ debug "tyilnet/tmpfile" "Temporary file created at $file"
+
+ printf "%s" "$file"
+}