blob: e5fc4ada8f70e7e8804b39df85d04c9871fa3c12 [file] [log] [blame]
#!/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"