Advertisement
agocska

rpi-clone2

Sep 22nd, 2013
1,544
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 15.22 KB | None | 0 0
  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
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement