Scripts for backing up and restoring ODROID microSD cards
The backup_ODROID_microSD_card.rb and
restore_ODRIOD_microSD_card_from_backup.rb scripts are used to backup
ODROID microSD cards and create new ones. They use tar, mkfs, and dd.
dd is only used for the master boot record. The traditional way of
copying a mircoSD card is to use dd to make a copy of the entire disk
and then copy that back using dd. This can take a long time - hours -
and requires having enough space to save a file that is the size of the
microSD card. These scripts run in less than 5 minutes each and use
only as much disk space as is require to hold the compressed contents
of the microSD card.
Change-Id: I763bcf8235596cc3e7dbba3ab3ec35fd6c680d5d
diff --git a/scripts/backup_ODROID_microSD_card.rb b/scripts/backup_ODROID_microSD_card.rb
new file mode 100755
index 0000000..e5fc4ad
--- /dev/null
+++ b/scripts/backup_ODROID_microSD_card.rb
@@ -0,0 +1,262 @@
+#!/usr/bin/env ruby
+
+HELP_MSG_SHORT = <<END
+#
+# Usage: #{__FILE__} [-h]
+#
+# -h Print this help message.
+#
+END
+
+HELP_MSG_LONG = <<END
+
+Summary:
+ Backup an ODROID microSD card.
+#
+# The backup_ODROID_microSD_card.rb and restore_ODRIOD_microSD_card_from_backup.rb
+# scripts are used to backup ODROID microSD cards and create new ones. They use
+# tar, mkfs, and dd. dd is only used for the master boot record. The traditional
+# way of copying a mircoSD card is to use dd to make a copy of the entire disk and then
+# copy that back using dd. This can take a long time - hours - and requires having
+# enough space to save a file that is the size of the microSD card. These scripts
+# run in less than 5 minutes each and use only as much disk space as is require
+# to hold the compressed contents of the microSD card.
+#
+# This https://wiki.odroid.com/odroid-xu4/software/partition_table
+# page has some useful information on how hard disks are put together.
+# It appears that the Ubuntu Partition Table Mainline U-boot (odroidxu4-v2017.05)
+# section near the bottom of the page is how the ODROIDs are set up. The
+# Boot Sequence part at the bottom is also helpful to understand the boot sequence.
+#
+# The scripts prompt the user for the device name to be used for the source
+# of the backup and as the target for the restore. The root filesystem of
+# the computer running these scripts is not allowed as an option.
+#
+# Note: If you have trouble booting the ODROID, make sure the small switch
+# around the corner from the HDMI port is set to microSD and not MMC.
+# If this does not work, try pushing and holding for a few seconds the
+# power button on top of the case. It is located right by the two USB ports.
+# Once it is booted, the blue heartbeat light should slowly flash.
+# https://magazine.odroid.com/wp-content/uploads/odroid-xu4-user-manual.pdf
+# has more information on the ODROID.
+END
+
+HELP_MSG = HELP_MSG_LONG + HELP_MSG_SHORT
+
+require 'json'
+require 'pp'
+
+# Process the command line arguments.
+def parse_args()
+ # If there are any command line arguments, print the help message.
+ # This script will fail otherwise because the command line arguments
+ # are passed to the gets commands below which is not what we want.
+ #
+ # Print the help message if there
+ if ((ARGV[0] == "-h") || (ARGV.length > 0) )
+ puts HELP_MSG
+ exit!()
+ end
+end
+
+# Get the partition label
+def get_label(partition)
+ return(`blkid -o export #{partition} | egrep "^LABEL="`.gsub(/LABEL=/,"").chomp)
+end
+
+# Get the partition uuid
+def get_uuid(partition)
+ return(`blkid -o export #{partition} | egrep "^UUID="`.gsub(/UUID=/,"").chomp)
+end
+
+# Get the two ODROID partition names.
+def get_partitions(device)
+ sfdisk = JSON.parse(`sfdisk --json /dev/#{device}`)
+ partitions = sfdisk['partitiontable']['partitions']
+ partition1 = partition2 = nil # return nil if the partition does not exist
+ partition1 = partitions[0]['node'] if partitions.length > 0
+ partition2 = partitions[1]['node'] if partitions.length > 1
+ [partition1, partition2]
+end
+
+# Check to see if a partition is mounted.
+def is_mounted(partition)
+ `lsblk -no MOUNTPOINT #{partition} | wc -c`.chomp.to_i>1
+end
+
+def umount(partition)
+ #puts `df #{partition}` if is_mounted(partition)
+ echo_and_run("umount #{partition}") if is_mounted(partition)
+end
+
+def mount(partition, dir)
+ # First make sure it is not mounted.
+ umount(partition)
+ echo_and_run("mount #{partition} #{dir}")
+end
+
+# Echo and run a command. Print out the output from the command too.
+def echo_and_run(cmd)
+ puts cmd
+ puts `#{cmd}`
+end
+
+# Convert a float to minutes and seconds.
+def time_to_minutes_and_seconds(time)
+ min = time.to_i/60
+ sec = time.to_i%60
+ sprintf("%d:%02d minutes:seconds",min,sec)
+end
+
+# Get the size of the size of the Master Boot Record (MBR).
+# Also include the 512 bytes for the partition table.
+# The MBR resides in the space before start of the first
+# partition.
+def get_MBR_size(device)
+ # Use Json to parse the sfdisk output.
+ sfdisk = JSON.parse(`sfdisk --json /dev/#{device}`)
+ first_partition_start_in_blocks = sfdisk['partitiontable']['partitions'][0]['start']
+ sector_size_in_bytes = `cat /sys/block/#{device}/queue/hw_sector_size`.chomp.to_i
+ return ([first_partition_start_in_blocks, sector_size_in_bytes])
+end
+
+$start_time = Time.now
+$date=`date "+%Y%m%d.%H%M"`.chomp
+$time = "/usr/bin/time -p"
+
+parse_args()
+
+# First figure out what devices are available. Do not show the root filesystem as an option.
+#
+# lsblk gives a listing (shown below) of the devices and mount points.
+# findmnt -n -o SOURCE / tells which device the root filesystem is mounted on.
+# /dev/nvme0n1p6 for the file systems below.
+#
+# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
+# mmcblk0 179:0 0 29.7G 0 disk
+# ├─mmcblk0p1 179:1 0 128M 0 part /media/michael/boot
+# └─mmcblk0p2 179:2 0 14.8G 0 part /media/michael/rootfs
+# nvme0n1 259:0 0 238.5G 0 disk
+# ├─nvme0n1p1 259:1 0 1000M 0 part
+# ├─nvme0n1p2 259:2 0 260M 0 part /boot/efi
+# ├─nvme0n1p3 259:3 0 128M 0 part
+# ├─nvme0n1p4 259:4 0 112.3G 0 part
+# ├─nvme0n1p5 259:5 0 15.6G 0 part
+# ├─nvme0n1p6 259:6 0 93.4G 0 part /
+# └─nvme0n1p7 259:7 0 15.8G 0 part [SWAP]
+
+# Locate the root partition.
+root_partition = `findmnt -n -o SOURCE /`.chomp.gsub('/dev/',"")
+
+# Use Json to parse the lsblk output.
+lsblk = JSON.parse(`lsblk --json`)
+
+# Create a list of devices
+devices = []
+partitions = {}
+
+lsblk['blockdevices'].each { |device|
+ devices.push(device_name = device["name"])
+ # For each partition of this device, create a mapping from the partition to the device name.
+ device['children'].each { |partition|
+ partitions[partition["name"]]=device_name
+ }
+}
+puts HELP_MSG_SHORT
+puts "\n# Backing up ODROID XU4 microSD card.\n"
+puts "\n# The available devices are: #{devices.join(" ")}"
+puts "# The root partition is: #{root_partition}"
+puts "# The root device is #{partitions[root_partition]}. This will not be offered as an option. "
+# Remove the root partition device from the list of devices.
+devices.delete(partitions[root_partition])
+non_root_devices = devices.join(" ")
+
+clone_source_device = ""
+
+puts "# The non root devices are: #{non_root_devices}"
+puts `lsblk #{non_root_devices.gsub(/\w+/){|s|'/dev/'+s}}`
+
+if (non_root_devices.length == 0) then
+ puts "\nERROR: No possible microSD card devices found. Exiting.\n\n"
+ exit -1
+end
+
+# Ask the user for the name of the device to be cloned.
+loop do
+ printf "\n# Enter the name of the device to be cloned [#{non_root_devices}]: "
+ clone_source_device = gets.chomp
+ STDOUT.flush
+ if devices.include?(clone_source_device) then
+ break
+ else
+ puts "ERROR: device name '#{clone_source_device}' not found. Please try again."
+ end
+end
+
+puts "# Using #{clone_source_device} for the device to be cloned."
+
+#
+# Make tar and dd files of the microSD card.
+#
+
+printf("\n# Provide a comment to be included in the filenames. i.e. 971-champs: ")
+comment = gets.chomp.gsub("_","-")
+#comment = "971_champs".gsub("_","-")
+puts "# Using comment: #{comment}"
+
+# List the partitions
+partition1, partition2 = get_partitions(clone_source_device)
+partition1_label = get_label(partition1)
+partition2_label = get_label(partition2)
+partition1_uuid = get_uuid(partition1)
+partition2_uuid = get_uuid(partition2)
+puts "\n# Summary information:"
+puts "# Partition 1 is #{partition1} with label #{partition1_label} and uuid #{partition1_uuid}"
+puts "# Partition 2 is #{partition2} with label #{partition2_label} and uuid #{partition2_uuid}"
+
+base_name = "#{$date}_ODROID_XU4"
+
+tar_file_name_boot = "#{base_name}_p1_#{comment}_#{partition1_label}_#{partition1_uuid}.tgz"
+tar_file_name_root = "#{base_name}_p2_#{comment}_#{partition2_label}_#{partition2_uuid}.tgz"
+dd_file_name_mbr = "#{base_name}_MBR_#{comment}.dd"
+sfdisk_file_name = "#{base_name}_sfdisk_#{comment}.txt"
+puts "# Using disk partition information name: #{sfdisk_file_name}"
+puts "# Using boot partition backup name: #{tar_file_name_boot }"
+puts "# Using root partition backup name: #{tar_file_name_root}"
+puts "# Using disk MBR (master boot record) information name: #{dd_file_name_mbr}"
+
+puts "\n# Backing up the sfdisk partition information."
+echo_and_run("sfdisk -d /dev/#{clone_source_device} > #{sfdisk_file_name}")
+
+puts "\n# Backing up the boot partition."
+`mkdir /new` if ! Dir.exists?("/new")
+puts "# Make sure nothing is mounted on /new before mounting #{partition1} there."
+puts "# Here is df output to check to see if anything is mounted on /new."
+echo_and_run("df /new")
+puts "# Running unmount /new to make sure nothing is mounted there."
+echo_and_run("umount /new")
+mount(partition1,"/new")
+echo_and_run("#{$time} tar -czf #{tar_file_name_boot} -C /new .")
+umount(partition1)
+
+puts "\n# Backing up the root partition."
+mount(partition2,"/new")
+echo_and_run("#{$time} tar -czf #{tar_file_name_root} -C /new .")
+umount(partition2)
+
+puts "\n# Backing up the master boot record using dd"
+mbr_size_in_blocks, sector_size_in_bytes = get_MBR_size(clone_source_device)
+puts "# The block size is #{sector_size_in_bytes} bytes and the master boot record is "
+puts "# #{mbr_size_in_blocks} blocks long for a total size of\
+ #{mbr_size_in_blocks*sector_size_in_bytes} bytes."
+echo_and_run("dd if=/dev/#{clone_source_device} of=#{dd_file_name_mbr} \
+ bs=#{sector_size_in_bytes} count=#{mbr_size_in_blocks}")
+
+$end_time = Time.now
+puts ""
+puts "Start time: #{$start_time}"
+puts " End time: #{$end_time}"
+puts "======================================"
+puts "Total time: #{time_to_minutes_and_seconds($end_time-$start_time)}"
+puts ""
+puts "# Done.\n"