Semi-automated backups with duplicity and an external drive
(Above: A bunch of hard drives. The original can be found here.)
Since I've recently got myself a raspberry pi to act as a server, I naturally needed a way to back it up. Not seeing anything completely to my tastes, I ended up putting something together that did the job for me. For this I used an external hard drive, duplicity, sendxmpp (sudo apt install sendxmpp
), and a bit of bash.
Since it's gone rather well for me so far, I thought I'd write a blog post on how I did it. It still needs some tidying up, of course - but it works in it's current state, and perhaps it will help someone else put together their own system!
Step 1: Configuring the XMPP server
I use XMPP as my primary instant messaging server, so it's only natural that I'd want to integrate the system in with it to remind me when to plug in the external drive, and so that it can tell me when it's done and what happened. Since I use prosody as my XMPP server, I can execute the following on the server:
sudo prosodyctl adduser rasperrypi@bobsrockets.com
...and then enter a random password for the new account. From there, I set up a new private persistent multi-user chatroom for the messages to filter into, and set my client to always notify when a message is posted.
After that, it was a case of creating a new config file in a format that sendxmpp
will understand:
rasperrypi@bobsrockets.com:5222 thesecurepassword
Step 2: Finding the id of the drive partition
With the XMPP side of things configured, next I needed a way to detect if the drie was plugged in or not. Thankfully all partitions have a unique id built-in, which you can use to see if it's plugged in or not. It's easy to find, too:
sudo blkid
The above will list all available partitions and their UUID
- the unique id I mentioned. With that in hand, we can now check if it's plugged in or not with a cleverly crafted use of the readlink
command:
readlink /dev/disk/by-uuid/${partition_uuid} 1>/dev/null 2>&2;
partition_found=$?
if [[ "${partition_found}" -eq "0" ]]; then
echo "It's plugged in!";
else
echo "It's not plugged in :-(";
fi
Simple, right? readlink
has an exit code of 0 if it managed to read the symbolik link in /dev/disk/by-uuid
ok, and 1 if it didn't. The symbolic links in /deve/disk/by-uuid
are helpfuly created automatically for us :D From here, we can take it a step further to wait until the drive is plugged in:
# Wait until the drive is available
while true
do
readlink "${partition_uuid}";
if [[ "$?" -eq 0 ]]; then
break
fi
sleep 1;
done
Step 3: Mounting and unmounting the drive
Raspberry Pis don't mount drive automatically, so we'll have do that ourselves. Thankfully, it's not so tough:
# Create the fodler to mount the drive into
mkdir -p ${backup_drive_mount_point};
# Mount it in read-write mode
mount "/dev/disk/by-uuid/${partition_uuid}" "${backup_drive_mount_point}" -o rw;
# Do backup thingy here
# Sync changes to disk
sync
# Unmount the drive
umount "${backup_drive_mount_point}";
Make sure you've got the ntfs-3g
package installed if you want to back up to an NTFS volume (Raspberry Pis don't come with it by default!).
Step 4: Backup all teh things!
There are more steps involved in getting to this point than I thought there were, but if you've made it this far, than congrats! Have a virtual cookie :D 🍪
The next part is what you probably came here for: duplicity itself. I've had an interesting time getting this to work so far, actually. It's probably easier if I show you the duplicity commands I came up with first.
# Create the archive & temporary directories
mkdir -p /mnt/data_drive/.duplicity/{archives,tmp}/{os,data_drive}
# Do a new backup
PASSPHRASE=${encryption_password} duplicity --full-if-older-than 2M --archive-dir /mnt/data_drive/.duplicity/archives/os --tempdir /mnt/data_drive/.duplicity/tmp/os --exclude /proc --exclude /sys --exclude /tmp --exclude /dev --exclude /mnt --exclude /var/cache --exclude /var/tmp --exclude /var/backups / file://${backup_drive_mount_point}/duplicity-backups/os/
PASSPHRASE=${data_drive_encryption_password} duplicity --full-if-older-than 2M --archive-dir /mnt/data_drive/.duplicity/archives/data_drive --tempdir /mnt/data_drive/.duplicity/tmp/data_drive /mnt/data_drive --exclude '**.duplicity/**' file://${backup_drive_mount_point}/duplicity-backups/data_drive/
# Remove old backups
PASSPHRASE=${encryption_password} duplicity remove-older-than 6M --force --archive-dir /mnt/data_drive/.duplicity/archives/os file:///${backup_drive_mount_point}/duplicity-backups/os/
PASSPHRASE=${data_drive_encryption_password} duplicity remove-older-than 6M --force --archive-dir /mnt/data_drive/.duplicity/archives/data_drive file:///${backup_drive_mount_point}/duplicity-backups/data_drive/
Path names have been altered for privacy reasons. The first duplicity
command in the above was fairly straight forward - backup everything, except a few folders with cache files / temporary / weird stuff in them (like /proc
).
I ended up having to specify the archive and temporary directories here to be on another disk because the Raspberry Pi I'm running this on has a rather... limited capacity on it's internal micro SD card, so the default location for both isn't a good idea.
The second duplicity
call is a little more complicated. It backs up the data disk I have attached to my Raspberry Pi to the external drive I've got plugged in that we're backing up to. The awkward bit comes when you realise that the archive and temporary directories are located on this same data-disk that we're trying to back up. To this end, I eventually found (through lots of fiddling) that you can exclude a folder duplicity via the --exclude '**.duplicity/**'
syntax. I've no idea why it's different when you're not backing up the root of the filesystem, but it is (--exclude ./.duplicity/
didn't work, and neither did /mnt/data_drive/.duplicity/
).
The final two duplicity calls just clean up and remove old backups that are older than 6 months, so that the drive doesn't fill up too much :-)
Step 5: What? Where? Who?
We've almost got every piece of the puzzle, but there's still one left: letting us know what's going on! This is a piece of cake in comparison to the above:
function xmpp_notify {
echo $1 | sendxmpp --file "${xmpp_config_file}" --resource "${xmpp_resource}" --tls --chatroom "${xmpp_target_chatroom}"
}
Easy! All we have to do is point sendxmpp
at our config file we created waaay in step #1, and tell it where the chatroom is that we'd like it to post messages in. With that, we can put all the pieces of the puzzle together:
#!/usr/bin/env bash
source .backup-settings
function xmpp_notify {
echo $1 | sendxmpp --file "${xmpp_config_file}" --resource "${xmpp_resource}" --tls --chatroom "${xmpp_target_chatroom}"
}
xmpp_notify "Waiting for the backup disk to be plugged in.";
# Wait until the drive is available
while true
do
readlink "${backup_drive_dev}";
if [[ "$?" -eq 0 ]]; then
break
fi
sleep 1;
done
xmpp_notify "Backup disk detected - mounting";
mkdir -p ${backup_drive_mount_point};
mount "${backup_drive_dev}" "${backup_drive_mount_point}" -o rw
xmpp_notify "Mounting complete - performing backup";
# Create the archive & temporary directories
mkdir -p /mnt/data_drive/.duplicity/{archives,tmp}/{os,data_drive}
echo '--- Root Filesystem ---' >/tmp/backup-status.txt
# Create the archive & temporary directories
mkdir -p /mnt/data_drive/.duplicity/{archives,tmp}/{os,data_drive}
# Do a new backup
PASSPHRASE=${encryption_password} duplicity --full-if-older-than 2M --archive-dir /mnt/data_drive/.duplicity/archives/os --tempdir /mnt/data_drive/.duplicity/tmp/os --exclude /proc --exclude /sys --exclude /tmp --exclude /dev --exclude /mnt --exclude /var/cache --exclude /var/tmp --exclude /var/backups / file://${backup_drive_mount_point}/duplicity-backups/os/ 2>&1 >>/tmp/backup-status.txt
echo '--- Data Disk ---' >>/tmp/backup-status.txt
PASSPHRASE=${data_drive_encryption_password} duplicity --full-if-older-than 2M --archive-dir /mnt/data_drive/.duplicity/archives/data_drive --tempdir /mnt/data_drive/.duplicity/tmp/data_drive /mnt/data_drive --exclude '**.duplicity/**' file://${backup_drive_mount_point}/duplicity-backups/data_drive/ 2>&1 >>/tmp/backup-status.txt
xmpp_notify "Backup complete!"
cat /tmp/backup-status.txt | sendxmpp --file "${xmpp_config_file}" --resource "${xmpp_resource}" --tls --chatroom "${xmpp_target_chatroom}"
rm /tmp/backup-status.txt
xmpp_notify "Performing cleanup."
PASSPHRASE=${encryption_password} duplicity remove-older-than 6M --force --archive-dir /mnt/data_drive/.duplicity/archives/os file:///${backup_drive_mount_point}/duplicity-backups/os/
PASSPHRASE=${data_drive_encryption_password} duplicity remove-older-than 6M --force --archive-dir /mnt/data_drive/.duplicity/archives/data_drive file:///${backup_drive_mount_point}/duplicity-backups/data_drive/
sync;
umount "${backup_drive_mount_point}";
xmpp_notify "Done! Backup completed. You can now remove the backup disk."
I've tweaked a few of the pieces to get them to work better together, and created a separate .backup-settings
file to store all the settings in.
That completes my backup script! Found this useful? Got an improvement? Use a different strategy? Post a comment below!