This project is not associated with Proxmox Server Solutions GmbH nor the official Proxmox Virtual Environment (PVE) project. Please report any bugs or suggestions to us, do NOT use the official Proxmox support channels.
pve_backup_usb.sh
is a script for smaller environments without dedicated Proxmox Backup Server. It helps you to copy PVE dumps (created using the built-in backup functionality stored on a PVE Host) to external, encrypted USB drives for offsite disaster backups.
Features:
- Easy selection of PVE dumps to copy (including the limitation of „only the N newest ones of machine X“).
- Can search multiple backup source directories for PVE dumps.
- Automatic search of USB drive, mount including decryption/
cryptsetup open
. - Extensive output, syslog, and optional mail notification (using the system's
mail
; please make sure a proper mail relay is configured). - Robust error handling and checks (e.g. available space on target, prevent parallel execution and so on).
Simply store pve_backup_usb.sh
where you like and make it executable. /usr/local/bin/pve_backup_usb.sh
is usually a good place.
You can download the latest release via command line as follows:
# install dependencies (all except of hdparm and jq should be already
# installed on a common PVE host; jq is not needed by the script for
# some of the code snippets of the README)
apt-get install coreutils hdparm jq lsof util-linux
# get version number of the latest release
version="$(curl -s -L https://api.github.com/repos/foundata/proxmox-pve-backup-usb/releases/latest | jq -r '.tag_name' | sed -e 's/^v//g')"
printf '%s\n' "${version}"
# download
curl -L "https://raw.githubusercontent.com/foundata/proxmox-pve-backup-usb/v${version}/pve_backup_usb.sh" > "/usr/local/bin/pve_backup_usb.sh"
# check the content (you've just downloaded a file from the internet :-D)
cat "/usr/local/bin/pve_backup_usb.sh"
# take care about owner and permissions
chown "root:root" "/usr/local/bin/pve_backup_usb.sh"
chmod 0755 "/usr/local/bin/pve_backup_usb.sh"
# print the script's version number
/usr/local/bin/pve_backup_usb.sh -v
Updating is as simple as overwriting the old script file. Just follow the installation instructions to get the latest release. This should be a low-risk operation as there were no backwards-compatibility-breaking releases yet (for example, all existing releases handle the target storage the same way).
Example call:
pve_backup_usb.sh \
-b "10:1,22:4,333" -s "/mnt/backup1/dump:/mnt/backup2/dump" \
-c -e "[email protected]" -g "[email protected],[email protected]"
Explanation:
-b "10:1,22:4,333"
: Handling backups of- machine with PVE ID
10
: only the last backup (:1
) (if there are more, they will be ignored) - machine with PVE ID
22
: only the last four backups (:4
) (if there are more, they will be ignored) - machine with PVE ID
333
: all existing backups (no:X
behind the PVE ID)
- machine with PVE ID
-s
: Search in/mnt/backup1/dump
and/mnt/backup2/dump
for PVE backup dumps to copy. Both paths have to exist. Separator for multiple sources is:
.-c
: Create a checksums file and verify the backup copies afterwards.-e
: email the backup report to[email protected]
.-g
: email the backup report (as CC) to[email protected]
and[email protected]
.
The script deletes the old backup content on the target device (after copying the new data if there is enough space to copy the new files and keep the old ones during copy operation or upfront if there is not enough space to keep both). To keep multiple revisions of the last N
PVE dumps, you can use multiple external drives and rotate them as you wish (=disconnect the old drive, change and connect the new drive).
By default, the script searches the following locations for a partition to use as the backup target for decryption and mounting:
- The first partition labeled
pve_backup_usb
listed under/dev/disk/by-label/
. - The first partition on the first USB disk found via
/dev/disk/by-path/
.
No need to worry: existing partitions or drives not intended for backups will not be destroyed, as decryption will simply fail, and the script will stop. If this automated behavior does not match your environment, you can provide a custom list of partition labels or UUIDs to search before the default locations are checked (cf. -d
parameter).
Mandatory:
-b
: Defines which PVE dumps will be copied. The format is a CSV list ofPveID:maxCount
value tuples where:maxCount
is optional. All backups forPveId
will be copied if:maxCount
is not given. Example: The value123:2,456:4,789
will copy- the last two backups of machine
123
- the last four backups of machine
456
- all backups of machine
789
- the last two backups of machine
-s
: List of one or more directories to search for PVE dumps, without trailing slash, separated by:
. Examples:/path/to/pve/dumps
or/pve1/dumps:/pve2/dumps
.
Important, but optional
-c
: Flag to enable checksum creation and verification of the copies (recommended for safety but propably doubles the time needed for completing the task).-e
: Email address to send notifications to. Format:[email protected]
. Has to be set for sending mails. This script is using the system'smail
command, so please make sure a proper relay is configured.-g
: Email address(es) to send notifications to (CC). Format:[email protected]
. Separate multiple addresses via comma (CSV list).
Miscellaneous, optional
-d
: List of partition labels or UUIDs to search before the default locations are checked for the backup target partion. By default, the script searches the following locations for a partition to use as the backup target for decryption and mounting:
1. The first partition labeledpve_backup_usb
listed under/dev/disk/by-label/
.
2. The first partition on the first USB disk found via/dev/disk/by-path/
.
If this automated behavior does not match your environment, you can provide a custom list of partition labels or UUIDs to search before using this parameter. Separate multiple targets with commas (CSV format). Any given UUID will be searched under/dev/disk/by-uuid/
(you might useblkid /dev/sdX1
to determine a UUID), while any other string matching the pattern^[0-9a-zA-Z_ \-]{1,16}$
will be searched under/dev/disk/by-label/
. The search will be performed in the exact order the items appear in the list.
No need to worry: existing partitions or drives not intended for backups will not be destroyed, as decryption will simply fail, and the script will stop.-h
: Flag to print help.-j
: Flag to enable the continuation of the backup process in case of an error during the copying and/or verification of files. If this flag is not set, the script will immediately halt the entire backup process and display an error if any file to be backed up cannot be copied (or verified, cf.-c
). If the flag is set, the backup will continue with the remaining files to be backed up (if any). Activating this feature requires careful review of the backup logs and emails/messages, as a quick inspection of the backup media might incorrectly suggest that all PVE sources were successfully backed up, even though some files may be missing or damaged. However, this allows for the possibility that at least some partially useful backup data will be available.-k
: Path to a keyfile containing a passphrase to unlock the target device. Defaults to/etc/credentials/luks/pve_backup_usb
. There must be no other chars beside the passphrase, including no trailing new line orEOF
. You might useperl -pi -e 'chomp if eof' /etc/credentials/luks/pve_backup_usb
to get rid of an invisible, unwantedEOF
.-l
: Name used for handling LUKS via/dev/mapper/
and creating a mountpoint subdirectory at/media/
. Defaults topve_backup_usb
. 16 alphanumeric chars at max.-q
: Flag to enable quiet mode. Emails will be sent only onerror
orwarning
then (but not oninfo
orsuccess
).-u
: Username of the account used to run the backups. Defaults toroot
. The script checks if the correct user is calling it and permissions of e.g. the keyfile are fitting or are too permissive. The user also needs permissions to mount devices. Running the script asroot
is propably a good choice for most environments.-v
: Print the script's version number, then exit.
The easiest way to get a rotation in place and use this script is a cronjob. For example, place something like the following via crontab -e
in the crontab of root
:
0 19 * * Sat /usr/local/bin/pve_backup_usb.sh -b "10:1,22:4,333" -s "/mnt/backup1/dump:/mnt/backup2/dump" -c -e "[email protected]" -g "[email protected],[email protected]" > /dev/null 2>&1
Explanation:
0 19 * * Sat /usr/local/bin/pve_backup_usb.sh
: Run on every Saturday at 19:00 o'clock.-b "10:1,22:4,333"
: Handling backups of- machine with PVE ID
10
: Only the last backup (if there are more, they will be ignored) - machine with PVE ID
22
: Only the last four backups (if there are more, they will be ignored) - machine with PVE ID
333
: All existing backups
- machine with PVE ID
- Search in
/mnt/backup1/dump
and/mnt/backup2/dump
for backups. -c
: Create a checksums file and verifies the backup copies afterwards.-e
: Email the backup report to[email protected]
.-g
: Email the backup report (as CC) to[email protected]
and[email protected]
.
An external USB drive has to be prepared before using it as storage target for PVE dump copies:
- Add a GPT partitioning table and one primary partition for the whole disk.
- Encrypt it with LUKS.
- Format the partition with a filesystem (e.g. EXT4 or XSF).
- Place a keyfile with the LUKS passphrase on the PVE host for automatic opening of the device for backups.
Full example of preparing a drive:
# determine your device
lsblk
lsblk -l -p
ls -l /dev/disk/by-path/*usb*
TARGETDEVICE='/dev/sdX' # adapt X to point to your USB disk
DEVICELABEL='pve_backup_usb' # 16 chars max
MAPPERNAME="${DEVICELABEL}"
# get some infos about the drive
apt-get install hdparm
hdparm -I "${TARGETDEVICE}"
# make sure predefined filesystems are currently not mounted (new USB drives
# are usually shipped with a filesystem).
umount --force --recursive --all-targets "${TARGETDEVICE}"*
# Create a partition and encrypt it.
#
# Please use a long passphrase (at least 20 characters) for security and
# store it in your password management. You do not have to type it anywhere,
# the script will grab it from a keyfile later.
#
# You might want to look at a current system with disk encryption which
# crypto default settings are en-vouge:
# dmsetup table ${deviceNameBelow/dev/mapper}
# cryptsetup luksDump ${device}
# As of 2023 "aes-xts-plain64" should be a good choice.
apt-get install parted cryptsetup
parted "${TARGETDEVICE}" mktable GPT
parted "${TARGETDEVICE}" mkpart primary 0% 100%
cryptsetup luksFormat --cipher aes-xts-plain64 --verify-passphrase "${TARGETDEVICE}1"
cryptsetup config "${TARGETDEVICE}1" --label "${DEVICELABEL}"
# optional: add an additional fallback key. Please use a long passphrase (at least
# 20 chars) for security and store it in your password management.
cryptsetup luksDump "${TARGETDEVICE}1"
cryptsetup luksAddKey "${TARGETDEVICE}1"
cryptsetup luksDump "${TARGETDEVICE}1"
# open and list, access possible via /dev/mappper/${MAPPERNAME} afterwards
cryptsetup open "${TARGETDEVICE}1" "${MAPPERNAME}"
dmsetup ls --target "crypt"
# create EXT4 system, prevent lazy init to get full performance at first use
mkfs.ext4 \
-E lazy_itable_init=0,lazy_journal_init=0 \
-L "${DEVICELABEL}" "/dev/mapper/${MAPPERNAME}" && sync
# test mount
tmpdirmnt="$(mktemp -d)"
mount "/dev/mapper/${MAPPERNAME}" "${tmpdirmnt}"
ls -la "${tmpdirmnt}"
# close and cleanup (the drive is ready for usage afterwards and/or
# can be disconnected now)
umount "/dev/mapper/${MAPPERNAME}" && sync
cryptsetup luksClose "${MAPPERNAME}" && sync
Your USB disk is now encrypted. Therefore it is secure to store copies of PVE backups dumps on it. So you can use the USB drive for offsite backup without getting in trouble when a disk gets lost or stolen.
Now place a keyfile containing the passphrase to make it possible to automatically unlock the disk(s). By default, the script searches at /etc/credentials/luks/pve_backup_usb
for it but you can specify another keyfile when calling pve_backup_usb.sh
by using the -k
parameter. Example:
# create the file
mkdir -p /etc/credentials/luks/
chmod 0770 /etc/credentials/
chmod 0770 /etc/credentials/luks/
touch /etc/credentials/luks/pve_backup_usb
chmod 0660 /etc/credentials/luks/pve_backup_usb
# now put the passphase (without linebreaks before or after) into
# /etc/credentials/luks/pve_backup_usb using your favorite editor
# nothing(!) is allowed at the end of the file, also no EOF like
# e.g. nano is adding it. Make sure there is none:
perl -pi -e 'chomp if eof' /etc/credentials/luks/pve_backup_usb
# test
TARGETDEVICE="/dev/$(ls -l /dev/disk/by-path/*usb*part1 | cut -f 7 -d "/" | head -n 1)"
cryptsetup open --key-file "/etc/credentials/luks/pve_backup_usb" "${TARGETDEVICE}" "pve_backup_usb"
dmsetup ls --target "crypt"
ls -l "/dev/mapper/pve_backup_usb"
cryptsetup luksClose "pve_backup_usb"
dmsetup ls --target "crypt"
The detailled logfile of a script run will be copied beside the mirrored backups and is named after the script's filename plus .log
extension. By default, this is /media/pve_backup_usb/dump/pve_backup_usb.sh.log
.
The logfile is handled as temporary file during the script execution and placed at ${TMPDIR}/pve_backup_usb_XXXXXXXXXXXXXX
where XXXXXXXXXXXXXX
is random and $TMPDIR
is either defined by your environment or set to /tmp
. If you want to look at the file during execution without blocking the file, the following command can do so (the :
at the beginning is no error):
: "${TMPDIR:=/tmp}"; cat "${TMPDIR}/pve_backup_usb_"* | less
If you are using email notifications (cf. -e
, -f
and -g
parameters), the complete logfile content will be added to the email message automatically.
The script logs with its own filename as SYSLOG_IDENTIFIER
. So by default, you can filter with pve_backup_usb.sh
as follows:
# all logs
journalctl -t "pve_backup_usb.sh"
# all logs, in reverse order
journalctl -t "pve_backup_usb.sh" -r
# all logs, in reverse order, without pager (so no scrolling, all written directly to STDOUT)
journalctl -t "pve_backup_usb.sh" -r --no-pager
# only errors
journalctl -t "pve_backup_usb.sh" -r -p 0..3
# only non-error messages
journalctl -t "pve_backup_usb.sh" -r -p 4..7
Other examples:
# search for messages including related items (produced by other units, e.g. mount
# messages, cronjob start, ...) in reverse order
journalctl -r -g "pve_backup_usb"
# JSON, pretty print
journalctl -o "json" --no-pager -t "pve_backup_usb.sh" -r | jq -C . | less
journalctl -o "json" --no-pager -g "pve_backup_usb" -r | jq -C . | less
Running the command
/usr/local/bin/pve_backup_usb.sh -c -b "120:1" -s "/mnt/localbackup01/pve/dump"
to mirror the lastest dump of the VM with PVE ID 120
from /mnt/localbackup01/pve/dump
to the encrypted USB device (a cheap 5TB WD Elements USB-HDD) gave the following logfile:
#### pve_backup_usb.sh ####
Current time: Wed Aug 30 05:58:34 PM UTC 2023.
CSV list of 'PveMachineID[:MaxBackupCount]' entries (defines what to copy): '120:1'
Sync, unmount and close of LUKS device (upfront safeguard against stale or previously interrupted execs).
Creating mountpoint at '/media/pve_backup_usb'
Going to unlock '/dev/sdc1', using using keyfile '/etc/credentials/luks/pve_backup_usb'
Successfully unlocked '/dev/sdc1', should be available at '/dev/mapper/pve_backup_usb' now.
Current time: Wed Aug 30 05:58:37 PM UTC 2023.
Elapsed time: 00h:00m:03s.
#### Info about physical disk (mounted at /media/pve_backup_usb) ####
Model Number: WDC WD50NDZW-11MR8S1
Serial Number: WD-<censored>
#### Checking for existing backups to copy for PVE ID 120 ####
Found backup 'vzdump-qemu-120-2023_08_29-21_00_03' in '/mnt/localbackup01/pve/dump'
Found backup 'vzdump-qemu-120-2023_08_28-21_00_02' in '/mnt/localbackup01/pve/dump'
Found backup 'vzdump-qemu-120-2023_08_24-21_00_05' in '/mnt/localbackup01/pve/dump'
Added backup 'vzdump-qemu-120-2023_08_29-21_00_03' to the list for processing.
Skipped backup 'vzdump-qemu-120-2023_08_28-21_00_02' as max backup count 1 for ID '120' was reached.
Skipped backup 'vzdump-qemu-120-2023_08_24-21_00_05' as max backup count 1 for ID '120' was reached.
#### Miscellaneous preparation ####
Copying the backup files will need 20.89GiB of space on the target device.
The target device mounted at '/media/pve_backup_usb' has a size of about 4.27TiB.
There seems to be older backup data on the target device, moving it from '/media/pve_backup_usb/dump' to '/media/pve_backup_usb/dump_old'
Successfully moved '/media/pve_backup_usb/dump' to '/media/pve_backup_usb/dump_old'.
There is about 4.27TiB of free space available on the target device.
Current time: Wed Aug 30 05:58:37 PM UTC 2023.
Elapsed time: 00h:00m:03s.
Going to process the created list of backups to copy now.
Creating copy target directory at '/media/pve_backup_usb/dump'.
#### Handling backup 'vzdump-qemu-120-2023_08_29-21_00_03' ####
Creating checksums file
cd "/mnt/localbackup01/pve/dump" && sha1sum "./vzdump-qemu-120-2023_08_29-21_00_03"* > "/media/pve_backup_usb/dump/vzdump-qemu-120-2023_08_29-21_00_03.sha1" 2>&1
Current time: Wed Aug 30 05:59:10 PM UTC 2023.
Elapsed time: 00h:00m:36s.
Starting copy of backup
cp -r -f -v "/mnt/localbackup01/pve/dump/vzdump-qemu-120-2023_08_29-21_00_03"* "/media/pve_backup_usb/dump" 2>&1
'/mnt/localbackup01/pve/dump/vzdump-qemu-120-2023_08_29-21_00_03.log' -> '/media/pve_backup_usb/dump/vzdump-qemu-120-2023_08_29-21_00_03.log'
'/mnt/localbackup01/pve/dump/vzdump-qemu-120-2023_08_29-21_00_03.vma.zst' -> '/media/pve_backup_usb/dump/vzdump-qemu-120-2023_08_29-21_00_03.vma.zst'
'/mnt/localbackup01/pve/dump/vzdump-qemu-120-2023_08_29-21_00_03.vma.zst.notes' -> '/media/pve_backup_usb/dump/vzdump-qemu-120-2023_08_29-21_00_03.vma.zst.notes'
Current time: Wed Aug 30 06:12:50 PM UTC 2023.
Elapsed time: 00h:14m:16s.
Verify checksums of file copies
cd "/media/pve_backup_usb/dump" && sha1sum -c "./vzdump-qemu-120-2023_08_29-21_00_03.sha1" 2>&1
./vzdump-qemu-120-2023_08_29-21_00_03.log: OK
./vzdump-qemu-120-2023_08_29-21_00_03.vma.zst: OK
./vzdump-qemu-120-2023_08_29-21_00_03.vma.zst.notes: OK
Verification was successful.
Current time: Wed Aug 30 06:13:19 PM UTC 2023.
Elapsed time: 00h:14m:45s.
All file operations were finished successfully.
Going to clean up the old backup data at '/media/pve_backup_usb/dump_old'.
Current time: Wed Aug 30 06:13:20 PM UTC 2023.
Elapsed time: 00h:14m:46s.
Mirroring backups to '/media/pve_backup_usb' was successful.
Syslog entry was created (priority: info)
Successfully unmounted '/media/pve_backup_usb'
Successfully deleted mountpoint '/media/pve_backup_usb'.
Successfully closed LUKS device 'pve_backup_usb'
The script should be compatible with Proxmox Virtual Environment (PVE) 7.X and newer. It was tested on:
- Proxmox VE 8: 8.3.0, 8.1.4, 8.0.4
- Proxmox VE 7: 7.4-16
See CONTRIBUTING.md
if you want to get involved.
The script's functionality is mature, so there might be little activity on the repository in the future. Don't get fooled by this, the project is under active maintenance and used daily by the maintainers.
Copyright (c) 2023, 2024 foundata GmbH (https://foundata.com)
This project is licensed under the Apache License 2.0 (SPDX-License-Identifier: Apache-2.0
), see LICENSES/Apache-2.0.txt
for the full text.
The .reuse/dep5
file provides detailed licensing and copyright information in a human- and machine-readable format. This includes parts that may be subject to different licensing or usage terms, such as third party components. The repository conforms to the REUSE specification, you can use reuse spdx
to create a SPDX software bill of materials (SBOM).
This project was created and is maintained by foundata. If you like it, you might buy them a coffee.