#!/usr/bin/env bash set -o pipefail function usage() { local full=${1:-false} cat <&2 && return 254 echo "Commands of proxmox-backup-client:" echo "$PBC" help | tail -n +3 } function pbc-version() { local vers_info rc vers_info=$("$PBC" version) rc=$? (( rc != 0 )) && echo "Error running $PBC version ... exited with rc=$rc" >&2 && return $rc echo "$vers_info" | grep client rc=$? (( rc != 0 )) && echo "Error reading client version, output was:" >&2 && echo "$vers_info" >&2 return $rc } function pbc-backup() { "$PBC" backup "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "${BACKUP[@]}" "$@"; } function pbc-list() { "$PBC" list "${PBC_ARGS[@]}" "$@"; } function pbc-snapshot-list() { "$PBC" snapshot list "${PBC_ARGS[@]}" "$@"; } function pbc-files() { "$PBC" files "${PBC_ARGS[@]}" "$@"; } function pbc-catalog-shell() { "$PBC" catalog shell "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "$@"; } function pbc-mount() { "$PBC" mount "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "$@"; } function get_group_names() { pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"' } function get_snapshot_names() { pbc-snapshot-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])/\(.["backup-time"] | todateiso8601)"' } function get_snapshot_files() { pbc-files "$1" --output-format json | jq -r '.[] | select(.filename | endswith(".pxar.didx")) | .filename | rtrimstr(".didx")' | sort } function apt_get_install() { local package=$1 echo "Installing $package using package manager ..." DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$package" >/dev/null local rc=$? (( rc != 0 )) && echo "Error installing package!" >&2 return $rc } WORK_DIR='' function cleanup_work_dir() { [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR" trap - EXIT } function install_proxmox_backup_client() { local update=${1:-false} local version [ -f "$PBC" ] && local installed=true || local installed=false ! $installed && update=false if $installed && ! $update; then version=$(pbc-version) || return 10 echo "Info: skip installation of proxmox-backup-client, already installed ($version)" return 0 fi if $update && dpkg -S "$PBC" &>/dev/null; then echo "Info: skip update of proxmox-backup-client, package is managed by the package manager" return 0 fi if apt-cache show proxmox-backup-client &>/dev/null; then apt_get_install proxmox-backup-client return $? fi echo "Info: proxmox-backup-client is not available from any configured apt repository" echo "Info: add the Proxmox pbs-client repository to install a signed, apt-managed package" $update && local action='Updating' || local action='Installing' echo "$action proxmox-backup-client-static binary ..." local pkgfile=$(curl -fsSL "$PBC_DOWNLOAD_URL/" | grep proxmox-backup-client-static_ | sed 's#.*href="##g;s#".*##g;' | sort --version-sort | tail -n 1) [ -z "$pkgfile" ] && echo "Unable to determine current package file!" >&2 && return 30 WORK_DIR=$(mktemp -d) trap cleanup_work_dir EXIT echo "Info: downloading $PBC_DOWNLOAD_URL/$pkgfile ..." ! curl -fsSL -o "$WORK_DIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40 echo "Info: extracting $pkgfile ..." ! dpkg-deb -x "$WORK_DIR/$pkgfile" "$WORK_DIR/" && echo "Error extracting file!" >&2 && return 50 echo "Info: installing proxmox-backup-client static binary to $PBC ..." ! install -m 0755 -D "$WORK_DIR/usr/bin/proxmox-backup-client" "$PBC" && echo "Error installing binary!" >&2 && return 60 version=$(pbc-version) || return $? $update && action='updated' || action='installed' echo "Info: successfully $action proxmox-backup-$version" cleanup_work_dir } function arr_contains() { local search=$1 shift [ -z "$search" ] && return 1 while (( $# > 0 )); do [ "$1" == "$search" ] && return 0 shift done return 1 } function may_select() { local value=$1 shift (( $# == 0 )) && echo "Error: no values to select" >&2 && return 1 if [ -z "$value" ]; then if (( $# == 1 )); then value=$1 elif [ ! -t 0 ]; then echo "Error: no value given and stdin is not a terminal" >&2 return 1 else local sorted readarray -t sorted < <(printf '%s\n' "$@" | sort) select value in "${sorted[@]}"; do [ -n "$value" ] && break done fi elif ! arr_contains "$value" "$@"; then return 1 fi [ -z "$value" ] && return 1 echo "$value" } function select_group() { local groups raw raw=$(get_group_names) || { echo "Error: failed to query PBS!" >&2; return 1; } [ -z "$raw" ] && return 2 readarray -t groups <<< "$raw" PS3="Snapshot group [1-${#groups[@]}]: " GROUP=$(may_select "$1" "${groups[@]}") || { echo "Error: invalid group" >&2; return 1; } } function select_snapshot_and_archive() { local snapshots archives raw raw=$(get_snapshot_names) || { echo "Error: failed to query PBS!" >&2; return 1; } [ -z "$raw" ] && return 2 readarray -t snapshots <<< "$raw" PS3="Snapshot [1-${#snapshots[@]}]: " SNAPSHOT=$(may_select "$1" "${snapshots[@]}") || { echo "Error: invalid snapshot" >&2; return 1; } raw=$(get_snapshot_files "$SNAPSHOT") || { echo "Error: failed to query PBS!" >&2; return 1; } [ -z "$raw" ] && return 3 readarray -t archives <<< "$raw" PS3="Archive [1-${#archives[@]}]: " ARCHIVE=$(may_select "$2" "${archives[@]}") || { echo "Error: invalid archive" >&2; return 1; } } SCRIPT_PATH=$(realpath -s "$0") SCRIPT_DIR=$(dirname "$(realpath "$0")") SCRIPT=$(basename "$SCRIPT_PATH") PBC=$(command -v proxmox-backup-client) PBC_DOWNLOAD_URL='http://download.proxmox.com/debian/pbs-client/dists/trixie/main/binary-amd64' ETC_DIR="/etc/$SCRIPT" full_help=false update_pbc=false action='' cfgfile="$ETC_DIR/config" args=() while (( $# > 0 )); do opt=$1 shift if [ -z "$action" ]; then case "$opt" in -c|--config) cfgfile=$1 shift [ -z "$cfgfile" ] && echo "Error: missing argument for $opt" >&2 && usage >&2 && exit 255 ;; -h|--help) usage exit 0 ;; -H|--full-help) full_help=true ;; -*) echo "Error: invalid option: $opt" >&2 echo >&2 usage >&2 exit 255 ;; *) action=$opt ;; esac elif [ "$action" == "install" ]; then case "$opt" in -u|--update) update_pbc=true ;; -*) echo "Error: invalid option: $opt" >&2 echo >&2 usage >&2 exit 255 ;; *) echo "Error: invalid argument: $opt" >&2 echo >&2 usage >&2 exit 255 ;; esac else args+=("$opt") fi done if [ -f "$cfgfile" ]; then perms=$(stat -c '%a' "$cfgfile") (( 8#$perms & 0022 )) && echo "Error: $cfgfile is writable by group or others!" >&2 && exit 202 (( 8#$perms & 0007 )) && echo "Warning: $cfgfile is readable by others!" >&2 fi if $full_help; then [ -f "$cfgfile" ] && source "$cfgfile" PBC=${PBC:-/usr/local/bin/proxmox-backup-client} full_usage exit $? fi [ -z "$action" ] && usage >&2 && exit 255 if [ "$action" == 'install' ]; then if [ -f "$cfgfile" ]; then source "$cfgfile" || exit $? fi PBC=${PBC:-/usr/local/bin/proxmox-backup-client} for exe in apt-cache apt-get dpkg dpkg-deb curl; do ! command -v "$exe" >/dev/null && echo "Error: $exe executable not found!" >&2 && exit 200 done else [ ! -f "$cfgfile" ] && echo "Error: $cfgfile: no such file" >&2 && exit 201 [ ! -r "$cfgfile" ] && echo "Error: $cfgfile: not readable" >&2 && exit 201 source "$cfgfile" || exit $? PBC=${PBC:-/usr/local/bin/proxmox-backup-client} [ ! -f "$PBC" ] && echo "Error: $PBC: no such file" >&2 && exit 200 [ ! -x "$PBC" ] && echo "Error: $PBC: not executable" >&2 && exit 200 ! command -v jq >/dev/null && echo "Error: jq executable not found!" >&2 && exit 200 if [ -n "$ENCRYPTION_KEYFILE" ]; then [ ! -f "$ENCRYPTION_KEYFILE" ] && echo "Error: $ENCRYPTION_KEYFILE: no such file" >&2 && exit 203 [ ! -r "$ENCRYPTION_KEYFILE" ] && echo "Error: $ENCRYPTION_KEYFILE: not readable" >&2 && exit 203 perms=$(stat -c '%a' "$ENCRYPTION_KEYFILE") (( 8#$perms & 0007 )) && echo "Warning: $ENCRYPTION_KEYFILE is readable by others!" >&2 fi fi PBC_ARGS=() KEY_ARGS=() [ -n "$PBS_NAMESPACE" ] && PBC_ARGS+=('--ns' "$PBS_NAMESPACE") [ -n "$ENCRYPTION_KEYFILE" ] && KEY_ARGS+=('--keyfile' "$ENCRYPTION_KEYFILE") export PBS_REPOSITORY="$PBS_USER@$PBS_SERVER:$PBS_DATASTORE" export PBS_PASSWORD="$PBS_PASSWORD" case "$action" in backup) pbc-backup "${args[@]}" || exit $? ;; backup-cron) output=$(pbc-backup "${args[@]}" 2>&1) rc=$? (( rc != 0 )) && echo "$output" >&2 exit $rc ;; list) pbc-list "${args[@]}" || exit $? ;; snapshot-list) select_group "${args[0]}" case $? in 0) ;; 2) echo "no snapshot groups found"; exit 0 ;; *) exit 1 ;; esac echo pbc-snapshot-list "$GROUP" "${args[@]:1}" || exit $? ;; catalog-shell) select_snapshot_and_archive "${args[0]}" "${args[1]}" case $? in 0) ;; 2) echo "no snapshots found"; exit 0 ;; 3) echo "no archives found"; exit 0 ;; *) exit 1 ;; esac echo pbc-catalog-shell "$SNAPSHOT" "$ARCHIVE" || exit $? ;; mount) select_snapshot_and_archive "${args[0]}" "${args[1]}" case $? in 0) ;; 2) echo "no snapshots found"; exit 0 ;; 3) echo "no archives found"; exit 0 ;; *) exit 1 ;; esac target=${args[2]} if [ -z "$target" ]; then [ ! -t 0 ] && echo "Error: no target path given and stdin is not a terminal" >&2 && exit 1 while [ -z "$target" ]; do echo -n "Target path: " ! read -r target && [ -z "$target" ] && echo "Error: no target path given" >&2 && exit 1 [ -d "$target" ] && break echo -e "\nError: $target: no such directory\n" >&2 target='' done elif [ ! -d "$target" ]; then echo "Error: $target: no such directory" >&2 exit 1 fi echo pbc-mount "$SNAPSHOT" "$ARCHIVE" "$target" || exit $? ;; install) (( EUID != 0 )) && echo "Error: installation only works as root!" >&2 && exit 1 install_proxmox_backup_client "$update_pbc" || exit $? if dpkg -s jq &>/dev/null; then $update_pbc && echo "Info: skip update of jq, package is managed by package manager" || echo "Info: jq is already installed" else apt_get_install jq || exit $? fi if [ ! -d "$ETC_DIR" ]; then echo "Creating config directory $ETC_DIR ..." ! mkdir -p "$ETC_DIR" && echo "Error creating directory!" >&2 && exit 3 fi if [ -f "$SCRIPT_DIR/config.example" ]; then if $update_pbc || [ ! -f "$ETC_DIR/config.example" ]; then echo "Install example config to $ETC_DIR/config.example ..." ! install -m 0640 "$SCRIPT_DIR/config.example" "$ETC_DIR/config.example" && echo "Error installing file!" >&2 && exit 4 fi fi echo "Info: successfully installed pbc dependencies" ;; *) "$PBC" "$action" "${args[@]}" || exit $? ;; esac