SHA256
420 lines
12 KiB
Bash
Executable File
420 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -o pipefail
|
|
|
|
function usage() {
|
|
local full=${1:-false}
|
|
cat <<EOF
|
|
Usage: $SCRIPT [-h] [-H] [-c config] command [OPTIONS]
|
|
|
|
Helper script for proxmox-backup-client.
|
|
|
|
Options:
|
|
-h, --help show this help message and exit
|
|
-H, --full-help show this help message and all commands of proxmox-backup-client
|
|
if it is available
|
|
-c, --config CONFIG path to config file, default: $ETC_DIR/config
|
|
|
|
Commands:
|
|
|
|
backup run backup and print output to screen
|
|
backup-cron run backup and suppress output on success
|
|
list list groups, latest snapshots and archives
|
|
snapshot-list [group] list snapshots, sizes and archives
|
|
catalog-shell [snapshot [archive]] run interactive shell to explore arhive contents
|
|
and restore files/dirs selectively
|
|
mount [snapshot [archive [target]]] mount snapshot archive locally
|
|
install [--update] install/update proxmox-backup-client and pdb dependencies
|
|
|
|
EOF
|
|
! $full && cat <<EOF
|
|
Other commands and options are passed to proxmox-backup-client.
|
|
See \`$SCRIPT -H\` for a full help message including all available commands.
|
|
|
|
EOF
|
|
}
|
|
|
|
function full_usage() {
|
|
usage true
|
|
[ ! -x "$PBC" ] && echo "Error: $PBC is not executable!" >&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
|