Compare commits

..
41 Commits
Author SHA256 Message Date
spacefreak 873601019a always install config.example if available 2026-08-27 23:44:08 +02:00
spacefreak 0ade18796f cleanup source 2026-08-27 23:39:46 +02:00
spacefreak 0f61e177f8 allow installation of a specific tag/branch by exporting PBC_GIT_TAG 2026-08-27 23:29:43 +02:00
spacefreak 2fdf065017 document encryption key handling in README.md 2026-08-27 23:14:49 +02:00
spacefreak 6239fe5ca6 add support for encryption keys 2026-08-27 23:14:18 +02:00
spacefreak bc8287a28c only look for version tags 2026-08-24 01:11:01 +02:00
spacefreak 04d351a859 add WSL section to README.md 2026-08-24 01:07:11 +02:00
spacefreak d5d4ea2430 change README.md 2026-08-24 00:53:49 +02:00
spacefreak e423b97c8a add README.md 2026-08-24 00:40:09 +02:00
spacefreak 01c299e242 fail instead of looping when no mount target can be read 2026-08-22 07:45:56 +02:00
spacefreak 56b619372d report PBC query failures instead of an empty datastore 2026-08-22 07:39:16 +02:00
spacefreak 73202f5e3b extract shared snapshot and archive selection logic 2026-08-22 07:36:30 +02:00
spacefreak d243ea1c89 resolve symlinks for SCRIPT_DIR lookup 2026-08-22 06:58:45 +02:00
spacefreak 0623c6a985 remove duplicate ETC_DIR assignment 2026-08-22 06:54:35 +02:00
spacefreak 4fb949c26e declare variable full as local in usage 2026-08-22 06:51:56 +02:00
spacefreak e065ab2cd7 use dedicated variable update_pbc for the install flag 2026-08-22 06:48:59 +02:00
spacefreak 7421c8f43e remove redundant $ inside arithmetic contexts 2026-08-22 06:43:04 +02:00
spacefreak e184d1c92c test may_select directly instead of $? 2026-08-22 06:41:05 +02:00
spacefreak ee86603dcd forward extra arguments to proxmox-backup-client backup 2026-08-22 06:37:37 +02:00
spacefreak 1eee4b2d1d remove no-op return from the pbc-* wrappers 2026-08-22 06:35:53 +02:00
spacefreak 7555745704 install pbc with an explicit mode and owner 2026-08-22 06:26:20 +02:00
spacefreak 49d2e35106 parse gitea tags without jq 2026-08-22 06:24:26 +02:00
spacefreak 9722741bfd check EUID instead of whoami for root 2026-08-22 06:21:31 +02:00
spacefreak 22abaf6431 rename TMPDIR to WORK_DIR 2026-08-22 06:19:51 +02:00
spacefreak 4e24ee218c send usage to stderr on error paths 2026-08-22 06:17:51 +02:00
spacefreak acac5b685b show usage when no command is given 2026-08-22 06:15:59 +02:00
spacefreak 3869ca5a95 install example config with a restrictive mode 2026-08-22 06:12:38 +02:00
spacefreak 3d5757effc check config file permissions before sourcing 2026-08-22 06:10:50 +02:00
spacefreak 6c7db396b2 make apt-get install non-interactive 2026-08-22 06:07:40 +02:00
spacefreak eeb0c9cb47 install client binary instead of cp 2026-08-22 06:06:29 +02:00
spacefreak 44cc111b3d fix version sorting when determining debian pkg to download 2026-08-22 06:03:06 +02:00
spacefreak f0610091bb fail curl on http errors 2026-08-22 06:00:44 +02:00
spacefreak f93f647504 add guard to may_select in case stdin is not a terminal 2026-08-22 05:54:50 +02:00
spacefreak 32e88beedc fix argument iteration 2026-08-22 05:48:23 +02:00
spacefreak 6b2847fec7 make full_usage aware of possible PBC setting in config 2026-08-22 05:46:16 +02:00
spacefreak 127fb83567 fix executable checks 2026-08-22 05:37:25 +02:00
spacefreak bfa15561d8 use local keywords only once for version variable 2026-08-22 05:34:26 +02:00
spacefreak 22675d1be3 fix error handling 2026-08-22 05:31:27 +02:00
spacefreak 62f6865a06 fix pkg install status check with apt-cache and mention pbs-client repos 2026-08-22 05:20:23 +02:00
spacefreak 331f75a943 replace whish -s with command -v 2026-08-22 05:15:25 +02:00
spacefreak d1791cfcc8 fix exit code and output to stderr in backup-cron command 2026-08-22 05:09:15 +02:00
4 changed files with 485 additions and 156 deletions
+315
View File
@@ -0,0 +1,315 @@
# pbc
A Bash wrapper around Proxmox's `proxmox-backup-client` for Proxmox Backup Server (PBS).
It keeps the repository, credentials and the list of paths to back up in a single config
file, injects the configured namespace (`--ns`) and encryption key (`--keyfile`) into every
command that needs them, and lets you pick a snapshot group, snapshot and archive from an
interactive menu instead of typing them out. Any command it does not implement itself is
passed straight through to `proxmox-backup-client`.
## Requirements
- A Debian-based host with `bash` and `curl`
- `jq` and `proxmox-backup-client` — both installed by `pbc install`
- A reachable PBS with a datastore and an API token
## Installation
```bash
curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.sh | sudo bash
```
This installs the newest tagged version to `/usr/local/bin/pbc`, installs the
dependencies, and puts an example config at `/etc/pbc/config.example`.
### Installing a specific version
Set `PBC_GIT_TAG` to install that tag instead of the newest one:
```bash
curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.sh | sudo PBC_GIT_TAG=v1.0.0 bash
```
`sudo` drops the environment, so set the variable on the `sudo` command itself (or use
`sudo -E`), not before `curl`. Any Git ref works, e.g. a branch name for testing.
### Updating
Run the same command again to update `pbc` itself:
```bash
curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.sh | sudo bash
```
It fetches the newest tag and overwrites `/usr/local/bin/pbc`. Your `/etc/pbc/config` is
left untouched. `PBC_GIT_TAG` works here too, to pin or roll back to a given version.
To update only the `proxmox-backup-client` binary:
```bash
sudo pbc install --update
```
This is skipped when the client is managed by the package manager — use `apt` in that case.
## Configuration
```bash
sudo cp /etc/pbc/config.example /etc/pbc/config
sudo chmod 640 /etc/pbc/config
sudo editor /etc/pbc/config
```
| Key | Description |
|---|---|
| `PBS_SERVER` | PBS host and port |
| `PBS_USER` | User and API token name, `user@realm:token-name` |
| `PBS_PASSWORD` | The API token secret |
| `PBS_DATASTORE` | Datastore to back up to |
| `PBS_NAMESPACE` | Optional, namespace inside the datastore |
| `BACKUP` | Array of `archive-name.pxar:/path` entries to back up |
| `ENCRYPTION_KEYFILE` | Optional, path to the client encryption key — see [Encryption](#encryption) |
| `PBC` | Optional, path to `proxmox-backup-client` (default `/usr/local/bin/proxmox-backup-client`) |
`pbc` never creates the config for you — copy the example and edit it yourself. The file
has to be readable for the user that runs `pbc`, so if that is not root, hand
it over:
```bash
sudo chown youruser /etc/pbc/config
```
The config is sourced as Bash and contains your token secret. `pbc` refuses to run if the
file is writable by group or others, and warns if it is readable by others.
Check that it works:
```bash
pbc list
```
Use a different config with `-c`:
```bash
pbc -c ./myconfig list
```
## Encryption
Backups can be encrypted client-side, so PBS only ever sees ciphertext. Create a key and
point `ENCRYPTION_KEYFILE` at it:
```bash
sudo proxmox-backup-client key create /etc/pbc/backup.key --kdf none
sudo chmod 600 /etc/pbc/backup.key
```
`pbc` errors out if the key is missing or unreadable, and warns if it is readable by
others. As with the config, the file has to be readable for the user that runs `pbc`
`sudo chown youruser /etc/pbc/backup.key` if that is not root.
**Back the key up somewhere else.** Without it the backups are unrecoverable, and a key
stored only on the machine you are backing up is gone exactly when you need it. Print a
recovery sheet and keep it off-host:
```bash
sudo proxmox-backup-client key paperkey /etc/pbc/backup.key
```
`--kdf none` leaves the key unprotected on disk, which is what makes unattended backups
possible. With a passphrase-protected key (`--kdf scrypt`) the client prompts on every run
and `backup-cron` hangs; export `PBS_ENCRYPTION_PASSWORD` in that case.
The key is passed to `backup`, `backup-cron`, `mount` and `catalog-shell`. It is *not*
passed to commands that fall through to `proxmox-backup-client``pbc restore` needs both
flags spelled out:
```bash
pbc restore --ns MyBackups --keyfile /etc/pbc/backup.key \
host/myhost/2026-08-24T01:00:00Z root.pxar /mnt/restore
```
Setting `ENCRYPTION_KEYFILE` only affects snapshots made from then on. Older unencrypted
snapshots stay readable, and listing works without the key either way — only reading
archive contents needs it.
## Usage
Run `pbc --help` for the synopsis and the list of options, or `pbc -H` to additionally
list every command of `proxmox-backup-client`. The commands `pbc` implements itself are
described below.
### backup
Back up everything listed in `BACKUP` and print the client output:
```bash
pbc backup
```
Extra arguments are forwarded to the client:
```bash
pbc backup --exclude '/mnt/data/cache'
```
### backup-cron
Same backup, but output is suppressed unless it fails — so cron only mails you on errors:
```cron
0 3 * * * /usr/local/bin/pbc backup-cron
```
### list
List backup groups with their latest snapshot and archives:
```bash
pbc list
```
### snapshot-list
List snapshots of a group with sizes and archives. Without an argument you get a menu of
the available groups:
```bash
pbc snapshot-list
pbc snapshot-list host/myhost
```
### mount
Mount a single archive of a snapshot on a local directory. Anything you leave out is
asked for interactively:
```bash
pbc mount
pbc mount host/myhost/2026-08-24T01:00:00Z root.pxar /mnt/restore
```
The target directory is not created for you — create it beforehand and make sure it is
writable for the user that runs `pbc`. Encrypted archives are decrypted transparently when
`ENCRYPTION_KEYFILE` is configured. Unmount when done:
```bash
umount /mnt/restore
```
### catalog-shell
Open an interactive shell to browse an archive and restore selected files — see below.
Encrypted archives are decrypted transparently when `ENCRYPTION_KEYFILE` is configured.
```bash
pbc catalog-shell
pbc catalog-shell host/myhost/2026-08-24T01:00:00Z root.pxar
```
### install
Install or update the dependencies. Requires root:
```bash
sudo pbc install
sudo pbc install --update
```
## Restoring files
### Selectively, with `catalog-shell`
```bash
pbc catalog-shell
```
Pick a snapshot and an archive from the menu, then browse the archive as if it were a
filesystem and mark what you want back:
```
pxar:/ > cd etc
pxar:/etc > ls
pxar:/etc > select hosts
pxar:/etc > find etc/nginx/** --select
pxar:/etc > list-selected
pxar:/etc > restore-selected /mnt/restore
pxar:/etc > exit
```
- `ls`, `cd`, `pwd`, `stat` — browse the archive
- `select <path>` — mark a single file, relative to the current directory
- `find <pattern> --select` — mark everything matching a glob
- `deselect <path>` / `clear-selected` — drop one entry or all of them
- `list-selected` — show what is currently marked
- `restore-selected <target>` — restore only the marked entries
- `restore <target> [pattern]` — restore the current directory and everything below it
- `help` — list all available shell commands
- `exit` — leave the shell
#### Selecting a directory and everything beneath it
`select` marks exactly the entry you give it and nothing else. Marking a directory
therefore restores an empty directory — its contents are *not* included. Use a glob with
`find --select` instead:
```
pxar:/ > find etc/nginx/** --select
pxar:/ > select etc/nginx
pxar:/ > restore-selected /mnt/restore
```
The `find` line picks up everything below `etc/nginx`; the `select` line adds the
directory itself, so its own permissions and ownership are restored as well.
Note that `find` patterns are always matched against paths relative to the archive root,
no matter which directory you are in, and that `find` scans the whole archive — expect it
to take a while on large backups.
The target path must not exist yet; the restore creates it. Its parent directory has to be
writable for the user that runs `pbc`.
### Everything, or with normal tools
To copy files out with `cp`, `rsync` or a file manager, mount the archive instead:
```bash
mkdir -p /mnt/restore # must exist and be writable for the user running pbc
pbc mount
cp -a /mnt/restore/etc/nginx /etc/nginx
umount /mnt/restore
```
## Running in WSL
`pbc` also works inside a Debian-based WSL instance, which makes it a way to back up
Windows directories: the Windows drives show up under `/mnt`, so they can be listed in
`BACKUP` like any other path.
```bash
BACKUP=('users.pxar:/mnt/c/Users' 'projects.pxar:/mnt/d/projects')
```
A few things to keep in mind:
- Access to files under `/mnt` is governed by Windows, not by the user you are inside the
WSL instance — `sudo` does not help there. To back up paths your Windows user cannot
read, start the WSL instance itself as administrator (run the terminal or `wsl.exe` via
*Run as administrator*), then run `pbc` in it.
- The owner and permission metadata stored in the archive is the one WSL synthesizes for
Windows files, not the original Windows ACLs.
- Cron is not running in a WSL instance by default, so `backup-cron` only fires if you
enable it — either by starting `cron` yourself, or by triggering `pbc backup-cron` from
the Windows Task Scheduler with `wsl.exe`:
```
wsl.exe -d Debian -u root /usr/local/bin/pbc backup-cron
```
For the reason above, such a task has to run with highest privileges to reach files that
are not accessible to your Windows user.
## License
GPLv3 — see [LICENSE](LICENSE).
+1
View File
@@ -3,5 +3,6 @@ PBS_USER='backup@pam:token-name'
PBS_PASSWORD='XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
PBS_DATASTORE='backup'
PBS_NAMESPACE='MyBackups'
#ENCRYPTION_KEYFILE='/etc/pbc/backup.key'
BACKUP=('root.pxar:/' 'data.pxar:/mnt/data')
+18 -18
View File
@@ -1,52 +1,52 @@
#!/usr/bin/env bash
[ "$(whoami)" != "root" ] && echo "Error: installation only works as root!" >&2 && exit 1
(( EUID != 0 )) && echo "Error: installation only works as root!" >&2 && exit 1
API_ENDPOINT="https://git.ccc-rheintal.ch/api/v1/repos/spacefreak/pbc"
TMPDIR=''
function cleanup_tmpdir() {
[ -n "$TMPDIR" -a -d "$TMPDIR" ] && rm -r "$TMPDIR"
WORK_DIR=''
function cleanup_work_dir() {
[ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR"
trap - EXIT
}
function download_files() {
local path rc
local query_params=''
[ -n "$GIT_TAG" ] && query_params="?ref=$GIT_TAG"
[ -n "$PBC_GIT_TAG" ] && query_params="?ref=$PBC_GIT_TAG"
while [ -n "$1" ]; do
while (( $# > 0 )); do
path=$1
shift
curl -s --output-dir "$TMPDIR/" --remote-name "$API_ENDPOINT/raw/$path$query_params"
curl -fsSL --output-dir "$WORK_DIR/" --remote-name "$API_ENDPOINT/raw/$path$query_params"
rc=$?
if (( $rc != 0 )); then
if (( rc != 0 )); then
return $rc
fi
done
}
GIT_TAG=$(curl -s "$API_ENDPOINT/tags" | jq -r '.[] | .name' | sort --version-sort | tail -n 1)
if [ -n "$GIT_TAG" ]; then
echo "Info: installing pbc version $GIT_TAG"
[ -z "$PBC_GIT_TAG" ] && PBC_GIT_TAG=$(curl -fsSL "$API_ENDPOINT/tags" | grep -o '{"name":"[^"]*"' | sed 's/^{"name":"//;s/"$//' | grep -E '^v[0-9]+' | sort --version-sort | tail -n 1)
if [ -n "$PBC_GIT_TAG" ]; then
echo "Info: installing pbc version $PBC_GIT_TAG"
else
echo "Warning: no Git tag found, installing from repository default branch" >&2
fi
TMPDIR=$(mktemp -d)
trap cleanup_tmpdir EXIT
WORK_DIR=$(mktemp -d)
trap cleanup_work_dir EXIT
! download_files config.example pbc && echo "Error downloading files!" >&2 && exit 10
chmod +x "$TMPDIR/pbc"
chmod o= "$TMPDIR/config.example"
chmod +x "$WORK_DIR/pbc"
chmod o= "$WORK_DIR/config.example"
"$TMPDIR/pbc" install
"$WORK_DIR/pbc" install
rc=$?
if (( $rc != 0 )); then
if (( rc != 0 )); then
echo "Error installing pbc requirements!" >&2
exit $rc
fi
! mv "$TMPDIR/pbc" /usr/local/bin/pbc && echo 'Error moving file!' >&2 && exit 20
! install -m 0755 -o root -g root "$WORK_DIR/pbc" /usr/local/bin/pbc && echo 'Error installing file!' >&2 && exit 20
echo "Success!"
+151 -138
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -o pipefail
function usage() {
full=${1:-false}
local full=${1:-false}
cat <<EOF
Usage: $SCRIPT [-h] [-H] [-c config] command [OPTIONS]
@@ -34,53 +36,34 @@ EOF
function full_usage() {
usage true
[ ! -e "$PBC" ] && echo "Error: $PBC is not executable!" >&2 && return 254
[ ! -x "$PBC" ] && echo "Error: $PBC is not executable!" >&2 && return 254
echo "Commands of proxmox-backup-client:"
echo
"$PBC" help | tail -n +3
echo
return $?
}
function pbc-version() {
local vers_info=$("$PBC" version)
local rc=$?
(( $rc != 0 )) && echo "Error running $PBC version ... exited with rc=$rc" >&2 && return $rc
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
(( 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-backup() { "$PBC" backup "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "${BACKUP[@]}" "$@"; }
function pbc-list() {
"$PBC" list --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-list() { "$PBC" list "${PBC_ARGS[@]}" "$@"; }
function pbc-snapshot-list() {
"$PBC" snapshot list --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-snapshot-list() { "$PBC" snapshot list "${PBC_ARGS[@]}" "$@"; }
function pbc-files() {
"$PBC" files --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-files() { "$PBC" files "${PBC_ARGS[@]}" "$@"; }
function pbc-catalog-shell() {
"$PBC" catalog shell --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-catalog-shell() { "$PBC" catalog shell "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "$@"; }
function pbc-mount() {
"$PBC" mount --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-mount() { "$PBC" mount "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "$@"; }
function get_group_names() {
pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"'
@@ -97,27 +80,26 @@ function get_snapshot_files() {
function apt_get_install() {
local package=$1
echo "Installing $package using package manager ..."
apt-get install "$package" >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$package" >/dev/null
local rc=$?
(( $rc != 0 )) && echo "Error installing package!" >&2
(( rc != 0 )) && echo "Error installing package!" >&2
return $rc
}
TMPDIR=''
function cleanup_tmpdir() {
[ -n "$TMPDIR" -a -d "$TMPDIR" ] && rm -r "$TMPDIR"
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
local version=$("$PBC" version | head -n 1)
(( $? != 0 )) && echo "Error running $PBC version: exited with rc=$?" >&2 && return 10
version=$(pbc-version) || return 10
echo "Info: skip installation of proxmox-backup-client, already installed ($version)"
return 0
fi
@@ -127,42 +109,44 @@ function install_proxmox_backup_client() {
return 0
fi
if apt-get search proxmox-backup-client &>/dev/null; then
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 -s "$PBC_DOWNLOAD_URL/" | grep proxmox-backup-client-static_ | tail -n 1 | sed 's#.*href="##g;s#".*##g;')
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
WORK_DIR=$(mktemp -d)
trap cleanup_work_dir EXIT
echo "Info: downloading $PBC_DOWNLOAD_URL/$pkgfile ..."
! curl -s -o "$TMPDIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40
! curl -fsSL -o "$WORK_DIR/$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
! dpkg-deb -x "$WORK_DIR/$pkgfile" "$WORK_DIR/" && echo "Error extracting file!" >&2 && return 50
echo "Info: copying proxmox-backup-client static binary to $PBC ..."
! cp "$TMPDIR/usr/bin/proxmox-backup-client" "$PBC" && echo "Error copying binary!" >&2 && return 60
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
local version=$(pbc-version) || return $?
version=$(pbc-version) || return $?
$update && action='updated' || action='installed'
echo "Info: successfully $action proxmox-backup-$version"
cleanup_tmpdir
cleanup_work_dir
}
function arr_contains() {
local search=$1
shift
[ -z "$search" ] && return 1
while [ -n "$1" ]; do
while (( $# > 0 )); do
[ "$1" == "$search" ] && return 0
shift
done
@@ -178,8 +162,12 @@ function may_select() {
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=($(printf "%s\n" "$@" | sort))
local sorted
readarray -t sorted < <(printf '%s\n' "$@" | sort)
select value in "${sorted[@]}"; do
[ -n "$value" ] && break
done
@@ -188,24 +176,54 @@ function may_select() {
return 1
fi
[ -z "$value" ] && return 1
echo "$value"
return 0
}
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 "$SCRIPT_PATH")
SCRIPT_DIR=$(dirname "$(realpath "$0")")
SCRIPT=$(basename "$SCRIPT_PATH")
PBC=$(which proxmox-backup-client)
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 [ -n "$1" ]; do
while (( $# > 0 )); do
opt=$1
shift
if [ -z "$action" ]; then
@@ -213,20 +231,19 @@ while [ -n "$1" ]; do
-c|--config)
cfgfile=$1
shift
[ -z "$cfgfile" ] && usage && exit 255
[ -z "$cfgfile" ] && echo "Error: missing argument for $opt" >&2 && usage >&2 && exit 255
;;
-h|--help)
usage
exit 0
;;
-H|--full-help)
full_usage
exit 0
full_help=true
;;
-*)
echo "Error: invalid option: $opt" >&2
echo
usage
echo >&2
usage >&2
exit 255
;;
*)
@@ -236,18 +253,18 @@ while [ -n "$1" ]; do
elif [ "$action" == "install" ]; then
case "$opt" in
-u|--update)
args+=('true')
update_pbc=true
;;
-*)
echo "Error: invalid option: $opt" >&2
echo
usage
echo >&2
usage >&2
exit 255
;;
*)
echo "Error: invalid argument: $opt" >&2
echo
usage
echo >&2
usage >&2
exit 255
;;
esac
@@ -256,104 +273,106 @@ while [ -n "$1" ]; do
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-get dpkg dpkg-deb curl; do
! which -s "$exe" && echo "Error: $exe executable not found!" >&2 && exit 200
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
[ ! -e "$PBC" ] && echo "Error: $PBC: not executable" >&2 && exit 200
! which -s jq && echo "Error: jq executable not found!" >&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
export PBS_REPOSITORY="$PBS_USER@${PBS_SERVER:-8007}:$PBS_DATASTORE"
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 || exit $?
pbc-backup "${args[@]}" || exit $?
;;
backup-cron)
output=$(pbc-backup 2>&1)
output=$(pbc-backup "${args[@]}" 2>&1)
rc=$?
(( $rc != 0 )) && echo "$output"
(( 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
select_group "${args[0]}"
case $? in
0) ;;
2) echo "no snapshot groups found"; exit 0 ;;
*) exit 1 ;;
esac
echo
pbc-snapshot-list "$group" "${args[@]}" || exit $?
pbc-snapshot-list "$GROUP" "${args[@]:1}" || 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
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 $?
pbc-catalog-shell "$SNAPSHOT" "$ARCHIVE" || exit $?
;;
mount)
readarray -t snapshots < <(get_snapshot_names)
(( ${#snapshots[@]} == 0 )) && echo "no snapshots found" && exit 0
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
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}")
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
! 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=''
@@ -364,33 +383,27 @@ case "$action" in
fi
echo
pbc-mount "$snapshot" "$archive" "$target" || exit $?
pbc-mount "$SNAPSHOT" "$ARCHIVE" "$target" || exit $?
;;
install)
update=${args[0]:-false}
(( EUID != 0 )) && echo "Error: installation only works as root!" >&2 && exit 1
[ "$(whoami)" != "root" ] && echo "Error: installation only works as root!" >&2 && exit 1
install_proxmox_backup_client "$update" || exit $?
install_proxmox_backup_client "$update_pbc" || 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"
$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
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 "Copy example config to $ETC_DIR/config.example ..."
! cp "$SCRIPT_DIR/config.example" "$ETC_DIR/config.example" && echo "Error copying file!" >&2 && exit 4
fi
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
echo "Info: successfully installed pbc dependencies"