SHARE
TWEET

rpi-clone2

agocska Sep 22nd, 2013 1,152 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/bin/bash
  2. #
  3. # Originally written by Bill Wilson, modified by Peter Agoston
  4. # Original code: https://github.com/billw2/rpi-clone
  5.  
  6. PGM=`basename $0`
  7.  
  8. RSYNC_OPTIONS="--force -rltWDEgopt"
  9.  
  10. # List of extra dirs to create under /mnt.
  11. OPTIONAL_MNT_DIRS="clone mnt sda sdb rpi0 rpi1"
  12.  
  13. # Where to mount the disk filesystems to be rsynced.
  14. CLONE=/mnt/clone
  15.  
  16. CLONE_LOG=/var/log/$PGM.log
  17.  
  18. HOSTNAME=`hostname`
  19.  
  20. SRC_BOOT_PARTITION_TYPE=`parted /dev/mmcblk0 -ms p | grep "^1" | cut -f 5 -d:`
  21. SRC_ROOT_PARTITION_TYPE=`parted /dev/mmcblk0 -ms p | grep "^2" | cut -f 5 -d:`
  22.  
  23. if [ `id -u` != 0 ]
  24. then
  25.     echo -e "$PGM needs to be run as root.\n"
  26.     exit 1
  27. fi
  28. echo "Checking necessary programs..."
  29. if [[ `which rsync` ]]; then
  30.         echo "Package 'rsync' is installed."
  31. else
  32.         echo "Package 'rsync' is NOT installed."
  33.         echo "Make sure rsync is installed:"
  34.         echo "    $ apt-get update"
  35.         echo -e "    $ apt-get install rsync\n"
  36.         exit 0
  37. fi
  38. if [[ `which kpartx` ]]; then
  39.         echo "Package 'kpartx' is installed."
  40. else
  41.         echo "Package 'kpartx' is NOT installed."
  42.         echo "Installing package 'kpartx'. Please wait..."
  43.         apt-get -y install kpartx || (echo "Installing package 'kpartx' was unsuccesful. Please install it manually!"; exit 1)
  44. fi
  45. if [[ `which pv` ]]; then
  46.         echo "Package 'pv' is installed."
  47. else
  48.         echo "Package 'pv' is NOT installed."
  49.         echo "Installing package 'pv'. Please wait..."
  50.         apt-get -y install pv || (echo "Installing package 'pv' was unsuccesful. Please install it manually!"; exit 1)
  51. fi
  52.  
  53.  
  54. usage()
  55.         {
  56.         echo ""
  57.         echo "usage: $PGM sdN {-f|--force-initialize} {-l|--loop} {-v|--verbose} {-y|--yes}"
  58.         echo "    Example:  $PGM sda"
  59.         echo "    -v - list all files as they are copied."
  60.         echo "    -l - destination is not sdN but an image file (created with a loop device)."
  61.         echo "    -f - force initialize the destination partitions."
  62.         echo "    -y - assume 'yes' to all questions during backup. (Silent backup)"
  63.         echo ""
  64.         echo "    Clone (rsync) a running Raspberry Pi file system to a destination"
  65.         echo "    SD card 'sdN' plugged into a Pi USB port (via a USB card reader)."
  66.         echo "    $PGM can clone the running system to a new SD card or can"
  67.         echo "    incrementally rsync to existing backup Raspberry Pi SD cards."
  68.         echo ""
  69.         echo "    If the destination SD card has an existing $SRC_BOOT_PARTITION_TYPE partition 1 and a"
  70.         echo "    $SRC_ROOT_PARTITION_TYPE partition 2, $PGM assumes (unless using the -f option)"
  71.         echo "    that the SD card is an existing backup with the partitions"
  72.         echo "    properly sized and set up for a Raspberry Pi.  All that is needed"
  73.         echo "    is to mount the partitions and rsync them to the running system."
  74.         echo ""
  75.         echo "    If these partitions are not found (or -f), then $PGM will ask"
  76.         echo "    if it is OK to initialize the destination SD card partitions."
  77.         echo "    This is done by a partial 'dd' from the running booted device"
  78.         echo "    /dev/mmcblk0 to the destination SD card /dev/sdN followed by a"
  79.         echo "    fdisk resize and mkfs.ext4 of /dev/sdN partition 2."
  80.         echo "    This creates a completed $SRC_BOOT_PARTITION_TYPE partition 1 containing all boot"
  81.         echo "    files and an empty but properly sized partition 2 rootfs."
  82.         echo "    The SD card  partitions are then mounted and rsynced to the"
  83.         echo "    running system."
  84.         echo ""
  85.         echo "    The SD card destination partitions will be mounted on $CLONE."
  86.         echo "    A log will be written to $CLONE_LOG."
  87.         echo "    Avoid running other disk writing programs when running $PGM."
  88.         echo ""
  89.         echo "    In case of cloning to a local image file (with -l), the filesystem"
  90.         echo "    needs to have a little more than twice as much free space as the"
  91.         echo "    total used space on the source Raspberry Pi, because it keeps"
  92.         echo "    the created temporary image for further (incremental) backups."
  93.         exit 0
  94.         }
  95.  
  96. VERBOSE=off
  97.  
  98. while [ "$1" ]
  99. do
  100.         case "$1" in
  101.                 -v|--verbose)
  102.                         VERBOSE=on
  103.                         RSYNC_OPTIONS=${RSYNC_OPTIONS}v
  104.                         ;;
  105.                 -f|--force-initialize)
  106.                         FORCE_INITIALIZE=true
  107.                         ;;
  108.                 -y|--yes)
  109.                         YES=true
  110.                         ;;
  111.                 -l|--loop)
  112.                         LOOP="-o loop"
  113.                         DST_FILE=$DST_DISK
  114.                         ;;
  115.                 -h|--help)
  116.                         usage
  117.                         ;;
  118.                 *)
  119.                         if [ "$DST_DISK" != "" ]
  120.                         then
  121.                                 echo "Bad args"
  122.                                 usage
  123.                         fi
  124.                         DST_DISK=$1
  125.                         ;;
  126.         esac
  127.         shift
  128. done
  129.  
  130.  
  131. if [ "$DST_DISK" = "" ]
  132. then
  133.         usage
  134.         exit 0
  135. fi
  136.  
  137. if [[ ! `cat /proc/partitions | grep -q $DST_DISK` && $LOOP = "" ]]; then
  138.         echo "Destination disk '$DST_DISK' does not exist."
  139.         echo "Plug the destination SD card into a USB port."
  140.         echo "If it does not show up  as '$DST_DISK', then do a"
  141.         echo -e "'cat /proc/partitions' to see where it might be.\n"
  142.         exit 0
  143. fi
  144.  
  145. unmount_or_abort()
  146.         {
  147.         echo -n "Do you want to unmount $1? (yes/no): "
  148.         [ $YES ] && resp="yes" || read resp
  149.         if [ "$resp" = "y" ] || [ "$resp" = "yes" ]
  150.         then
  151.                 if ! umount $1
  152.                 then
  153.                         echo "Sorry, $PGM could not unmount $1."
  154.                         echo -e "Aborting!\n"
  155.                         exit 0
  156.                 fi
  157.         else
  158.                 echo -e "Aborting!\n"
  159.                 exit 0
  160.         fi
  161.         }
  162.  
  163. if [ "$LOOP" != "" ]; then
  164.         DST_ROOT_PARTITION_SIZE=`df | grep '^/dev/root' | awk '{print $3}'`
  165.         DST_BOOT_PARTITION_SIZE=`df | grep '^/dev/mmcblk0p1' | awk '{print $3}'`
  166.         DST_SIZE=$((DST_BOOT_PARTITION_SIZE+DST_ROOT_PARTITION_SIZE+1024*500))
  167.         echo "Checking necessary disk space..."
  168.         DST_FS=$DST_FILE
  169.         until [[ `df|grep -c "${DST_FS}$"` -gt 0 ]]; do DST_FS=${DST_FS%/*}; done #getting filesystem of destination file
  170.         if [ `df -k | grep $DST_FS | awk '{print $4}'` -le $((DST_SIZE*2)) ]; then
  171.                 echo "Not enough free space in $DST_FS for the temporary file and the backup image. I need min. $((DST_SIZE*2/1024))MB. Aborting."
  172.                 exit 1
  173.         fi
  174.         if [ ! -e $DST_FILE.tmp ]; then
  175.                 echo "Temporary loop device file ($DST_FILE.tmp) does not exist yet, creating it now. Well, it may take a while, but this is must"
  176.                 echo "be done once only if you won't delete it after backup..."
  177.                 (pv -terpb /dev/zero -s ${DST_SIZE}k | dd of=$DST_FILE.tmp bs=1024 count=$DST_SIZE) || (echo "Creating temp file was unsuccesful."; exit 1)
  178.         fi
  179.         echo "Attaching the file to the first free loop device..."
  180.         if [ `losetup -a | grep -c "$DST_FILE.tmp"` -eq 0 ]; then
  181.                 (losetup -f $DST_FILE.tmp) || (echo "Loop device could not be attached."; exit 1)
  182.         else
  183.                 echo "The file has been already attached to a loop device."
  184.         fi
  185.         if [ $? -gt 0 ]; then
  186.                 echo "Attaching was unsuccesful. Do you have any free loop devices?"
  187.                 losetup -a
  188.                 exit 1
  189.         fi
  190.         LOOP_DEVICE=`losetup -a | grep "$DST_FILE.tmp" | cut -f 1 -d:`
  191.         echo "Used loop device: $LOOP_DEVICE"
  192.         DST_DISK=${LOOP_DEVICE#/dev/}
  193.         DST_ROOT_PARTITION=/dev/mapper/${DST_DISK}p2
  194.         DST_BOOT_PARTITION=/dev/mapper/${DST_DISK}p1
  195. else
  196.         DST_ROOT_PARTITION=/dev/${DST_DISK}
  197.         DST_BOOT_PARTITION=/dev/${DST_DISK}
  198. fi
  199.  
  200. # Check that none of the destination partitions are busy (mounted).
  201. #
  202. DST_ROOT_CURMOUNT=`fgrep "$DST_ROOT_PARTITION " /etc/mtab | cut -f 2 -d ' ' `
  203. DST_BOOT_CURMOUNT=`fgrep "$DST_BOOT_PARTITION " /etc/mtab | cut -f 2 -d ' ' `
  204.  
  205. if [ "$DST_ROOT_CURMOUNT" != "" ] || [ "$DST_BOOT_CURMOUNT" != "" ]
  206. then
  207.         echo "A destination partition is busy (mounted).  Mount status:"
  208.         echo "    $DST_ROOT_PARTITION:  $DST_ROOT_CURMOUNT"
  209.         echo "    $DST_BOOT_PARTITION:  $DST_BOOT_CURMOUNT"
  210.         if [ "$DST_BOOT_CURMOUNT" != "" ]
  211.         then
  212.                 unmount_or_abort $DST_BOOT_CURMOUNT
  213.         fi
  214.         if [ "$DST_ROOT_CURMOUNT" != "" ]
  215.         then
  216.                 unmount_or_abort $DST_ROOT_CURMOUNT
  217.         fi
  218. fi
  219.  
  220.  
  221. TEST_MOUNTED=`fgrep " $CLONE " /etc/mtab | cut -f 1 -d ' ' `
  222. if [ "$TEST_MOUNTED" != "" ]
  223. then
  224.         echo "This script uses $CLONE for mounting filesystems, but"
  225.         echo "$CLONE is already mounted with $TEST_MOUNTED."
  226.         unmount_or_abort $CLONE
  227. fi
  228.  
  229. if [ ! -d $CLONE ]
  230. then
  231.         MNT_MOUNT=`fgrep " /mnt " /etc/mtab | cut -f 1 -d ' ' `
  232.         if [ "$MNT_MOUNT" = "" ]
  233.         then
  234.                 mkdir $CLONE
  235.         else
  236.                 echo "$MNT_MOUNT is currently mounted on /mnt."
  237.                 unmount_or_abort /mnt
  238.                 mkdir $CLONE
  239.         fi
  240. fi
  241.  
  242.  
  243. # Borrowed from do_expand_rootfs in raspi-config
  244. expand_rootfs()
  245.         {
  246.         # Get the starting offset of the root partition
  247.         PART_START=$(parted /dev/mmcblk0 -ms unit s p | grep "^2" | cut -f 2 -d:)
  248.         [ "$PART_START" ] || return 1
  249.         # Return value will likely be error for fdisk as it fails to reload the
  250.         # partition table because the root fs is mounted
  251.         fdisk /dev/$DST_DISK > /dev/null <<EOF
  252. p
  253. d
  254. 2
  255. n
  256. p
  257. 2
  258. $PART_START
  259.  
  260. p
  261. w
  262. q
  263. EOF
  264.         }
  265.  
  266.  
  267. # =========== Disk Setup and Checks ===========
  268. #
  269. # Check that destination partitions are the right type.
  270. #
  271. DST_BOOT_PARTITION_TYPE=`parted /dev/$DST_DISK -ms p \
  272.                 | grep "^1" | cut -f 5 -d:`
  273. DST_ROOT_PARTITION_TYPE=`parted /dev/$DST_DISK -ms p \
  274.                 | grep "^2" | cut -f 5 -d:`
  275.  
  276.  
  277. if [ "$DST_BOOT_PARTITION_TYPE" != "$SRC_BOOT_PARTITION_TYPE" ] || \
  278.    [ "$DST_ROOT_PARTITION_TYPE" != "$SRC_ROOT_PARTITION_TYPE" ] || \
  279.    [ "$FORCE_INITIALIZE" = "true" ]
  280. then
  281.         echo ""
  282.         if [ "$FORCE_INITIALIZE" = "true" ]
  283.         then
  284.                 echo "*** Forcing a partition initialization of destination '$DST_DISK' ***"
  285.         fi
  286.  
  287.         echo "The existing partitions on destination disk '$DST_DISK' are:"
  288. #       fdisk -l /dev/$DST_DISK | grep $DST_DISK
  289.         parted /dev/$DST_DISK unit MB p \
  290.                 | sed "/^Model/d ; /^Sector/d"
  291.         if [ "$DST_BOOT_PARTITION_TYPE" != "$SRC_BOOT_PARTITION_TYPE" ]
  292.         then
  293.                 echo -e "  ... Cannot find a destination boot file system of type: $SRC_BOOT_PARTITION_TYPE\n"
  294.         fi
  295.         if [ "$DST_ROOT_PARTITION_TYPE" != "$SRC_ROOT_PARTITION_TYPE" ]
  296.         then
  297.                 echo -e "  ... Cannot find a destination root file system of type: $SRC_ROOT_PARTITION_TYPE\n"
  298.         fi
  299.         echo "This script can initialize the destination disk with a partition"
  300.         echo "structure copied from the currently booted filesytem and then resize"
  301.         echo "partition 2 (the root filesystem) to use all space on the SD card."
  302.         echo -n "Do you want to initialize the destination /dev/$DST_DISK? (yes/no): "
  303.         [ $YES ] && resp="yes" || read resp
  304.         if [ "$resp" = "y" ] || [ "$resp" = "yes" ]
  305.         then
  306.                 # Image onto the destination disk a beginning fragment of the
  307.                 # running SD card file structure that spans at least more than
  308.                 # the start of partition 2.
  309.                 #
  310.                 # Calculate the start of partition 2 in MB for the dd.
  311.                 PART2_START=$(parted /dev/mmcblk0 -ms unit MB p | grep "^2" \
  312.                                 | cut -f 2 -d: | sed s/MB// | tr "," "." | cut -f 1 -d.)
  313.                 # and add some slop
  314.                 DD_COUNT=`expr $PART2_START + 8`
  315.  
  316.                 echo ""
  317.                 echo "Imaging the partition structure, copying $DD_COUNT megabytes..."
  318.                 dd if=/dev/mmcblk0 of=/dev/$DST_DISK bs=1M count=$DD_COUNT
  319.  
  320.                 # But, though Partion 1 is now imaged, partition 2 is incomplete and
  321.                 # maybe the wrong size for the destination SD card.  So fdisk it to
  322.                 # make it fill the rest of the disk and mkfs it to clean it out.
  323.                 #
  324.                 echo "Sizing partition 2 (root partition) to use all SD card space..."
  325.                 expand_rootfs
  326.                 if [[ $LOOP != "" ]]; then
  327.                         #parted /dev/$DST_DISK mkfs y 2 ext2
  328.                         #tune2fs -O extents,uninit_bg,dir_index /dev/$DST_DISK
  329.                         echo "... and we do the partition mappings for the loop device:"
  330.                         kpartx -av /dev/$DST_DISK
  331.                 fi
  332.                 mkfs.ext4 $DST_ROOT_PARTITION > /dev/null
  333.  
  334.                 echo ""
  335.                 echo "/dev/$DST_DISK is initialized and resized.  Its partitions are:"
  336. #               fdisk -l /dev/$DST_DISK | grep $DST_DISK
  337.                 parted /dev/$DST_DISK unit MB p \
  338.                         | sed "/^Model/d ; /^Sector/d"
  339.  
  340.                 SRC_ROOT_VOL_NAME=`e2label /dev/mmcblk0p2`
  341.                 echo ""
  342.                 echo "Your booted /dev/mmcblk0p2 rootfs existing label: $SRC_ROOT_VOL_NAME"
  343.                 echo -n "You may enter a label for the destination rootfs $DST_ROOT_PARTITION: "
  344.                 [ $YES ] && resp="" || read resp
  345.                 if [ "$resp" != "" ]
  346.                 then
  347.                         e2label $DST_ROOT_PARTITION $resp
  348.                 fi
  349.         else
  350.                 echo -e "Aborting\n"
  351.                 exit 0
  352.         fi
  353. fi
  354.  
  355.  
  356. # =========== Setup Summary ===========
  357. #
  358. DST_ROOT_VOL_NAME=`e2label $DST_ROOT_PARTITION`
  359.  
  360. if [ "$DST_ROOT_VOL_NAME" = "" ]
  361. then
  362.         DST_ROOT_VOL_NAME="no label"
  363. fi
  364.  
  365. echo ""
  366. echo "Clone destination disk   :  $DST_DISK"
  367. echo "Clone destination rootfs :  $DST_ROOT_PARTITION ($DST_ROOT_VOL_NAME) on ${CLONE}"
  368. echo "Clone destination bootfs :  $DST_BOOT_PARTITION on ${CLONE}/boot"
  369. echo "Verbose mode             :  $VERBOSE"
  370.  
  371. echo "==============================="
  372.  
  373.  
  374. # If this is an SD card initialization, can watch progress of the clone
  375. # in another terminal with:  watch df -h
  376. #
  377. echo -n "Final check, is it Ok to proceed with the clone (yes/no)?: "
  378. [ $YES ] && resp="yes" || read resp
  379. if [ "$resp" != "y" ] && [ "$resp" != "yes" ]
  380. then
  381.         echo -e "Aborting the disk clone.\n"
  382.         exit 0
  383. fi
  384.  
  385. #
  386. # =========== End of Setup  ===========
  387.  
  388.  
  389.  
  390.  
  391. # Mount destination filesystems.
  392.  
  393. if [[ ! -e $DST_ROOT_PARTITION || ! -e $DST_BOOT_PARTITION ]]; then
  394.         echo "... and we do the partition mappings for the loop device:"
  395.         kpartx -av /dev/$DST_DISK
  396. fi
  397. echo "=> Mounting $DST_ROOT_PARTITION ($DST_ROOT_VOL_NAME) on $CLONE"
  398. if ! mount $LOOP ${DST_ROOT_PARTITION} $CLONE
  399. then
  400.         echo -e "Mount failure of $DST_ROOT_PARTITION, aborting!\n"
  401.         exit 0
  402. fi
  403.  
  404. if [ ! -d $CLONE/boot ]
  405. then
  406.         mkdir $CLONE/boot
  407. fi
  408.  
  409. echo "=> Mounting $DST_BOOT_PARTITION on $CLONE/boot"
  410. if ! mount $LOOP ${DST_BOOT_PARTITION} $CLONE/boot
  411. then
  412.         umount $CLONE
  413.         echo -e "Mount failure of $DST_BOOT_PARTITION, aborting!\n"
  414.         exit 0
  415. fi
  416.  
  417. echo "==============================="
  418.  
  419.  
  420.  
  421.  
  422. START_TIME=`date '+%H:%M:%S'`
  423.  
  424. # Exclude fuse mountpoint .gvfs, various other mount points, and tmpfs
  425. # file systems from the rsync.
  426. #
  427. sync
  428. echo "Starting the filesystem rsync to $DST_DISK"
  429. echo -n "(This may take several minutes)..."
  430. rsync $RSYNC_OPTIONS --delete \
  431.                 --exclude '.gvfs' \
  432.                 --exclude '/dev' \
  433.                 --exclude '/media' \
  434.                 --exclude '/mnt' \
  435.                 --exclude '/proc' \
  436.                 --exclude '/run' \
  437.                 --exclude '/sys' \
  438.                 --exclude '/tmp' \
  439.                 --exclude 'lost\+found' \
  440.         // \
  441.         $CLONE
  442.  
  443.  
  444.  
  445. # Fixup some stuff
  446. #
  447.  
  448. for i in dev media mnt proc run sys
  449. do
  450.         if [ ! -d $CLONE/$i ]
  451.         then
  452.                 mkdir $CLONE/$i
  453.         fi
  454. done
  455.  
  456. if [ ! -d $CLONE/tmp ]
  457. then
  458.         mkdir $CLONE/tmp
  459.         chmod a+w $CLONE/tmp
  460. fi
  461.  
  462. # Some extra optional dirs I create under /mnt
  463. for i in $OPTIONAL_MNT_DIRS
  464. do
  465.         if [ ! -d $CLONE/mnt/$i ]
  466.         then
  467.                 mkdir $CLONE/mnt/$i
  468.         fi
  469. done
  470.  
  471. rm -f $CLONE/etc/udev/rules.d/70-persistent-net.rules
  472.  
  473.  
  474. DATE=`date '+%F %H:%M'`
  475.  
  476. echo "$DATE  $HOSTNAME $PGM : clone to $DST_DISK ($DST_ROOT_VOL_NAME)" \
  477.                 >> $CLONE_LOG
  478. echo "$DATE  $HOSTNAME $PGM : clone to $DST_DISK ($DST_ROOT_VOL_NAME)" \
  479.                 >> $CLONE/$CLONE_LOG
  480.  
  481.  
  482. STOP_TIME=`date '+%H:%M:%S'`
  483.  
  484. echo ""
  485. echo "*** Done with clone to /dev/$DST_DISK ***"
  486. echo "    Started: $START_TIME    Finished: $STOP_TIME"
  487. echo ""
  488.  
  489. # Pause before unmounting in case I want to inspect the clone results
  490. # or need to custom modify any files on the destination SD clone.
  491. # Eg. modify $CLONE/etc/hostname, $CLONE/etc/network/interfaces, etc
  492. # if I'm cloning into a card to be installed on another Pi.
  493. #
  494. echo -n "Hit Enter when ready to unmount the /dev/$DST_DISK partitions..."
  495. [ $YES ] && resp="yes" || read resp
  496.  
  497. echo "unmounting $CLONE/boot"
  498. umount $CLONE/boot
  499.  
  500. echo "unmounting $CLONE"
  501. umount $CLONE
  502.  
  503. if [[ $LOOP != "" ]]; then
  504.         echo "Creating $DST_FILE..."
  505.         pv -terpb $DST_FILE.tmp > $DST_FILE
  506.         echo "Removing loop device mappings..."
  507.         kpartx -ad /dev/$DST_DISK
  508.         echo "Deattaching /dev/$DST_DISK loop device..."
  509.         losetup -d /dev/$DST_DISK
  510.         echo "Hint: If you want to make further backups later, do not delete the temporary file ($DST_FILE.tmp), you can always use that if it's size is still enough."
  511. fi
  512. echo "==============================="
  513.  
  514. exit 0
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
Not a member of Pastebin yet?
Sign Up, it unlocks many cool features!
 
Top