#!/bin/bash # Adpated for cloning LibreELEC # from https://github.com/billw2/rpi-clone/commit/f961aee3f9816d345c2df335aa96b3907ee68b09 VERSION=1.5 # Version 1.5 2016/09/09 # * Remove any leading /dev/ from dest disk. # * Warn dest disk may be a partition if it ends with a digit. # Version 1.4 2016/03/23 # * Use -F on mkfs.ext4 and avoid the question. # Version 1.3 2016/03/16 # * don't > /dev/null for the mkfs.ext4 since mkfs can ask a question # Version 1.2 2015/02/17 # * add -x option # * tweak some echo messages # Version 1.1 2015/02/16 # * Do not include dphys swapfile in the clone. # * Add a missing -s flag to one of the parted commands. # * Dump parted stderr to /dev/null since it now complains about destination # disk partitions ending outside of disk before I get chance to resize # the partitions. # * Strip trailing s from PART_START - (fdisk now doesn't seem to accept...) # PGM=`basename $0` RSYNC_OPTIONS="--force -rltWDEgopt" # List of extra dirs to create under /storage. OPTIONAL_MNT_DIRS="clone mnt sda sdb rpi0 rpi1" # Where to mount the disk filesystems to be rsynced. CLONE=/storage/clone CLONE_LOG=/var/log/$PGM.log HOSTNAME=`hostname` SRC_BOOT_PARTITION_TYPE=`parted /dev/mmcblk0 -ms p | grep "^1" | cut -f 5 -d:` SRC_ROOT_PARTITION_TYPE=`parted /dev/mmcblk0 -ms p | grep "^2" | cut -f 5 -d:` if [ `id -u` != 0 ] then echo -e "$PGM needs to be run as root.\n" exit 1 fi if ! rsync --version > /dev/null then echo -e "\nOoops! rpi-clone needs the rsync program but cannot find it." echo "Make sure rsync is installed:" echo " $ sudo apt-get update" echo -e " $ sudo apt-get install rsync\n" exit 0 fi if test -e /usr/sbin/fsck.vfat then HAVE_FSCK_VFAT="yes" else echo "[Note] fsck.vfat was not found." echo "It is recommended to install dosfstools:" echo " $ sudo apt-get update" echo " $ sudo apt-get install dosfstools" fi usage() { echo "" echo "usage: $PGM sdN {-f|--force-initialize} {-v|--verbose} {-x}" echo " Example: $PGM sda" echo " -v - list all files as they are copied." echo " -f - force initialize the destination partitions" echo " -x - use set -x for very verbose bash shell script debugging" echo "" echo " Clone (rsync) a running Raspberry Pi file system to a destination" echo " SD card 'sdN' plugged into a Pi USB port (via a USB card reader)." echo " $PGM can clone the running system to a new SD card or can" echo " incrementally rsync to existing backup Raspberry Pi SD cards." echo "" echo " If the destination SD card has an existing $SRC_BOOT_PARTITION_TYPE partition 1 and a" echo " $SRC_ROOT_PARTITION_TYPE partition 2, $PGM assumes (unless using the -f option)" echo " that the SD card is an existing backup with the partitions" echo " properly sized and set up for a Raspberry Pi. All that is needed" echo " is to mount the partitions and rsync them to the running system." echo "" echo " If these partitions are not found (or -f), then $PGM will ask" echo " if it is OK to initialize the destination SD card partitions." echo " This is done by a partial 'dd' from the running booted device" echo " /dev/mmcblk0 to the destination SD card /dev/sdN followed by a" echo " fdisk resize and mkfs.ext4 of /dev/sdN partition 2." echo " This creates a completed $SRC_BOOT_PARTITION_TYPE partition 1 containing all boot" echo " files and an empty but properly sized partition 2 rootfs." echo " The SD card partitions are then mounted and rsynced to the" echo " running system." echo "" echo " The SD card destination partitions will be mounted on $CLONE." echo " A log will be written to $CLONE_LOG." echo " It's better to avoid running other disk writing programs" echo " when running $PGM." echo "" echo " Version $VERSION" exit 0 } VERBOSE=off while [ "$1" ] do case "$1" in -v|--verbose) VERBOSE=on RSYNC_OPTIONS=${RSYNC_OPTIONS}v ;; -f|--force-initialize) FORCE_INITIALIZE=true ;; -x) set -x ;; -h|--help) usage ;; *) if [ "$DST_DISK" != "" ] then echo "Bad args" usage fi DST_DISK=$1 ;; esac shift done if [ "$DST_DISK" = "" ] then usage exit 0 fi # Remove leading /dev/ DIR=${DST_DISK:0:5} if [ "$DIR" == "/dev/" ] then DST_DISK=${DST_DISK#/dev/} fi if ! cat /proc/partitions | grep -q $DST_DISK then echo "Destination disk '$DST_DISK' does not exist." echo "Plug the destination SD card into a card reader connected to a" echo "USB port. If it does not show up as '$DST_DISK', then do a" echo -e "'cat /proc/partitions' to see where it might be.\n" exit 0 fi CHK_DISK=`cat /proc/partitions | grep -m 1 $DST_DISK` D=${CHK_DISK:-1} if [ "$D" -eq "$D" ] 2>/dev/null then echo " The target disk you entered, $DST_DISK, ends with a digit and this" echo " may mean you have given a partition name instead of a disk name." echo " For example, if you want to clone to a disk sda," echo " specify sda and not one of its partition names like sda1 or sda2." echo -n "Do you want to continue anyway? (yes/no):" read resp if [ "$resp" != "y" ] && [ "$resp" != "yes" ] then echo "" exit 0 fi fi unmount_or_abort() { echo -n "Do you want to unmount $1? (yes/no): " read resp if [ "$resp" = "y" ] || [ "$resp" = "yes" ] then if ! umount $1 then echo "Sorry, $PGM could not unmount $1." echo -e "Aborting!\n" exit 0 fi else echo -e "Aborting!\n" exit 0 fi } DST_ROOT_PARTITION=/dev/${DST_DISK}2 DST_BOOT_PARTITION=/dev/${DST_DISK}1 # Check that none of the destination partitions are busy (mounted). # DST_ROOT_CURMOUNT=`fgrep "$DST_ROOT_PARTITION " /etc/mtab | cut -f 2 -d ' ' ` DST_BOOT_CURMOUNT=`fgrep "$DST_BOOT_PARTITION " /etc/mtab | cut -f 2 -d ' ' ` if [ "$DST_ROOT_CURMOUNT" != "" ] || [ "$DST_BOOT_CURMOUNT" != "" ] then echo "A destination partition is busy (mounted). Mount status:" echo " $DST_ROOT_PARTITION: $DST_ROOT_CURMOUNT" echo " $DST_BOOT_PARTITION: $DST_BOOT_CURMOUNT" if [ "$DST_BOOT_CURMOUNT" != "" ] then unmount_or_abort $DST_BOOT_CURMOUNT fi if [ "$DST_ROOT_CURMOUNT" != "" ] then unmount_or_abort $DST_ROOT_CURMOUNT fi fi TEST_MOUNTED=`fgrep " $CLONE " /etc/mtab | cut -f 1 -d ' ' ` if [ "$TEST_MOUNTED" != "" ] then echo "This script uses $CLONE for mounting filesystems, but" echo "$CLONE is already mounted with $TEST_MOUNTED." unmount_or_abort $CLONE fi if [ ! -d $CLONE ] then MNT_MOUNT=`fgrep " /mnt " /etc/mtab | cut -f 1 -d ' ' ` if [ "$MNT_MOUNT" = "" ] then mkdir $CLONE else echo "$MNT_MOUNT is currently mounted on /mnt." unmount_or_abort /mnt mkdir $CLONE fi fi # Borrowed from do_expand_rootfs in raspi-config expand_rootfs() { # Get the starting offset of the root partition # (with Jessie's parted, now need to strip trailing 's' from PART_START) PART_START=$(parted /dev/mmcblk0 -ms unit s p \ | grep "^2" | cut -f 2 -d: | cut -f 1 -d s) [ "$PART_START" ] || return 1 # Return value will likely be error for fdisk as it fails to reload the # partition table because the root fs is mounted fdisk /dev/$DST_DISK > /dev/null </dev/null \ | grep "^1" | cut -f 5 -d:` DST_ROOT_PARTITION_TYPE=`parted /dev/$DST_DISK -ms p 2>/dev/null \ | grep "^2" | cut -f 5 -d:` CLONE_MODE="rsync modified files to existing $DST_DISK file systems" if [ "$DST_BOOT_PARTITION_TYPE" != "$SRC_BOOT_PARTITION_TYPE" ] || \ [ "$DST_ROOT_PARTITION_TYPE" != "$SRC_ROOT_PARTITION_TYPE" ] || \ [ "$FORCE_INITIALIZE" = "true" ] then CLONE_MODE="rsync all files to $DST_DISK root file system" echo "" if [ "$FORCE_INITIALIZE" = "true" ] then echo "Forcing a partition initialization of destination disk $DST_DISK" else echo "The destination disk $DST_DISK partition table does not" echo "match the booted disk /dev/mmcblk0 partition table so a" echo "destination disk initialize is required." fi echo "The existing destination disk '$DST_DISK' partitions are:" parted /dev/$DST_DISK -s unit MB p 2>/dev/null \ | sed "/^Model/d ; /^Sector/d ; /^Disk Flags/d" # if [ "$DST_BOOT_PARTITION_TYPE" != "$SRC_BOOT_PARTITION_TYPE" ] # then # echo -e " ... Cannot find a destination boot file system of type: $SRC_BOOT_PARTITION_TYPE\n" # fi # if [ "$DST_ROOT_PARTITION_TYPE" != "$SRC_ROOT_PARTITION_TYPE" ] # then # echo -e " ... Cannot find a destination root file system of type: $SRC_ROOT_PARTITION_TYPE\n" # fi echo "*** All data on destination disk $DST_DISK will be overwritten! ***" echo "" echo -n "Do you want to initialize the destination disk /dev/$DST_DISK? (yes/no): " read resp if [ "$resp" = "y" ] || [ "$resp" = "yes" ] then # Image onto the destination disk a beginning fragment of the # running SD card file structure that spans at least more than # the start of partition 2. # # Calculate the start of partition 2 in MB for the dd. PART2_START=$(parted /dev/mmcblk0 -ms unit MB p | grep "^2" \ | cut -f 2 -d: | sed s/MB// | tr "," "." | cut -f 1 -d.) # and add some slop DD_COUNT=$(($PART2_START + 8)) echo "" echo "Imaging the partition structure, copying $DD_COUNT megabytes..." sync dd if=/dev/mmcblk0 of=/dev/$DST_DISK bs=1M count=$DD_COUNT # Partition was copied live so fsck to clean up for possible future # "Volume was not properly unmounted" warnings. if [ "$HAVE_FSCK_VFAT" = "yes" ] then echo "Running fsck on $DST_BOOT_PARTITION..." fsck -p $DST_BOOT_PARTITION &> /dev/null fi # But, though Partion 1 is now imaged, partition 2 is incomplete and # maybe the wrong size for the destination SD card. So fdisk it to # make it fill the rest of the disk and mkfs it to clean it out. # #echo "Sizing partition 2 (root partition) to use all SD card space..." #expand_rootfs mkfs.ext4 -F $DST_ROOT_PARTITION echo "" echo "/dev/$DST_DISK is initialized and resized. Its partitions are:" # fdisk -l /dev/$DST_DISK | grep $DST_DISK parted /dev/$DST_DISK unit MB p \ | sed "/^Model/d ; /^Sector/d ; /^Disk Flags/d" SRC_ROOT_VOL_NAME=`e2label /dev/mmcblk0p2` echo "" echo "Your booted /dev/mmcblk0p2 rootfs existing label: $SRC_ROOT_VOL_NAME" echo -n "You may enter a label for the destination rootfs $DST_ROOT_PARTITION: " read resp if [ "$resp" != "" ] then e2label $DST_ROOT_PARTITION $resp fi else echo -e "Aborting\n" exit 0 fi fi # =========== Setup Summary =========== # DST_ROOT_VOL_NAME=`e2label $DST_ROOT_PARTITION` if [ "$DST_ROOT_VOL_NAME" = "" ] then DST_ROOT_VOL_NAME="no label" fi echo "" echo "======== Clone Summary ========" echo "Clone mode : $CLONE_MODE" echo "Clone destination disk : $DST_DISK" echo "Clone destination rootfs : $DST_ROOT_PARTITION ($DST_ROOT_VOL_NAME) on ${CLONE}" echo "Clone destination bootfs : $DST_BOOT_PARTITION on ${CLONE}/boot" echo "Verbose mode : $VERBOSE" echo "===============================" # If this is an SD card initialization, can watch progress of the clone # in another terminal with: watch df -h # echo -n "Final check, is it Ok to proceed with the clone (yes/no)?: " read resp if [ "$resp" != "y" ] && [ "$resp" != "yes" ] then echo -e "Aborting the disk clone.\n" exit 0 fi # # =========== End of Setup =========== # Mount destination filesystems. echo "=> Mounting $DST_ROOT_PARTITION ($DST_ROOT_VOL_NAME) on $CLONE" if ! mount $DST_ROOT_PARTITION $CLONE then echo -e "Mount failure of $DST_ROOT_PARTITION, aborting!\n" exit 0 fi #if [ ! -d $CLONE/boot ] #then # mkdir $CLONE/boot #fi #echo "=> Mounting $DST_BOOT_PARTITION on $CLONE/boot" #if ! mount $DST_BOOT_PARTITION $CLONE/boot #then # umount $CLONE # echo -e "Mount failure of $DST_BOOT_PARTITION, aborting!\n" # exit 0 #fi echo "===============================" # Do not include a dhpys swapfile in the clone. dhpys-swapfile will # regenerate it at boot. # if [ -f /etc/dphys-swapfile ] then SWAPFILE=`cat /etc/dphys-swapfile | grep ^CONF_SWAPFILE | cut -f 2 -d=` if [ "$SWAPFILE" = "" ] then SWAPFILE=/var/swap fi EXCLUDE_SWAPFILE="--exclude $SWAPFILE" fi START_TIME=`date '+%H:%M:%S'` # Exclude fuse mountpoint .gvfs, various other mount points, and tmpfs # file systems from the rsync. # sync echo "Starting the filesystem rsync to $DST_DISK" echo -n "(This may take several minutes)..." rsync $RSYNC_OPTIONS --delete \ $EXCLUDE_SWAPFILE \ --exclude '.gvfs' \ --exclude '/dev' \ --exclude '/media' \ --exclude '/mnt' \ --exclude '/proc' \ --exclude '/run' \ --exclude '/sys' \ --exclude '/tmp' \ --exclude 'clone' \ --exclude 'lost\+found' \ /storage/ \ $CLONE # Fixup some stuff # #for i in dev media mnt proc run sys #do # if [ ! -d $CLONE/$i ] # then # mkdir $CLONE/$i # fi #done #if [ ! -d $CLONE/tmp ] #then # mkdir $CLONE/tmp # chmod a+w $CLONE/tmp #fi # Some extra optional dirs I create under /mnt #for i in $OPTIONAL_MNT_DIRS #do # if [ ! -d $CLONE/mnt/$i ] # then # mkdir $CLONE/mnt/$i # fi #done #rm -f $CLONE/etc/udev/rules.d/70-persistent-net.rules DATE=`date '+%F %H:%M'` echo "$DATE $HOSTNAME $PGM : clone to $DST_DISK ($DST_ROOT_VOL_NAME)" \ >> $CLONE_LOG #echo "$DATE $HOSTNAME $PGM : clone to $DST_DISK ($DST_ROOT_VOL_NAME)" \ # >> $CLONE/$CLONE_LOG STOP_TIME=`date '+%H:%M:%S'` echo "" echo "*** Done with clone to /dev/$DST_DISK ***" echo " Started: $START_TIME Finished: $STOP_TIME" echo "" # Pause before unmounting in case I want to inspect the clone results # or need to custom modify any files on the destination SD clone. # Eg. modify $CLONE/etc/hostname, $CLONE/etc/network/interfaces, etc # if I'm cloning into a card to be installed on another Pi. # #echo -n "Hit Enter when ready to unmount the /dev/$DST_DISK partitions..." #read resp #echo "unmounting $CLONE/boot" #umount $CLONE/boot #echo "unmounting $CLONE" #umount $CLONE echo "===============================" exit 0