blob: e5fc4ada8f70e7e8804b39df85d04c9871fa3c12 [file] [log] [blame]
Michael Schuh8d8cd832018-11-10 13:03:59 -08001#!/usr/bin/env ruby
2
3HELP_MSG_SHORT = <<END
4#
5# Usage: #{__FILE__} [-h]
6#
7# -h Print this help message.
8#
9END
10
11HELP_MSG_LONG = <<END
12
13Summary:
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.
42END
43
44HELP_MSG = HELP_MSG_LONG + HELP_MSG_SHORT
45
46require 'json'
47require 'pp'
48
49# Process the command line arguments.
50def 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
60end
61
62# Get the partition label
63def get_label(partition)
64 return(`blkid -o export #{partition} | egrep "^LABEL="`.gsub(/LABEL=/,"").chomp)
65end
66
67# Get the partition uuid
68def get_uuid(partition)
69 return(`blkid -o export #{partition} | egrep "^UUID="`.gsub(/UUID=/,"").chomp)
70end
71
72# Get the two ODROID partition names.
73def 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]
80end
81
82# Check to see if a partition is mounted.
83def is_mounted(partition)
84 `lsblk -no MOUNTPOINT #{partition} | wc -c`.chomp.to_i>1
85end
86
87def umount(partition)
88 #puts `df #{partition}` if is_mounted(partition)
89 echo_and_run("umount #{partition}") if is_mounted(partition)
90end
91
92def mount(partition, dir)
93 # First make sure it is not mounted.
94 umount(partition)
95 echo_and_run("mount #{partition} #{dir}")
96end
97
98# Echo and run a command. Print out the output from the command too.
99def echo_and_run(cmd)
100 puts cmd
101 puts `#{cmd}`
102end
103
104# Convert a float to minutes and seconds.
105def 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)
109end
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.
115def 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])
121end
122
123$start_time = Time.now
124$date=`date "+%Y%m%d.%H%M"`.chomp
125$time = "/usr/bin/time -p"
126
127parse_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.
149root_partition = `findmnt -n -o SOURCE /`.chomp.gsub('/dev/',"")
150
151# Use Json to parse the lsblk output.
152lsblk = JSON.parse(`lsblk --json`)
153
154# Create a list of devices
155devices = []
156partitions = {}
157
158lsblk['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}
165puts HELP_MSG_SHORT
166puts "\n# Backing up ODROID XU4 microSD card.\n"
167puts "\n# The available devices are: #{devices.join(" ")}"
168puts "# The root partition is: #{root_partition}"
169puts "# 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.
171devices.delete(partitions[root_partition])
172non_root_devices = devices.join(" ")
173
174clone_source_device = ""
175
176puts "# The non root devices are: #{non_root_devices}"
177puts `lsblk #{non_root_devices.gsub(/\w+/){|s|'/dev/'+s}}`
178
179if (non_root_devices.length == 0) then
180 puts "\nERROR: No possible microSD card devices found. Exiting.\n\n"
181 exit -1
182end
183
184# Ask the user for the name of the device to be cloned.
185loop 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
194end
195
196puts "# Using #{clone_source_device} for the device to be cloned."
197
198#
199# Make tar and dd files of the microSD card.
200#
201
202printf("\n# Provide a comment to be included in the filenames. i.e. 971-champs: ")
203comment = gets.chomp.gsub("_","-")
204#comment = "971_champs".gsub("_","-")
205puts "# Using comment: #{comment}"
206
207# List the partitions
208partition1, partition2 = get_partitions(clone_source_device)
209partition1_label = get_label(partition1)
210partition2_label = get_label(partition2)
211partition1_uuid = get_uuid(partition1)
212partition2_uuid = get_uuid(partition2)
213puts "\n# Summary information:"
214puts "# Partition 1 is #{partition1} with label #{partition1_label} and uuid #{partition1_uuid}"
215puts "# Partition 2 is #{partition2} with label #{partition2_label} and uuid #{partition2_uuid}"
216
217base_name = "#{$date}_ODROID_XU4"
218
219tar_file_name_boot = "#{base_name}_p1_#{comment}_#{partition1_label}_#{partition1_uuid}.tgz"
220tar_file_name_root = "#{base_name}_p2_#{comment}_#{partition2_label}_#{partition2_uuid}.tgz"
221dd_file_name_mbr = "#{base_name}_MBR_#{comment}.dd"
222sfdisk_file_name = "#{base_name}_sfdisk_#{comment}.txt"
223puts "# Using disk partition information name: #{sfdisk_file_name}"
224puts "# Using boot partition backup name: #{tar_file_name_boot }"
225puts "# Using root partition backup name: #{tar_file_name_root}"
226puts "# Using disk MBR (master boot record) information name: #{dd_file_name_mbr}"
227
228puts "\n# Backing up the sfdisk partition information."
229echo_and_run("sfdisk -d /dev/#{clone_source_device} > #{sfdisk_file_name}")
230
231puts "\n# Backing up the boot partition."
232`mkdir /new` if ! Dir.exists?("/new")
233puts "# Make sure nothing is mounted on /new before mounting #{partition1} there."
234puts "# Here is df output to check to see if anything is mounted on /new."
235echo_and_run("df /new")
236puts "# Running unmount /new to make sure nothing is mounted there."
237echo_and_run("umount /new")
238mount(partition1,"/new")
239echo_and_run("#{$time} tar -czf #{tar_file_name_boot} -C /new .")
240umount(partition1)
241
242puts "\n# Backing up the root partition."
243mount(partition2,"/new")
244echo_and_run("#{$time} tar -czf #{tar_file_name_root} -C /new .")
245umount(partition2)
246
247puts "\n# Backing up the master boot record using dd"
248mbr_size_in_blocks, sector_size_in_bytes = get_MBR_size(clone_source_device)
249puts "# The block size is #{sector_size_in_bytes} bytes and the master boot record is "
250puts "# #{mbr_size_in_blocks} blocks long for a total size of\
251 #{mbr_size_in_blocks*sector_size_in_bytes} bytes."
252echo_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
256puts ""
257puts "Start time: #{$start_time}"
258puts " End time: #{$end_time}"
259puts "======================================"
260puts "Total time: #{time_to_minutes_and_seconds($end_time-$start_time)}"
261puts ""
262puts "# Done.\n"