Compare commits

...
5 Commits
4 changed files with 85 additions and 34 deletions
+60 -17
View File
@@ -3,10 +3,10 @@
A Bash wrapper around Proxmox's `proxmox-backup-client` for Proxmox Backup Server (PBS). 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 It keeps the repository, credentials and the list of paths to back up in a single config
file, injects the configured namespace (`--ns`) into every command that needs it, and lets file, injects the configured namespace (`--ns`) and encryption key (`--keyfile`) into every
you pick a snapshot group, snapshot and archive from an interactive menu instead of typing command that needs them, and lets you pick a snapshot group, snapshot and archive from an
them out. Any command it does not implement itself is passed straight through to interactive menu instead of typing them out. Any command it does not implement itself is
`proxmox-backup-client`. passed straight through to `proxmox-backup-client`.
## Requirements ## Requirements
@@ -23,6 +23,17 @@ curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.
This installs the newest tagged version to `/usr/local/bin/pbc`, installs the This installs the newest tagged version to `/usr/local/bin/pbc`, installs the
dependencies, and puts an example config at `/etc/pbc/config.example`. 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 ### Updating
Run the same command again to update `pbc` itself: Run the same command again to update `pbc` itself:
@@ -32,7 +43,7 @@ curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.
``` ```
It fetches the newest tag and overwrites `/usr/local/bin/pbc`. Your `/etc/pbc/config` is It fetches the newest tag and overwrites `/usr/local/bin/pbc`. Your `/etc/pbc/config` is
left untouched. 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: To update only the `proxmox-backup-client` binary:
@@ -50,24 +61,15 @@ sudo chmod 640 /etc/pbc/config
sudo editor /etc/pbc/config sudo editor /etc/pbc/config
``` ```
```bash
PBS_SERVER='pbs.domain.tld:8007'
PBS_USER='backup@pam:token-name'
PBS_PASSWORD='XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
PBS_DATASTORE='backup'
PBS_NAMESPACE='MyBackups'
BACKUP=('root.pxar:/' 'data.pxar:/mnt/data')
```
| Key | Description | | Key | Description |
|---|---| |---|---|
| `PBS_SERVER` | PBS host and port | | `PBS_SERVER` | PBS host and port |
| `PBS_USER` | User and API token name, `user@realm:token-name` | | `PBS_USER` | User and API token name, `user@realm:token-name` |
| `PBS_PASSWORD` | The API token secret | | `PBS_PASSWORD` | The API token secret |
| `PBS_DATASTORE` | Datastore to back up to | | `PBS_DATASTORE` | Datastore to back up to |
| `PBS_NAMESPACE` | Namespace inside the datastore | | `PBS_NAMESPACE` | Optional, namespace inside the datastore |
| `BACKUP` | Array of `archive-name.pxar:/path` entries to back up | | `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` | 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 `pbc` never creates the config for you — copy the example and edit it yourself. The file
@@ -93,6 +95,45 @@ Use a different config with `-c`:
pbc -c ./myconfig list 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 ## Usage
Run `pbc --help` for the synopsis and the list of options, or `pbc -H` to additionally Run `pbc --help` for the synopsis and the list of options, or `pbc -H` to additionally
@@ -150,7 +191,8 @@ 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 The target directory is not created for you — create it beforehand and make sure it is
writable for the user that runs `pbc`. Unmount when done: writable for the user that runs `pbc`. Encrypted archives are decrypted transparently when
`ENCRYPTION_KEYFILE` is configured. Unmount when done:
```bash ```bash
umount /mnt/restore umount /mnt/restore
@@ -159,6 +201,7 @@ umount /mnt/restore
### catalog-shell ### catalog-shell
Open an interactive shell to browse an archive and restore selected files — see below. 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 ```bash
pbc catalog-shell pbc catalog-shell
+1
View File
@@ -3,5 +3,6 @@ PBS_USER='backup@pam:token-name'
PBS_PASSWORD='XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' PBS_PASSWORD='XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
PBS_DATASTORE='backup' PBS_DATASTORE='backup'
PBS_NAMESPACE='MyBackups' PBS_NAMESPACE='MyBackups'
#ENCRYPTION_KEYFILE='/etc/pbc/backup.key'
BACKUP=('root.pxar:/' 'data.pxar:/mnt/data') BACKUP=('root.pxar:/' 'data.pxar:/mnt/data')
+4 -4
View File
@@ -13,7 +13,7 @@ function cleanup_work_dir() {
function download_files() { function download_files() {
local path rc local path rc
local query_params='' local query_params=''
[ -n "$GIT_TAG" ] && query_params="?ref=$GIT_TAG" [ -n "$PBC_GIT_TAG" ] && query_params="?ref=$PBC_GIT_TAG"
while (( $# > 0 )); do while (( $# > 0 )); do
path=$1 path=$1
@@ -26,9 +26,9 @@ function download_files() {
done done
} }
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) [ -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 "$GIT_TAG" ]; then if [ -n "$PBC_GIT_TAG" ]; then
echo "Info: installing pbc version $GIT_TAG" echo "Info: installing pbc version $PBC_GIT_TAG"
else else
echo "Warning: no Git tag found, installing from repository default branch" >&2 echo "Warning: no Git tag found, installing from repository default branch" >&2
fi fi
+18 -11
View File
@@ -53,17 +53,17 @@ function pbc-version() {
return $rc return $rc
} }
function pbc-backup() { "$PBC" backup --ns "$PBS_NAMESPACE" "${BACKUP[@]}" "$@"; } function pbc-backup() { "$PBC" backup "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "${BACKUP[@]}" "$@"; }
function pbc-list() { "$PBC" list --ns "$PBS_NAMESPACE" "$@"; } function pbc-list() { "$PBC" list "${PBC_ARGS[@]}" "$@"; }
function pbc-snapshot-list() { "$PBC" snapshot list --ns "$PBS_NAMESPACE" "$@"; } function pbc-snapshot-list() { "$PBC" snapshot list "${PBC_ARGS[@]}" "$@"; }
function pbc-files() { "$PBC" files --ns "$PBS_NAMESPACE" "$@"; } function pbc-files() { "$PBC" files "${PBC_ARGS[@]}" "$@"; }
function pbc-catalog-shell() { "$PBC" catalog shell --ns "$PBS_NAMESPACE" "$@"; } function pbc-catalog-shell() { "$PBC" catalog shell "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "$@"; }
function pbc-mount() { "$PBC" mount --ns "$PBS_NAMESPACE" "$@"; } function pbc-mount() { "$PBC" mount "${PBC_ARGS[@]}" "${KEY_ARGS[@]}" "$@"; }
function get_group_names() { function get_group_names() {
pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"' pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"'
@@ -86,7 +86,6 @@ function apt_get_install() {
return $rc return $rc
} }
WORK_DIR=''
function cleanup_work_dir() { function cleanup_work_dir() {
[ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR" [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR"
trap - EXIT trap - EXIT
@@ -143,7 +142,6 @@ function install_proxmox_backup_client() {
cleanup_work_dir cleanup_work_dir
} }
function arr_contains() { function arr_contains() {
local search=$1 local search=$1
shift shift
@@ -275,7 +273,6 @@ while (( $# > 0 )); do
fi fi
done done
if [ -f "$cfgfile" ]; then if [ -f "$cfgfile" ]; then
perms=$(stat -c '%a' "$cfgfile") perms=$(stat -c '%a' "$cfgfile")
(( 8#$perms & 0022 )) && echo "Error: $cfgfile is writable by group or others!" >&2 && exit 202 (( 8#$perms & 0022 )) && echo "Error: $cfgfile is writable by group or others!" >&2 && exit 202
@@ -307,8 +304,20 @@ else
[ ! -f "$PBC" ] && echo "Error: $PBC: no such file" >&2 && exit 200 [ ! -f "$PBC" ] && echo "Error: $PBC: no such file" >&2 && exit 200
[ ! -x "$PBC" ] && echo "Error: $PBC: not executable" >&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 ! 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 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_REPOSITORY="$PBS_USER@$PBS_SERVER:$PBS_DATASTORE"
export PBS_PASSWORD="$PBS_PASSWORD" export PBS_PASSWORD="$PBS_PASSWORD"
@@ -393,11 +402,9 @@ case "$action" in
fi fi
if [ -f "$SCRIPT_DIR/config.example" ]; then 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 ..." 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 ! install -m 0640 "$SCRIPT_DIR/config.example" "$ETC_DIR/config.example" && echo "Error installing file!" >&2 && exit 4
fi fi
fi
echo "Info: successfully installed pbc dependencies" echo "Info: successfully installed pbc dependencies"
;; ;;