Linux – Automatic backup to a hotplugged USB device

Summary

I was looking for a way to automatically kick off a backup script whenever I plugged into a specific USB device. After playing around it on and off for a number of months, I finally found a good way to achieve this using udev and systemd. I wrote and tested this configuration on Fedora 28.

At a high level, it can be done using 4 basic steps. First identify the device that you want to use when it is plugged in using some sort of unique attribute, in my case that would be the device serial number. Secondly once udev identifies that said device is plugged in, it will call systemd to mount the device. Thirdly systemd mounts the device then calls the backup script. Fourth and finally the backup is complete, the device will be unmounted.

Steps

Figure out the device serial number

I am using a Western Digital Elements device to automate backups. To determine the serial number (which should be unique), first I plug in the device.

List all connected USB devices. Then find the corresponding Bus and Device number for the USB device you will be using to auto backup.

$ lsusb
Bus 002 Device 028: ID 1058:25fe Western Digital Technologies, Inc.
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 004: ID 0a5c:5832 Broadcom Corp.
Bus 001 Device 020: ID 12d1:14db Huawei Technologies Co., Ltd. E353/E3131
Bus 001 Device 002: ID 0bda:5686 Realtek Semiconductor Corp.

In my case, I am interested in the first line of output, my WD device is on Bus 002 and is Device 028. Get more information about the device you want to backup.


$ lsusb -s 002:028 -v
Bus 002 Device 028: ID 1058:25fe Western Digital Technologies, Inc.
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 3.10
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 9
idVendor 0x1058 Western Digital Technologies, Inc.
idProduct 0x25fe
bcdDevice 10.21
iManufacturer 1 Western Digital
iProduct 2 Elements SE 25FE
iSerial 3 575839314138383154484655
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 44
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0x80
(Bus Powered)
MaxPower 896mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 8 Mass Storage
bInterfaceSubClass 6 SCSI
bInterfaceProtocol 80 Bulk-Only
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0400 1x 1024 bytes
bInterval 0
bMaxBurst 15
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x02 EP 2 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0400 1x 1024 bytes
bInterval 0
bMaxBurst 15
Binary Object Store Descriptor:
bLength 5
bDescriptorType 15
wTotalLength 22
bNumDeviceCaps 2
USB 2.0 Extension Device Capability:
bLength 7
bDescriptorType 16
bDevCapabilityType 2
bmAttributes 0x00000f0e
BESL Link Power Management (LPM) Supported
BESL value 3840 us
SuperSpeed USB Device Capability:
bLength 10
bDescriptorType 16
bDevCapabilityType 3
bmAttributes 0x00
wSpeedsSupported 0x000e
Device can operate at Full Speed (12Mbps)
Device can operate at High Speed (480Mbps)
Device can operate at SuperSpeed (5Gbps)
bFunctionalitySupport 1
Lowest fully-functional device speed is Full Speed (12Mbps)
bU1DevExitLat 10 micro seconds
bU2DevExitLat 32 micro seconds
can't get debug descriptor: Resource temporarily unavailable
Device Status: 0x000c
(Bus Powered)
U1 Enabled
U2 Enabled

Wow that is a lot of output, the information I am interested in is contained in the line.


iSerial 3 575839314138383154484655

The serial number if the 3rd column, which in this case is 575839314138383154484655. For future reference, pipe the lsusb command through grep and filter for Serial.


$ lsusb -s 002:028 -v |grep Serial

https://linux.die.net/man/8/lsusb

Once I have the serial number of the device, the next step involves writing a udev rule.

Udev rules

Create /dev/udev/rules.d/10-backup_usb.rules

ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[b-z]1", ATTRS{serial}=="575839314138383154484655", ATTRS{manufacturer}=="Western Digital", RUN+="/usr/bin/systemctl --no-block start backup@$name.service"

What this is doing is listening on the block device subsystem, for a device that is mapped to sdX1, with attibutes of Manufacturer and Serial being tested (both of which can be obtained from the the commands above). Once a device that matches is detected the script /usr/bin/systemctl –no-block start backup@$name.service is run. $name is a special udev variable which basically will pass the device name in the format of sdXX to the script.

https://linux.die.net/man/7/udev

We have not created the script as yet, so don’t fret, that will be next.

Systemd Unit Script

Create the file /etc/systemd/system/backup@.service


[Unit]
Description=Backup to USB Flash Disk
BindsTo=dev-%i.device
[Service]
Type=simple
ExecStart=/usr/local/bin/backupUSB.sh %I incbackup

This is a basic service unit file, we use systemd because if you attempt to run a mount from udev, systemd will step in and unmount the device. If you let systemd call the script and wait for it to end, the device will not be unmounted until the backup is complete. The unit file just takes the device passed in, then passes it onto the backup script (%I).

http://man7.org/linux/man-pages/man5/systemd.service.5.html

Last of all the backup script.

Backup Script

I have written the backup script to allow for full or incremental backups. The incremental backups creates a new backup directory, which is a timestamp, by hardlinking to the previous backup. After the new backup directory is created an rsync is run and only new files are created. You should not always rely on the incremental backups, once a week a full backup should be run then each day between full backup days, run an incremental backup. Here is my script…

#!/bin/bash
#
#
# test argument passed
#
if [ $# -ne 2 ]
then
echo "Error, invalid arguments"
echo "Usage: ${0} DEV_NAME BACKUPTYPE"
echo "Example: ${0} sdd1 incbackup|fullbackup"
exit 1
fi
#
# setup option
#
OPTION=${2}
#
# set DEV var using passed in arg
#
DEV="/dev/${1}"
#
# create logfile var
#
LOGFILE=/var/log/backup.log
#
# check if dev exists
#
if [ ! -e "$DEV" ]
then
echo "Error, device ${DEV} doesn't exist" >> ${LOGFILE}
exit 1
fi
#echo "${DEV}" >> ${LOGFILE}
#
# mount device
#
mount ${DEV} /mnt/usb &>> ${LOGFILE}
#
# set TIMESTAMP VAR
#
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
MOUNT_PATH=/mnt/usb
#
# define backup function
#
incbackup() {
echo "backup started on $(date)" >> ${LOGFILE}
# find last backup
LASTBACKUP=$(ls ${MOUNT_PATH}/backups/ | sort -nr |head -1)
# create current date/time dir var
BACKUPDIR=${MOUNT_PATH}/backups/${TIMESTAMP}
# check if last backup exists
if [ ! -z ${LASTBACKUP} ]
then
# if it exists hard link it
cp -al ${MOUNT_PATH}/backups/${LASTBACKUP} $BACKUPDIR
fi
# sync files
rsync -aP /home $BACKUPDIR &>> ${LOGFILE}
}
#
# full backup
#
fullbackup() {
echo "Full backup started on $(date)" >> ${LOGFILE}
# setup backdir var
BACKUPDIR=${MOUNT_PATH}/backups/${TIMESTAMP}
# sync files
rsync -aP /home $BACKUPDIR &>> ${LOGFILE}
}
#
# check if mounted, start backup if true
#
if grep -qs '/mnt/usb' /proc/mounts
then
echo "USB Mounted, backup starting" >> ${LOGFILE}
# check valid backup option
if [ $OPTION == "incbackup" ]
then
incbackup
elif [ $OPTION == "fullbackup" ]
then
fullbackup
else
echo "Error, invalid option incbackup or fullbackup" >> ${LOGFILE}
exit 1
fi
else
echo "Not mounted, backup exiting" >> ${LOGFILE}
exit 1
fi

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s