SHA256
Compare commits
49
Commits
22965dc8ed
..
master
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
ef76bdd8dd
|
||
|
|
873601019a
|
||
|
|
0ade18796f
|
||
|
|
0f61e177f8
|
||
|
|
2fdf065017
|
||
|
|
6239fe5ca6
|
||
|
|
bc8287a28c
|
||
|
|
04d351a859
|
||
|
|
d5d4ea2430
|
||
|
|
e423b97c8a
|
||
|
|
01c299e242
|
||
|
|
56b619372d
|
||
|
|
73202f5e3b
|
||
|
|
d243ea1c89
|
||
|
|
0623c6a985
|
||
|
|
4fb949c26e
|
||
|
|
e065ab2cd7
|
||
|
|
7421c8f43e
|
||
|
|
e184d1c92c
|
||
|
|
ee86603dcd
|
||
|
|
1eee4b2d1d
|
||
|
|
7555745704
|
||
|
|
49d2e35106
|
||
|
|
9722741bfd
|
||
|
|
22abaf6431
|
||
|
|
4e24ee218c
|
||
|
|
acac5b685b
|
||
|
|
3869ca5a95
|
||
|
|
3d5757effc
|
||
|
|
6c7db396b2
|
||
|
|
eeb0c9cb47
|
||
|
|
44cc111b3d
|
||
|
|
f0610091bb
|
||
|
|
f93f647504
|
||
|
|
32e88beedc
|
||
|
|
6b2847fec7
|
||
|
|
127fb83567
|
||
|
|
bfa15561d8
|
||
|
|
22675d1be3
|
||
|
|
62f6865a06
|
||
|
|
331f75a943
|
||
|
|
d1791cfcc8
|
||
|
|
8f83861965
|
||
|
|
e613532322
|
||
|
|
d54fe81dc5
|
||
|
|
bf54eab7c7
|
||
|
|
6a8b4b86f3
|
||
|
|
37dbf61f07
|
||
|
|
c245c4fa1e
|
@@ -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).
|
||||
+2
-1
@@ -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']
|
||||
BACKUP=('root.pxar:/' 'data.pxar:/mnt/data')
|
||||
|
||||
+18
-18
@@ -1,51 +1,51 @@
|
||||
#!/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
|
||||
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 +x "$WORK_DIR/pbc"
|
||||
|
||||
"$TMPDIR/pbc" install
|
||||
"$WORK_DIR/pbc" install
|
||||
rc=$?
|
||||
if (( $? != 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 pbc to /usr/local/bin/pbc !' >&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!"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -22,8 +24,7 @@ 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 proxmox-backup-client binary and dependencies
|
||||
use --update to update the currently installed binary
|
||||
install [--update] install/update proxmox-backup-client and pdb dependencies
|
||||
|
||||
EOF
|
||||
! $full && cat <<EOF
|
||||
@@ -35,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"])"'
|
||||
@@ -98,72 +80,73 @@ 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
|
||||
echo "Skip installation of proxmox-backup-client, already installed ($version)"
|
||||
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 "Skip update of proxmox-backup-client, package is managed by the package manager"
|
||||
echo "Info: skip update of proxmox-backup-client, package is managed by the package manager"
|
||||
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 "Downloading $PBC_DOWNLOAD_URL/$pkgfile ..."
|
||||
! curl -s -o "$TMPDIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40
|
||||
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 "Extracting $pkgfile ..."
|
||||
! dpkg-deb -x "$TMPDIR/$pkgfile" "$TMPDIR/" && echo "Error extracting file!" >&2 && return 50
|
||||
echo "Info: extracting $pkgfile ..."
|
||||
! dpkg-deb -x "$WORK_DIR/$pkgfile" "$WORK_DIR/" && echo "Error extracting file!" >&2 && return 50
|
||||
|
||||
echo "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 "Successfully $action proxmox-backup-$version"
|
||||
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
|
||||
@@ -179,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
|
||||
@@ -189,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
|
||||
@@ -214,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
|
||||
;;
|
||||
*)
|
||||
@@ -237,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
|
||||
@@ -257,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=''
|
||||
@@ -365,36 +383,30 @@ 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 "Skip update of jq, package is managed by package manager" || echo "Skip installation of jq, 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 "Success!"
|
||||
echo "Info: successfully installed pbc dependencies"
|
||||
;;
|
||||
*)
|
||||
"$PBC" "$action" "${args[@]}" || exit $?
|
||||
|
||||
Reference in New Issue
Block a user