diskimg 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. #!/bin/bash
  2. # This script allows backuping a complete SD card, NAND or eMMC card using tar utility
  3. #
  4. _resolve_file_location(){
  5. SOURCE="$1"
  6. while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  7. THE_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
  8. SOURCE="$(readlink "$SOURCE")"
  9. [[ "$SOURCE" != "/*" ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  10. done
  11. THE_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
  12. echo $THE_DIR
  13. }
  14. _get_relative_path_to(){
  15. source=$1
  16. target=$2
  17. common_part=$source
  18. back=
  19. while [ "${target#$common_part}" = "${target}" ]; do
  20. common_part=$(dirname $common_part)
  21. back="../${back}"
  22. done
  23. echo ${back}${target#$common_part/}
  24. }
  25. printHelp(){
  26. cat << EOF
  27. NAME:
  28. diskimg -- Creates or restores a partition
  29. SYNOPSIS:
  30. $0 -c TARGET_DIR -d DISK_DEVICE [-v]
  31. $0 -s SOURCE_DIR -d DISK_DEVICE [-v]
  32. DESCRIPTION:
  33. Creates a backup of the given device using the `-c` option. All partition
  34. table, boot sector, partition contents are stored into the given target
  35. directory in different files.
  36. Restores the backup using the `-r` option to the given device. All partition
  37. table, boot sector, partition contents are restored from the given target
  38. directory
  39. OPTIONS:
  40. -c creates the backup using the given TARGET_DIR value
  41. -d the DISK_DEVICE from/to process
  42. -e exclude file patterns from tar (creation). Re-use for several values (e.g -e /foo/bar -e /my/path)
  43. -u uncompressed image (disables compression)
  44. -z uses gzip compression
  45. -j uses bzip2 compression
  46. -P keep existing partitions
  47. -r restores from the data stored into the SOURCE_DIR value
  48. -J uses XZ compression (DEFAULT)
  49. -T skip tar archive creation (useful if backup only partition layout)
  50. -v Verbose
  51. -v Very verbose (debug mode)
  52. -D dry run
  53. EXAMPLES:
  54. One can easily recreate the partition scheme using using the following command
  55. (which will not write the data from the tar files) :
  56. $0 -r /my/path/to/backup -d /dev/sdb -T
  57. Then it is possible to change the layout (resize filesystem to partition size e.g.)
  58. And then to restore the data (option -P will preserve existing partitions):
  59. $0 -r /my/path/to/backup -d /dev/sdb -P
  60. EOF
  61. exit 0
  62. }
  63. OPTS=$(getopt c:d:e:FDhjJPr:uTvVz $*)
  64. #Test les paramètres
  65. if [ $? != 0 ]
  66. then
  67. echo "Error while retrieving paramger data from getopt"
  68. printHelp
  69. exit 1
  70. fi
  71. eval set -- "$OPTS"
  72. DISK_DEVICE=""
  73. TARGET_DIR=""
  74. SOURCE_DIR=""
  75. COMPRESSION_ALGORITHM="-J"
  76. COMPRESSION_SUFFIX=".xz"
  77. VERBOSE_OPTION=""
  78. EXCLUDE_PATTERNS=""
  79. KEEP_PARTITIONS=""
  80. FAKE_DEVICE=""
  81. while true ; do
  82. case "$1" in
  83. -d) DISK_DEVICE="$2" ; shift ; shift
  84. ;;
  85. -c) TARGET_DIR="$2" ; shift ; shift
  86. ;;
  87. -e) EXCLUDE_PATTERNS="$EXCLUDE_PATTERNS $2" ; shift ; shift
  88. ;;
  89. -F) FAKE_DEVICE="1"; shift
  90. ;;
  91. -r) SOURCE_DIR="$2" ; shift ; shift
  92. ;;
  93. -u) COMPRESSION_ALGORITHM="" ; COMPRESSION_SUFFIX="" ; shift
  94. ;;
  95. -z) COMPRESSION_ALGORITHM="-z" ; COMPRESSION_SUFFIX=".gz" ; shift
  96. ;;
  97. -j) COMPRESSION_ALGORITHM="-j"; COMPRESSION_SUFFIX=".bzip2" ; shift
  98. ;;
  99. -v) VERBOSE="1"; VERBOSE_OPTION="-v" ; shift
  100. ;;
  101. -V) VERBOSE="1"
  102. VERBOSE_OPTION="-v"
  103. set -x
  104. shift
  105. ;;
  106. -D) DEBUG="1"; shift
  107. ;;
  108. -P) KEEP_PARTITIONS="1"; shift
  109. ;;
  110. -T) SKIP_TAR="1"; shift
  111. ;;
  112. -h|--help)
  113. printHelp
  114. exit 0
  115. ;;
  116. --) shift; break
  117. ;;
  118. esac
  119. done
  120. create_exclude_string(){
  121. EXCLUDE_OPTION="--exclude=."
  122. EXCLUDE_STRING=""
  123. for pattern in $(echo $EXCLUDE_PATTERNS) ; do
  124. EXCLUDE_STRING="$EXCLUDE_STRING $EXCLUDE_OPTION$pattern"
  125. done
  126. }
  127. log_debug(){
  128. if [[ "$DEBUG" = "1" ]] ; then
  129. echo $*
  130. fi
  131. }
  132. prepare_btrfs_subvols(){
  133. local target_btrfs_dev=$1
  134. local temp_mount_point=$(mktemp -d)
  135. mount $target_btrfs_dev $temp_mount_point
  136. mkdir $temp_mount_point/.snapshots
  137. btrfs sub snap $temp_mount_point/ $temp_mount_point/.snapshots/ACTIVE
  138. #Gets the active sub snapshot id:
  139. local snap_id=$(btrfs sub list $temp_mount_point |grep '.snapshots/ACTIVE' | awk '{print $2}')
  140. #Sets it at the current snap
  141. btrfs sub set $snap_id $temp_mount_point
  142. }
  143. format_partition(){
  144. local partition_type="$1"
  145. local target_device="$2"
  146. echo "Formating $target_device with type $partition_type"
  147. if [[ "1" != "$DEBUG" ]] ; then
  148. case $partition_type in
  149. btrfs) mkfs.btrfs -f -L $partition_type $target_device
  150. test "$?" == 0 || exit $?
  151. prepare_btrfs_subvols $target_device
  152. ;;
  153. *)
  154. mkfs -t $partition_type $target_device
  155. test "$?" == 0 || exit $?
  156. ;;
  157. esac
  158. fi
  159. _REAL_TARGET_DEVICE=$target_device
  160. }
  161. create_luks_container(){
  162. local target_device="$1"
  163. echo "Creating a new LUKS container"
  164. if [ "$DEBUG" != "1" ] && [ "$FAKE_DEVICE" != "1" ] ; then
  165. cryptsetup luksFormat -c aes-xts-plain64 -s 512 -h sha512 $target_device <<EOF
  166. $_luks_password
  167. EOF
  168. fi
  169. }
  170. prepare_partition(){
  171. local partition_type="$1"
  172. local the_target_device="$2"
  173. local backup_basename=$3
  174. log_debug "partition_type=$partition_type"
  175. for part_type in $(echo $partition_type) ; do
  176. log_debug "part_type=$part_type"
  177. log_debug "the_target_device=$the_target_device"
  178. case $part_type in
  179. crypto_LUKS)
  180. ask_password $the_target_device confirm
  181. create_luks_container $the_target_device
  182. open_luks_container $the_target_device $backup_basename
  183. #Now that the partition is opened, we will format it at next loop:
  184. the_target_device=$_UNCRYPTED_DEVICE
  185. ;;
  186. *)
  187. format_partition $part_type $the_target_device
  188. ;;
  189. esac
  190. done
  191. _REAL_FILESYSTEM_TYPE=$part_type
  192. }
  193. restore_partition(){
  194. local partition_file=$1
  195. local partition_dev=$2
  196. local backup_basename=$3
  197. mount_dir_name="diskimg-$backup_basename"
  198. mountpoint_dir="/mnt/$mount_dir_name"
  199. test "1" = "$VERBOSE" && echo "Mounting $partition_dev to $mountpoint_dir"
  200. if [[ "$FAKE_DEVICE" != "1" ]] ; then
  201. mkdir -p $mountpoint_dir
  202. test "$?" == 0 || exit $?
  203. mount $partition_dev $mountpoint_dir
  204. test "$?" == 0 || exit $?
  205. fi
  206. test "1" = "$VERBOSE" && echo "Restoring from $partition_file to $mountpoint_dir"
  207. if [[ "$SKIP_TAR" != 1 ]] ; then
  208. if [[ "1" = "$DEBUG" ]] ; then
  209. echo tar --numeric-owner $COMPRESSION_ALGORITHM $VERBOSE_OPTION -pxf $partition_file -C $mountpoint_dir
  210. fi
  211. if [[ "$FAKE_DEVICE" != "1" ]] ; then
  212. if [[ "1" != "$DEBUG" ]] ; then
  213. tar --numeric-owner $COMPRESSION_ALGORITHM $VERBOSE_OPTION -pxf $partition_file -C $mountpoint_dir
  214. test "$?" == 0 || exit $?
  215. fi
  216. fi
  217. fi
  218. test "1" = "$VERBOSE" && echo "Un-mounting $partition_dev"
  219. if [[ "$FAKE_DEVICE" != "1" ]] ; then
  220. umount $partition_dev
  221. test "$?" == 0 || exit $?
  222. rmdir $mountpoint_dir
  223. test "$?" == 0 || exit $?
  224. fi
  225. }
  226. do_restore_disk(){
  227. echo "Restoring disk image from $SOURCE_DIR to $DISK_DEVICE"
  228. if [[ "$KEEP_PARTITIONS" != "1" ]] ; then
  229. echo "Restoring boot sector backup"
  230. if [[ "1" != "$DEBUG" ]] ; then
  231. dcfldd if=$SOURCE_DIR/boot-sector.img of=$DISK_DEVICE || exit 44
  232. fi
  233. echo "Restoring partition table backup"
  234. if [[ "1" != "$DEBUG" ]] ; then
  235. sfdisk $DISK_DEVICE < $SOURCE_DIR/partition-table.sfdisk || exit 45
  236. fi
  237. echo "Updating from partition information"
  238. if [[ "1" != "$DEBUG" ]] ; then
  239. hdparm -z $DISK_DEVICE
  240. fi
  241. fi
  242. ls $SOURCE_DIR/partition*.tar$COMPRESSION_SUFFIX > /dev/null 2>&1
  243. if [[ "$?" != "0" ]] ; then
  244. echo "There are no files with the suffix .tar$COMPRESSION_SUFFIX in directory $SOURCE_DIR"
  245. echo "Please check you are using the right compression algorithm (options could be -j -J or -z)"
  246. exit 6
  247. fi
  248. #For each partition, we restore the backup
  249. for partition_file in $(ls $SOURCE_DIR/partition*.tar$COMPRESSION_SUFFIX) ; do
  250. partition_dev=$(basename $partition_file | sed "s+partition-+$DISK_DEVICE+" |sed "s/\.tar$COMPRESSION_SUFFIX//")
  251. backup_basename=$(basename $partition_file | sed "s/\.tar$COMPRESSION_SUFFIX//")
  252. backup_filename="$backup_basename.tar$COMPRESSION_SUFFIX "
  253. partition_type_file=$SOURCE_DIR/$backup_basename.type
  254. echo "--- Preparing backup restoration of $partition_file into $partition_dev ---"
  255. if [[ "$KEEP_PARTITIONS" != "1" ]] ; then
  256. #First get the partition type
  257. partition_type=$(cat $partition_type_file)
  258. prepare_partition "$partition_type" $partition_dev $backup_basename
  259. fi
  260. restore_partition $partition_file $_REAL_TARGET_DEVICE $backup_basename
  261. done
  262. }
  263. restore_disk(){
  264. if [[ -d "$SOURCE_DIR" ]] ; then
  265. echo "Restoring disk image from $SOURCE_DIR to $DISK_DEVICE"
  266. echo ""
  267. echo "You will lose any data stored on $DISK_DEVICE"
  268. echo "Please make sure you have a backup"
  269. echo ""
  270. echo "ARE YOU SURE you want to erase any data on $DISK_DEVICE ? (yes/NO)"
  271. read answer
  272. while [[ "$answer" =~ [yY]$ ]] ; do
  273. echo "Please type yes or YES"
  274. read answer
  275. done
  276. if [[ "$answer" =~ yes ]] ; then
  277. do_restore_disk
  278. fi
  279. else
  280. echo "ERROR: $SOURCE_DIR does not exist"
  281. exit 4
  282. fi
  283. }
  284. check_override_file(){
  285. local target_path=$1
  286. answer=""
  287. if [[ -f $target_path ]] ; then
  288. echo "Are you sure you want to destroy all data in $target_path (all previously saved data will be losts !!!)"
  289. echo "(yes/No)"
  290. read answer
  291. while [[ "$answer" =~ [yY]$ ]] ; do
  292. echo "Please type yes or YES"
  293. read answer
  294. done
  295. if [[ "$answer" =~ yes ]] ; then
  296. return 0
  297. else
  298. return 1
  299. fi
  300. fi
  301. }
  302. backup_mounted_volume(){
  303. backup_basename=$1
  304. partition_dev=$2
  305. my_part_type=$3
  306. backup_filename="$backup_basename.tar$COMPRESSION_SUFFIX "
  307. echo "--- Creating backup for $partition_dev into $backup_filename ---"
  308. mount_dir_name="diskimg-$backup_basename"
  309. mountpoint_dir="/mnt/$mount_dir_name"
  310. test "1" = "$VERBOSE" && echo "Mounting $partition_dev to $mountpoint_dir"
  311. mkdir -p $mountpoint_dir
  312. mount $partition_dev $mountpoint_dir
  313. test "1" = "$VERBOSE" && echo "Backing up from $mountpoint_dir to $TARGET_DIR/$backup_filename"
  314. if [[ "$SKIP_TAR" != 1 ]] ; then
  315. if check_override_file $TARGET_DIR/$backup_filename ; then
  316. if [[ "1" != "$DEBUG" ]] ; then
  317. echo "tar --numeric-owner $COMPRESSION_ALGORITHM $VERBOSE_OPTION $EXCLUDE_STRING -pcf $TARGET_DIR/$backup_filename -C $mountpoint_dir ."
  318. tar --numeric-owner $COMPRESSION_ALGORITHM $VERBOSE_OPTION $EXCLUDE_STRING -pcf $TARGET_DIR/$backup_filename -C $mountpoint_dir .
  319. fi
  320. fi
  321. fi
  322. umount $partition_dev
  323. rmdir $mountpoint_dir
  324. }
  325. ask_password(){
  326. local partition_dev=$1
  327. local need_confirmation=$2
  328. local second_try="FAKE"
  329. while [[ "$second_try" != "$_luks_password" ]] ; do
  330. echo "Enter encrypted partition passphrase for $partition_dev:"
  331. read -s _luks_password
  332. if [[ "$need_confirmation" = "confirm" ]]; then
  333. echo "Please confirm passphrase:"
  334. read -s second_try
  335. if [[ "$second_try" != "$_luks_password" ]] ; then
  336. echo "ERROR: passphrases do not match ! Please try again"
  337. fi
  338. else
  339. $second_try="$_luks_password"
  340. fi
  341. done
  342. _luks_password=$second_try
  343. }
  344. open_luks_container(){
  345. local target_device=$1
  346. local backup_basename=$2
  347. if [ "$DEBUG" != "1" ] && [ "$FAKE_DEVICE" != "1" ] ; then
  348. cryptsetup luksOpen $target_device $backup_basename<<EOF
  349. $_luks_password
  350. EOF
  351. fi
  352. _UNCRYPTED_DEVICE=/dev/mapper/$backup_basename
  353. }
  354. ask_and_open_encrypted_part(){
  355. local partition_dev=$1
  356. local backup_basename=$2
  357. #Are we already opened ? in this case,
  358. #we should see "crypt" string in the output of lsblk -l -n $partition_dev
  359. lsblk -l -n $partition_dev | grep crypt > /dev/null 2>&1
  360. rv=$?
  361. if [ "$rv" != "0" ] ; then
  362. ask_password $partition_dev
  363. open_luks_container $partition_dev $backup_basename
  364. fi
  365. }
  366. ensure_crypt_part_is_opened(){
  367. partition_dev=$1
  368. backup_basename=$2
  369. ask_and_open_encrypted_part $partition_dev $backup_basename
  370. #Now we should get the uncrypted volume name (which is not necessarily the
  371. #same as $backup_basename)
  372. current_vol_name=$(lsblk -l -n $partition_dev |grep crypt | awk '{print $1}')
  373. #Now get the partition type and returns it
  374. my_part_type=`lsblk -l -n -o NAME,FSTYPE $partition_dev | grep $backup_basename | awk '{print $2}'`
  375. _UNCRYPTED_TYPE=${my_part_type}
  376. _UNCRYPTED_VOL=/dev/mapper/$current_vol_name
  377. }
  378. backup_disk(){
  379. if [[ ! -d "$TARGET_DIR" ]] ; then
  380. echo "$TARGET_DIR does not exist"
  381. echo "Do you want to create it ? (y/N)"
  382. read answer
  383. if [[ "$answer" =~ [yY] ]] ; then
  384. mkdir -p $TARGET_DIR
  385. else
  386. echo "Aborting because target dir does not exist"
  387. exit 126
  388. fi
  389. fi
  390. if [[ -d "$TARGET_DIR" ]] ; then
  391. echo "Creating disk image from $DISK_DEVICE to $TARGET_DIR..."
  392. echo "Creating boot sector backup"
  393. test "1" != "$DEBUG" && echo dcfldd if=$DISK_DEVICE bs=1M count=1 of=$TARGET_DIR/boot-sector.img
  394. test "1" != "$DEBUG" && dcfldd if=$DISK_DEVICE bs=1M count=1 of=$TARGET_DIR/boot-sector.img
  395. echo "Creating partition table backup"
  396. test "1" != "$DEBUG" && echo sfdisk -d $DISK_DEVICE > $TARGET_DIR/partition-table.sfdisk
  397. test "1" != "$DEBUG" && sfdisk -d $DISK_DEVICE > $TARGET_DIR/partition-table.sfdisk
  398. create_exclude_string
  399. #For each partition, we make a backup
  400. for partition_dev in $(sfdisk -d $DISK_DEVICE | grep -F 'start=' | awk '{print $1}') ; do
  401. backup_basename=$(echo $partition_dev | sed "s+$DISK_DEVICE+partition-+")
  402. #Device to mount will be empty unless necessary (ie: if a crypt volume is to be backup-ed)
  403. device_to_mount=""
  404. #Partition type
  405. echo "Managing partition type"
  406. part_type=`lsblk -lno NAME,FSTYPE $partition_dev | grep $(basename $partition_dev) | awk '{print $2}'`
  407. case $part_type in
  408. crypto_LUKS)
  409. #We extra steps here:
  410. #First decipher the luks partition:
  411. ensure_crypt_part_is_opened $partition_dev $backup_basename
  412. #The inner partition and device may have changed
  413. device_to_mount=${_UNCRYPTED_VOL}
  414. backup_mounted_volume $backup_basename $device_to_mount ${_UNCRYPTED_TYPE}
  415. #Concatenates the part types
  416. part_type="$part_type ${_UNCRYPTED_TYPE}"
  417. ;;
  418. *)
  419. backup_mounted_volume $backup_basename $partition_dev $part_type
  420. ;;
  421. esac
  422. part_type_filename="$backup_basename.type"
  423. echo $part_type > $TARGET_DIR/$part_type_filename
  424. done
  425. fi
  426. }
  427. if [ -z "$TARGET_DIR" ] && [ -z "$SOURCE_DIR" ]
  428. then
  429. echo "You must indicate an operation with -c or -r"
  430. exit 2
  431. fi
  432. if [ -z "$DISK_DEVICE" ]
  433. then
  434. echo "You must indicate a disk device"
  435. exit 3
  436. fi
  437. if [[ $EUID -ne 0 ]]; then
  438. echo "This script cannot work if not super-user. Please run as root" 1>&2
  439. exit 1
  440. fi
  441. if [[ "$TARGET_DIR" != "" ]] ; then
  442. backup_disk
  443. exit
  444. fi
  445. if [[ "$SOURCE_DIR" != "" ]] ; then
  446. restore_disk
  447. exit
  448. fi