#!/bin/bash # ####################################################################################################################### # # Either create an image file in backupdirectory with extension .dd which can be restored with dd or win32diskimager # from a tar or rsync backup created by raspiBackup # or restore the backup to another device, e.g. SD card or USB flashdrive to have a cold SD backup # # Visit http://www.linux-tips-and-tricks.de/raspiBackup to get more details about raspiBackup # # NOTE: This is sample code how to extend functionality of raspiBackup and is provided as is with no support. # ####################################################################################################################### # # Copyright (c) 2017-2024 framp at linux-tips-and-tricks dot de # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Kudos for kmbach who suggested to create this helper and who helped to improve it # ####################################################################################################################### MYSELF=${0##*/} MYNAME=${MYSELF%.*} VERSION="v0.2.1" function usage() { echo "Syntax: $MYSELF [ (e.g. /dev/sda, /dev/mmcblk1, /dev/nvme0n1) ]" } function cleanup() { local rc=$? echo "--- Cleaning up" if (( CREATE_DD_BACKUP )); then (( $rc )) && rm $IMAGE_FILENAME &>/dev/null if losetup $RBRI_RESTOREDEVICE &>/dev/null; then losetup -d "$RBRI_RESTOREDEVICE" fi fi rm -f $MSG_FILE &>/dev/null } function calcSumSizeFromSFDISK() { # sfdisk filename local file="$1" local partitionregex="/dev/.*[p]?([0-9]+)[^=]+=[^0-9]*([0-9]+)[^=]+=[^0-9]*([0-9]+)[^=]+=[^0-9a-z]*([0-9a-z]+)" local lineNo=0 local sumSize=0 while IFS="" read line; do (( lineNo++ )) if [[ -z $line ]]; then continue fi if [[ $line =~ $partitionregex ]]; then local p=${BASH_REMATCH[1]} local start=${BASH_REMATCH[2]} local size=${BASH_REMATCH[3]} local id=${BASH_REMATCH[4]} if [[ $id == 85 || $id == 5 ]]; then continue fi if [[ $sumSize == 0 ]]; then sumSize=$((start+size)) else (( sumSize+=size )) fi fi done < "$file" (( sumSize = ((sumSize - 1)/8 + 1)*4096 )) # align on 4096 boundary to speedup pishrink, kudos for kmbach echo "$sumSize" } # add pathes if not already set (usually not set in crontab) if [[ -e /bin/grep ]]; then PATHES="/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin" for p in $PATHES; do if ! /bin/grep -E -q "[\^:]$p[:$]" <<< $PATH; then [[ -z $PATH ]] && export PATH=$p || export PATH="$p:$PATH" fi done fi NL=$'\n' MAIL_EXTENSION_AVAILABLE=0 [[ $(which raspiImageMail.sh) ]] && MAIL_EXTENSION_AVAILABLE=1 if (( $MAIL_EXTENSION_AVAILABLE )); then # output redirection for email MSG_FILE=/tmp/msg$$.txt exec 1> >(stdbuf -i0 -o0 -e0 tee -a "$MSG_FILE" >&1) exec 2> >(stdbuf -i0 -o0 -e0 tee -a "$MSG_FILE" >&2) fi GIT_CODEVERSION="$MYSELF $VERSION" echo "$GIT_CODEVERSION" if (( $UID != 0 )); then echo "$MYSELF has to be invoked via sudo" exit fi # query invocation parms if (( $# < 1 )); then echo "??? Missing parameter Backupdirectory" usage exit 1 fi BACKUP_DIRECTORY="$1" if [[ ! -d $BACKUP_DIRECTORY ]]; then echo "??? Backupdirectory $BACKUP_DIRECTORY not found" usage exit 1 fi SFDISK_FILE="$(ls $BACKUP_DIRECTORY/*.sfdisk 2>/dev/null)" if [[ -z "$SFDISK_FILE" ]]; then echo "??? Incorrect backup path. .sfdisk file of backup not found" usage exit 1 fi if (( $# < 2 )); then CREATE_DD_BACKUP=1 IMAGE_FILENAME="${SFDISK_FILE%.*}.dd" RBRI_RESTOREDEVICE=$(losetup -f) else CREATE_DD_BACKUP=0 RBRI_RESTOREDEVICE="$2" if [[ ! -b $RBRI_RESTOREDEVICE ]]; then echo "??? Incorrect restore device" usage exit 1 fi fi # check for prerequisites if [[ ! $(which raspiBackup.sh) ]]; then echo "raspiBackup.sh not found" exit 1 fi if [[ ! $(which pishrink.sh) ]]; then echo "pishrink.sh not found" exit 1 fi if (( CREATE_DD_BACKUP )); then # wheezy does not discover more than one partition as default if grep -q "wheezy" /etc/os-release; then if ! grep -q "RBRI_RESTOREDEVICE.max_part=" /boot/cmdline.txt; then echo "Add 'RBRI_RESTOREDEVICE.max_part=15' in /boot/cmndline.txt first and reboot" exit 1 fi fi fi # cleanup trap cleanup SIGINT SIGTERM EXIT if (( CREATE_DD_BACKUP )); then rm "$IMAGE_FILENAME" &>/dev/null # calculate required image dis size SOURCE_DISK_SIZE=$(calcSumSizeFromSFDISK $SFDISK_FILE) mb=$(( $SOURCE_DISK_SIZE / 1024 / 1024 )) # calc MB echo "===> Backup source disk size: $mb (MiB)" # create image file dd if=/dev/zero of="$IMAGE_FILENAME" bs=1024k seek=$(( $mb )) count=0 losetup $RBRI_RESTOREDEVICE $IMAGE_FILENAME fi # restore backup now if (( CREATE_DD_BACKUP )); then echo "===> Restoring backup into $IMAGE_FILENAME" else echo "===> Restoring backup into $RBRI_RESTOREDEVICE" fi # prime partitions sfdisk -uSL -f $RBRI_RESTOREDEVICE < "$SFDISK_FILE" echo "===> Reloading new partition table" partprobe $RBRI_RESTOREDEVICE udevadm settle sleep 3 f=$(mktemp) echo 'DEFAULT_YES_NO_RESTORE_DEVICE=""' > $f raspiBackup.sh -1 -Y -d $RBRI_RESTOREDEVICE -f $f "$BACKUP_DIRECTORY" RC=$? rm $f if (( CREATE_DD_BACKUP )); then # The disk identifier is the Partition UUID (PTUUID displayed in blkid) and is stored just prior to the partition table in the MBR # The PARTUUID's aren't actually stored anywhere, they're simply PTUUID-01 for partition 1 and PTUUID-02 for partition 2 # You can change PTUUID on a live system with fdisk # Extract from https://www.raspberrypi.org/forums/viewtopic.php?t=191775 mkdir -p /mnt1 mount ${RBRI_RESTOREDEVICE}p2 /mnt1 PTUUID=$(grep -E "^[^#]+\s(/)\s.*" /mnt1/etc/fstab | cut -f 1 -d ' ' | sed 's/PARTUUID=//;s/\-.\+//') umount /mnt1 losetup -d $RBRI_RESTOREDEVICE if [[ -z $PTUUID ]]; then echo "??? Unrecoverable error. Unable to find PARTUUID of / in image" RC=1 fi # now shrink image if (( ! $RC )); then echo "===> PARTUUID to patch into image after pishrink: $PTUUID" echo echo "===> Shrinking Image $IMAGE_FILENAME" pishrink.sh "$IMAGE_FILENAME" RC=$? if (( $RC )); then echo "??? Error $RC received from piShrink" RC=1 echo "Program ends wihn error 42" fi else echo "??? Error $RC received" RC=1 fi # pishrink destroyes PARTUUID with resizsefs, restore original PTUUID now if (( ! $RC )); then RBRI_RESTOREDEVICE=$(losetup -f) echo "===> Patching image PARTUUID with $PTUUID" losetup -P $RBRI_RESTOREDEVICE $IMAGE_FILENAME printf "x\ni\n0x$PTUUID\nr\nw\nq\n" | fdisk $RBRI_RESTOREDEVICE partprobe $RBRI_RESTOREDEVICE udevadm settle sleep 3 fi fi if (( $MAIL_EXTENSION_AVAILABLE )); then IMAGE_FILENAME=${IMAGE_FILENAME##*/} HOST_NAME=${IMAGE_FILENAME%%-*} if (( $RC )); then status="with errors finished" else status="finished successfully" fi BODY="raspiBackupRestore2Image.sh $IMAGE_FILENAME$NL$NL$(echo -e "$(cat $MSG_FILE)")" raspiImageMail.sh "$HOSTNAME - Restore $status" "$BODY" if [[ $? = 0 ]]; then echo "-- Send email succeeded!" RC=0 fi fi exit $RC