Michael Schuh | 8d8cd83 | 2018-11-10 13:03:59 -0800 | [diff] [blame^] | 1 | #!/usr/bin/env ruby |
| 2 | |
| 3 | HELP_MSG_SHORT = <<END |
| 4 | # |
| 5 | # Usage: #{__FILE__} [-h] |
| 6 | # |
| 7 | # -h Print this help message. |
| 8 | # |
| 9 | END |
| 10 | |
| 11 | HELP_MSG_LONG = <<END |
| 12 | |
| 13 | Summary: |
| 14 | Backup an ODROID microSD card. |
| 15 | # |
| 16 | # The backup_ODROID_microSD_card.rb and restore_ODRIOD_microSD_card_from_backup.rb |
| 17 | # scripts are used to backup ODROID microSD cards and create new ones. They use |
| 18 | # tar, mkfs, and dd. dd is only used for the master boot record. The traditional |
| 19 | # way of copying a mircoSD card is to use dd to make a copy of the entire disk and then |
| 20 | # copy that back using dd. This can take a long time - hours - and requires having |
| 21 | # enough space to save a file that is the size of the microSD card. These scripts |
| 22 | # run in less than 5 minutes each and use only as much disk space as is require |
| 23 | # to hold the compressed contents of the microSD card. |
| 24 | # |
| 25 | # This https://wiki.odroid.com/odroid-xu4/software/partition_table |
| 26 | # page has some useful information on how hard disks are put together. |
| 27 | # It appears that the Ubuntu Partition Table Mainline U-boot (odroidxu4-v2017.05) |
| 28 | # section near the bottom of the page is how the ODROIDs are set up. The |
| 29 | # Boot Sequence part at the bottom is also helpful to understand the boot sequence. |
| 30 | # |
| 31 | # The scripts prompt the user for the device name to be used for the source |
| 32 | # of the backup and as the target for the restore. The root filesystem of |
| 33 | # the computer running these scripts is not allowed as an option. |
| 34 | # |
| 35 | # Note: If you have trouble booting the ODROID, make sure the small switch |
| 36 | # around the corner from the HDMI port is set to microSD and not MMC. |
| 37 | # If this does not work, try pushing and holding for a few seconds the |
| 38 | # power button on top of the case. It is located right by the two USB ports. |
| 39 | # Once it is booted, the blue heartbeat light should slowly flash. |
| 40 | # https://magazine.odroid.com/wp-content/uploads/odroid-xu4-user-manual.pdf |
| 41 | # has more information on the ODROID. |
| 42 | END |
| 43 | |
| 44 | HELP_MSG = HELP_MSG_LONG + HELP_MSG_SHORT |
| 45 | |
| 46 | require 'json' |
| 47 | require 'pp' |
| 48 | |
| 49 | # Process the command line arguments. |
| 50 | def parse_args() |
| 51 | # If there are any command line arguments, print the help message. |
| 52 | # This script will fail otherwise because the command line arguments |
| 53 | # are passed to the gets commands below which is not what we want. |
| 54 | # |
| 55 | # Print the help message if there |
| 56 | if ((ARGV[0] == "-h") || (ARGV.length > 0) ) |
| 57 | puts HELP_MSG |
| 58 | exit!() |
| 59 | end |
| 60 | end |
| 61 | |
| 62 | # Get the partition label |
| 63 | def get_label(partition) |
| 64 | return(`blkid -o export #{partition} | egrep "^LABEL="`.gsub(/LABEL=/,"").chomp) |
| 65 | end |
| 66 | |
| 67 | # Get the partition uuid |
| 68 | def get_uuid(partition) |
| 69 | return(`blkid -o export #{partition} | egrep "^UUID="`.gsub(/UUID=/,"").chomp) |
| 70 | end |
| 71 | |
| 72 | # Get the two ODROID partition names. |
| 73 | def get_partitions(device) |
| 74 | sfdisk = JSON.parse(`sfdisk --json /dev/#{device}`) |
| 75 | partitions = sfdisk['partitiontable']['partitions'] |
| 76 | partition1 = partition2 = nil # return nil if the partition does not exist |
| 77 | partition1 = partitions[0]['node'] if partitions.length > 0 |
| 78 | partition2 = partitions[1]['node'] if partitions.length > 1 |
| 79 | [partition1, partition2] |
| 80 | end |
| 81 | |
| 82 | # Check to see if a partition is mounted. |
| 83 | def is_mounted(partition) |
| 84 | `lsblk -no MOUNTPOINT #{partition} | wc -c`.chomp.to_i>1 |
| 85 | end |
| 86 | |
| 87 | def umount(partition) |
| 88 | #puts `df #{partition}` if is_mounted(partition) |
| 89 | echo_and_run("umount #{partition}") if is_mounted(partition) |
| 90 | end |
| 91 | |
| 92 | def mount(partition, dir) |
| 93 | # First make sure it is not mounted. |
| 94 | umount(partition) |
| 95 | echo_and_run("mount #{partition} #{dir}") |
| 96 | end |
| 97 | |
| 98 | # Echo and run a command. Print out the output from the command too. |
| 99 | def echo_and_run(cmd) |
| 100 | puts cmd |
| 101 | puts `#{cmd}` |
| 102 | end |
| 103 | |
| 104 | # Convert a float to minutes and seconds. |
| 105 | def time_to_minutes_and_seconds(time) |
| 106 | min = time.to_i/60 |
| 107 | sec = time.to_i%60 |
| 108 | sprintf("%d:%02d minutes:seconds",min,sec) |
| 109 | end |
| 110 | |
| 111 | # Get the size of the size of the Master Boot Record (MBR). |
| 112 | # Also include the 512 bytes for the partition table. |
| 113 | # The MBR resides in the space before start of the first |
| 114 | # partition. |
| 115 | def get_MBR_size(device) |
| 116 | # Use Json to parse the sfdisk output. |
| 117 | sfdisk = JSON.parse(`sfdisk --json /dev/#{device}`) |
| 118 | first_partition_start_in_blocks = sfdisk['partitiontable']['partitions'][0]['start'] |
| 119 | sector_size_in_bytes = `cat /sys/block/#{device}/queue/hw_sector_size`.chomp.to_i |
| 120 | return ([first_partition_start_in_blocks, sector_size_in_bytes]) |
| 121 | end |
| 122 | |
| 123 | $start_time = Time.now |
| 124 | $date=`date "+%Y%m%d.%H%M"`.chomp |
| 125 | $time = "/usr/bin/time -p" |
| 126 | |
| 127 | parse_args() |
| 128 | |
| 129 | # First figure out what devices are available. Do not show the root filesystem as an option. |
| 130 | # |
| 131 | # lsblk gives a listing (shown below) of the devices and mount points. |
| 132 | # findmnt -n -o SOURCE / tells which device the root filesystem is mounted on. |
| 133 | # /dev/nvme0n1p6 for the file systems below. |
| 134 | # |
| 135 | # NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT |
| 136 | # mmcblk0 179:0 0 29.7G 0 disk |
| 137 | # ├─mmcblk0p1 179:1 0 128M 0 part /media/michael/boot |
| 138 | # └─mmcblk0p2 179:2 0 14.8G 0 part /media/michael/rootfs |
| 139 | # nvme0n1 259:0 0 238.5G 0 disk |
| 140 | # ├─nvme0n1p1 259:1 0 1000M 0 part |
| 141 | # ├─nvme0n1p2 259:2 0 260M 0 part /boot/efi |
| 142 | # ├─nvme0n1p3 259:3 0 128M 0 part |
| 143 | # ├─nvme0n1p4 259:4 0 112.3G 0 part |
| 144 | # ├─nvme0n1p5 259:5 0 15.6G 0 part |
| 145 | # ├─nvme0n1p6 259:6 0 93.4G 0 part / |
| 146 | # └─nvme0n1p7 259:7 0 15.8G 0 part [SWAP] |
| 147 | |
| 148 | # Locate the root partition. |
| 149 | root_partition = `findmnt -n -o SOURCE /`.chomp.gsub('/dev/',"") |
| 150 | |
| 151 | # Use Json to parse the lsblk output. |
| 152 | lsblk = JSON.parse(`lsblk --json`) |
| 153 | |
| 154 | # Create a list of devices |
| 155 | devices = [] |
| 156 | partitions = {} |
| 157 | |
| 158 | lsblk['blockdevices'].each { |device| |
| 159 | devices.push(device_name = device["name"]) |
| 160 | # For each partition of this device, create a mapping from the partition to the device name. |
| 161 | device['children'].each { |partition| |
| 162 | partitions[partition["name"]]=device_name |
| 163 | } |
| 164 | } |
| 165 | puts HELP_MSG_SHORT |
| 166 | puts "\n# Backing up ODROID XU4 microSD card.\n" |
| 167 | puts "\n# The available devices are: #{devices.join(" ")}" |
| 168 | puts "# The root partition is: #{root_partition}" |
| 169 | puts "# The root device is #{partitions[root_partition]}. This will not be offered as an option. " |
| 170 | # Remove the root partition device from the list of devices. |
| 171 | devices.delete(partitions[root_partition]) |
| 172 | non_root_devices = devices.join(" ") |
| 173 | |
| 174 | clone_source_device = "" |
| 175 | |
| 176 | puts "# The non root devices are: #{non_root_devices}" |
| 177 | puts `lsblk #{non_root_devices.gsub(/\w+/){|s|'/dev/'+s}}` |
| 178 | |
| 179 | if (non_root_devices.length == 0) then |
| 180 | puts "\nERROR: No possible microSD card devices found. Exiting.\n\n" |
| 181 | exit -1 |
| 182 | end |
| 183 | |
| 184 | # Ask the user for the name of the device to be cloned. |
| 185 | loop do |
| 186 | printf "\n# Enter the name of the device to be cloned [#{non_root_devices}]: " |
| 187 | clone_source_device = gets.chomp |
| 188 | STDOUT.flush |
| 189 | if devices.include?(clone_source_device) then |
| 190 | break |
| 191 | else |
| 192 | puts "ERROR: device name '#{clone_source_device}' not found. Please try again." |
| 193 | end |
| 194 | end |
| 195 | |
| 196 | puts "# Using #{clone_source_device} for the device to be cloned." |
| 197 | |
| 198 | # |
| 199 | # Make tar and dd files of the microSD card. |
| 200 | # |
| 201 | |
| 202 | printf("\n# Provide a comment to be included in the filenames. i.e. 971-champs: ") |
| 203 | comment = gets.chomp.gsub("_","-") |
| 204 | #comment = "971_champs".gsub("_","-") |
| 205 | puts "# Using comment: #{comment}" |
| 206 | |
| 207 | # List the partitions |
| 208 | partition1, partition2 = get_partitions(clone_source_device) |
| 209 | partition1_label = get_label(partition1) |
| 210 | partition2_label = get_label(partition2) |
| 211 | partition1_uuid = get_uuid(partition1) |
| 212 | partition2_uuid = get_uuid(partition2) |
| 213 | puts "\n# Summary information:" |
| 214 | puts "# Partition 1 is #{partition1} with label #{partition1_label} and uuid #{partition1_uuid}" |
| 215 | puts "# Partition 2 is #{partition2} with label #{partition2_label} and uuid #{partition2_uuid}" |
| 216 | |
| 217 | base_name = "#{$date}_ODROID_XU4" |
| 218 | |
| 219 | tar_file_name_boot = "#{base_name}_p1_#{comment}_#{partition1_label}_#{partition1_uuid}.tgz" |
| 220 | tar_file_name_root = "#{base_name}_p2_#{comment}_#{partition2_label}_#{partition2_uuid}.tgz" |
| 221 | dd_file_name_mbr = "#{base_name}_MBR_#{comment}.dd" |
| 222 | sfdisk_file_name = "#{base_name}_sfdisk_#{comment}.txt" |
| 223 | puts "# Using disk partition information name: #{sfdisk_file_name}" |
| 224 | puts "# Using boot partition backup name: #{tar_file_name_boot }" |
| 225 | puts "# Using root partition backup name: #{tar_file_name_root}" |
| 226 | puts "# Using disk MBR (master boot record) information name: #{dd_file_name_mbr}" |
| 227 | |
| 228 | puts "\n# Backing up the sfdisk partition information." |
| 229 | echo_and_run("sfdisk -d /dev/#{clone_source_device} > #{sfdisk_file_name}") |
| 230 | |
| 231 | puts "\n# Backing up the boot partition." |
| 232 | `mkdir /new` if ! Dir.exists?("/new") |
| 233 | puts "# Make sure nothing is mounted on /new before mounting #{partition1} there." |
| 234 | puts "# Here is df output to check to see if anything is mounted on /new." |
| 235 | echo_and_run("df /new") |
| 236 | puts "# Running unmount /new to make sure nothing is mounted there." |
| 237 | echo_and_run("umount /new") |
| 238 | mount(partition1,"/new") |
| 239 | echo_and_run("#{$time} tar -czf #{tar_file_name_boot} -C /new .") |
| 240 | umount(partition1) |
| 241 | |
| 242 | puts "\n# Backing up the root partition." |
| 243 | mount(partition2,"/new") |
| 244 | echo_and_run("#{$time} tar -czf #{tar_file_name_root} -C /new .") |
| 245 | umount(partition2) |
| 246 | |
| 247 | puts "\n# Backing up the master boot record using dd" |
| 248 | mbr_size_in_blocks, sector_size_in_bytes = get_MBR_size(clone_source_device) |
| 249 | puts "# The block size is #{sector_size_in_bytes} bytes and the master boot record is " |
| 250 | puts "# #{mbr_size_in_blocks} blocks long for a total size of\ |
| 251 | #{mbr_size_in_blocks*sector_size_in_bytes} bytes." |
| 252 | echo_and_run("dd if=/dev/#{clone_source_device} of=#{dd_file_name_mbr} \ |
| 253 | bs=#{sector_size_in_bytes} count=#{mbr_size_in_blocks}") |
| 254 | |
| 255 | $end_time = Time.now |
| 256 | puts "" |
| 257 | puts "Start time: #{$start_time}" |
| 258 | puts " End time: #{$end_time}" |
| 259 | puts "======================================" |
| 260 | puts "Total time: #{time_to_minutes_and_seconds($end_time-$start_time)}" |
| 261 | puts "" |
| 262 | puts "# Done.\n" |