Files
pbc/pbc
T

427 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
function usage() {
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 --ns "$PBS_NAMESPACE" "${BACKUP[@]}"
return $?
}
function pbc-list() {
"$PBC" list --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-snapshot-list() {
"$PBC" snapshot list --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-files() {
"$PBC" files --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-catalog-shell() {
"$PBC" catalog shell --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-mount() {
"$PBC" mount --ns "$PBS_NAMESPACE" "$@"
return $?
}
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
}
TMPDIR=''
function cleanup_tmpdir() {
[ -n "$TMPDIR" -a -d "$TMPDIR" ] && rm -r "$TMPDIR"
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
TMPDIR=$(mktemp -d)
trap cleanup_tmpdir EXIT
echo "Info: downloading $PBC_DOWNLOAD_URL/$pkgfile ..."
! curl -fsSL -o "$TMPDIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40
echo "Info: extracting $pkgfile ..."
! dpkg-deb -x "$TMPDIR/$pkgfile" "$TMPDIR/" && echo "Error extracting file!" >&2 && return 50
echo "Info: installing proxmox-backup-client static binary to $PBC ..."
! install -m 0755 -D "$TMPDIR/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_tmpdir
}
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"
}
SCRIPT_PATH=$(realpath -s "$0")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
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
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" ] && usage && exit 255
;;
-h|--help)
usage
exit 0
;;
-H|--full-help)
FULL_HELP=true
;;
-*)
echo "Error: invalid option: $opt" >&2
echo
usage
exit 255
;;
*)
action=$opt
;;
esac
elif [ "$action" == "install" ]; then
case "$opt" in
-u|--update)
args+=('true')
;;
-*)
echo "Error: invalid option: $opt" >&2
echo
usage
exit 255
;;
*)
echo "Error: invalid argument: $opt" >&2
echo
usage
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
fi
export PBS_REPOSITORY="$PBS_USER@$PBS_SERVER:$PBS_DATASTORE"
export PBS_PASSWORD="$PBS_PASSWORD"
case "$action" in
backup)
pbc-backup || exit $?
;;
backup-cron)
output=$(pbc-backup 2>&1)
rc=$?
(( rc != 0 )) && echo "$output" >&2
exit $rc
;;
list)
pbc-list "${args[@]}" || exit $?
;;
snapshot-list)
readarray -t groups < <(get_group_names)
(( ${#groups[@]} == 0 )) && echo "no snapshot groups found" && exit 0
group=${args[0]}
args=("${args[@]:1}")
PS3="Snapshot group [1-${#groups[@]}]: "
group=$(may_select "$group" "${groups[@]}")
(( $? != 0 )) && echo "Error: invalid group" >&2 && exit 1
echo
pbc-snapshot-list "$group" "${args[@]}" || exit $?
;;
catalog-shell)
readarray -t snapshots < <(get_snapshot_names)
(( ${#snapshots[@]} == 0 )) && echo "no snapshots found" && exit 0
snapshot=${args[0]}
args=("${args[@]:1}")
PS3="Snapshot [1-${#snapshots[@]}]: "
snapshot=$(may_select "$snapshot" "${snapshots[@]}")
(( $? != 0 )) && echo "Error: invalid snapshot" >&2 && exit 1
readarray -t archives < <(get_snapshot_files "$snapshot")
(( ${#archives[@]} == 0 )) && echo "no archives found" && exit 0
archive=${args[0]}
args=("${args[@]:1}")
PS3="Archive [1-${#archives[@]}]: "
archive=$(may_select "$archive" "${archives[@]}")
(( $? != 0 )) && echo "Error: invalid archive" >&2 && exit 1
echo
pbc-catalog-shell "$snapshot" "$archive" || exit $?
;;
mount)
readarray -t snapshots < <(get_snapshot_names)
(( ${#snapshots[@]} == 0 )) && echo "no snapshots found" && exit 0
snapshot=${args[0]}
args=("${args[@]:1}")
PS3="Snapshot [1-${#snapshots[@]}]: "
snapshot=$(may_select "$snapshot" "${snapshots[@]}")
(( $? != 0 )) && echo "Error: invalid snapshot" >&2 && exit 1
readarray -t archives < <(get_snapshot_files "$snapshot")
(( ${#archives[@]} == 0 )) && echo "no archives found" && exit 0
archive=${args[0]}
args=("${args[@]:1}")
PS3="Archive [1-${#archives[@]}]: "
archive=$(may_select "$archive" "${archives[@]}")
(( $? != 0 )) && echo "Error: invalid archive" >&2 && exit 1
target=${args[0]}
args=("${args[@]:1}")
if [ -z "$target" ]; then
while [ -z "$target" ]; do
echo -n "Target path: "
read -r target
[ -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)
update=${args[0]:-false}
[ "$(whoami)" != "root" ] && echo "Error: installation only works as root!" >&2 && exit 1
install_proxmox_backup_client "$update" || exit $?
if dpkg -s jq &>/dev/null; then
$update && 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
ETC_DIR="/etc/$SCRIPT"
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 || [ ! -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