Replace fix_gpt.py script with `sgdisk -e'
[snf-image] / snf-image-helper / common.sh
1 # Copyright (C) 2011, 2012, 2013 GRNET S.A.
2 # Copyright (C) 2007, 2008, 2009 Google Inc.
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 # 02110-1301, USA.
18
19 PROGNAME=$(basename $0)
20
21 PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
22
23 # Programs
24 XMLSTARLET=xmlstarlet
25 TUNE2FS=tune2fs
26 RESIZE2FS=resize2fs
27 PARTED=parted
28 SFDISK=sfdisk
29 MKSWAP=mkswap
30 BLKID=blkid
31 BLOCKDEV=blockdev
32 REGLOOKUP=reglookup
33 CHNTPW=chntpw
34 SGDISK=sgdisk
35 DATE="date -u" # Time in UTC
36 EATMYDATA=eatmydata
37 MOUNT="mount -n"
38
39 CLEANUP=( )
40 ERRORS=( )
41 WARNINGS=( )
42
43 MSG_TYPE_TASK_START="TASK_START"
44 MSG_TYPE_TASK_END="TASK_END"
45
46 STDERR_LINE_SIZE=10
47
48 add_cleanup() {
49     local cmd=""
50     for arg; do cmd+=$(printf "%q " "$arg"); done
51     CLEANUP+=("$cmd")
52 }
53
54 close_fd() {
55     local fd=$1
56
57     exec {fd}>&-
58 }
59
60 send_result_kvm() {
61     echo "$@" > /dev/ttyS1
62 }
63
64 send_monitor_message_kvm() {
65     echo "$@" > /dev/ttyS2
66 }
67
68 send_result_xen() {
69     xenstore-write /local/domain/0/snf-image-helper/$DOMID "$*"
70 }
71
72 send_monitor_message_xen() {
73     #Broadcast the message
74     echo "$@" | socat "STDIO" "UDP-DATAGRAM:${BROADCAST}:${MONITOR_PORT},broadcast"
75 }
76
77 prepare_helper() {
78         local cmdline item key val hypervisor domid
79
80         read -a cmdline  < /proc/cmdline
81         for item in "${cmdline[@]}"; do
82             key=$(cut -d= -f1 <<< "$item")
83             val=$(cut -d= -f2 <<< "$item")
84             if [ "$key" = "hypervisor" ]; then
85                 hypervisor="$val"
86             fi
87             if [ "$key" = "rules_dev" ]; then
88                 export RULES_DEV="$val"
89             fi
90             if [ "$key" = "helper_ip" ]; then
91                 export IP="$val"
92                 export NETWORK="$IP/24"
93                 export BROADCAST="${IP%.*}.255"
94             fi
95             if [ "$key" = "monitor_port" ]; then
96                 export MONITOR_PORT="$val"
97             fi
98         done
99
100     case "$hypervisor" in
101     kvm)
102         HYPERVISOR=kvm
103         ;;
104     xen-hvm|xen-pvm)
105         if [ -z "$IP" ]; then
106             echo "ERROR: \`helper_ip' not defined or empty" >&2
107             exit 1
108         fi
109         if [ -z "$MONITOR_PORT" ]; then
110             echo "ERROR: \`monitor_port' not defined or empty" >&2
111             exit 1
112         fi
113         $MOUNT -t xenfs xenfs /proc/xen
114         ip addr add "$NETWORK" dev eth0
115         ip link set eth0 up
116         ip route add default dev eth0
117         export DOMID=$(xenstore-read domid)
118         HYPERVISOR=xen
119         ;;
120     *)
121         echo "ERROR: Unknown hypervisor: \`$hypervisor'" >&2
122         exit 1
123         ;;
124     esac
125
126     export HYPERVISOR
127 }
128
129 report_error() {
130     msg=""
131     if [ ${#ERRORS[*]} -eq 0 ]; then
132         # No error message. Print stderr
133         local lines
134         lines=$(tail --lines=${STDERR_LINE_SIZE} "$STDERR_FILE" | wc -l)
135         msg="STDERR:${lines}"
136         msg+=$(tail --lines=$lines  "$STDERR_FILE")
137     else
138         for line in "${ERRORS[@]}"; do
139             msg+="ERROR:$line"$'\n'
140         done
141     fi
142
143     send_monitor_message_${HYPERVISOR} "$msg"
144 }
145
146 log_error() {
147     ERRORS+=("$*")
148
149     send_monitor_message_${HYPERVISOR} "ERROR: $@"
150     send_result_${HYPERVISOR} "ERROR: $@"
151
152     # Use return instead of exit. The set -x options will terminate the script
153     # but will also trigger ERR traps if defined.
154     return 1
155 }
156
157 warn() {
158     echo "Warning: $@" >&2
159     send_monitor_message_${HYPERVISOR} "WARNING: $@"
160 }
161
162 report_task_start() {
163     send_monitor_message_${HYPERVISOR} "$MSG_TYPE_TASK_START:${PROGNAME:2}"
164 }
165
166 report_task_end() {
167     send_monitor_message_${HYPERVISOR} "$MSG_TYPE_TASK_END:${PROGNAME:2}"
168 }
169
170 system_poweroff() {
171     while [ 1 ]; do
172         # Credits to psomas@grnet.gr for this ...
173         echo o > /proc/sysrq-trigger
174         sleep 1
175     done
176 }
177
178 get_base_distro() {
179     local root_dir=$1
180
181     if [ -e "$root_dir/etc/debian_version" ]; then
182         echo "debian"
183     elif [ -e "$root_dir/etc/redhat-release" ]; then
184         echo "redhat"
185     elif [ -e "$root_dir/etc/slackware-version" ]; then
186         echo "slackware"
187     elif [ -e "$root_dir/etc/SuSE-release" ]; then
188         echo "suse"
189     elif [ -e "$root_dir/etc/gentoo-release" ]; then
190         echo "gentoo"
191     elif [ -e "$root_dir/etc/arch-release" ]; then
192         echo "arch"
193     else
194         warn "Unknown base distro."
195     fi
196 }
197
198 get_distro() {
199     local root_dir distro
200     root_dir=$1
201
202     if [ -e "$root_dir/etc/debian_version" ]; then
203         distro="debian"
204         if [ -e ${root_dir}/etc/lsb-release ]; then
205             ID=$(grep ^DISTRIB_ID= ${root_dir}/etc/lsb-release | cut -d= -f2)
206             if [ "x$ID" = "xUbuntu" ]; then
207                 distro="ubuntu"
208             fi
209         fi
210         echo "$distro"
211     elif [ -e "$root_dir/etc/fedora-release" ]; then
212         echo "fedora"
213     elif [ -e "$root_dir/etc/centos-release" ]; then
214         echo "centos"
215     elif [ -e "$root_dir/etc/redhat-release" ]; then
216         echo "redhat"
217     elif [ -e "$root_dir/etc/slackware-version" ]; then
218         echo "slackware"
219     elif [ -e "$root_dir/etc/SuSE-release" ]; then
220         echo "suse"
221     elif [ -e "$root_dir/etc/gentoo-release" ]; then
222         echo "gentoo"
223     elif [ -e "$root_dir/etc/arch-release" ]; then
224         echo "arch"
225     else
226         warn "Unknown distro."
227     fi
228 }
229
230
231 get_partition_table() {
232     local dev output
233     dev="$1"
234     # If the partition table is gpt then parted will raise an error if the
235     # secondary gpt is not it the end of the disk, and a warning that has to
236     # do with the "Last Usable LBA" entry in gpt.
237     if ! output="$("$PARTED" -s -m "$dev" unit s print | grep -E -v "^(Warning|Error): ")"; then
238         log_error "Unable to read partition table for device \`${dev}'. The image seems corrupted."
239     fi
240
241     echo "$output"
242 }
243
244 get_partition_table_type() {
245     local ptable dev field
246     ptable="$1"
247
248     dev="$(sed -n 2p <<< "$ptable")"
249     IFS=':' read -ra field <<< "$dev"
250
251     echo "${field[5]}"
252 }
253
254 get_partition_count() {
255     local ptable="$1"
256
257     expr $(echo "$ptable" | wc -l) - 2
258 }
259
260 get_partition_by_num() {
261     local ptable="$1"
262     local id="$2"
263
264     grep "^$id:" <<< "$ptable"
265 }
266
267 get_last_partition() {
268     local ptable="$1"
269
270     echo "$ptable" | tail -1
271 }
272
273 is_extended_partition() {
274     local dev="$1"
275     local part_num="$2"
276
277     id=$($SFDISK --print-id "$dev" "$part_num")
278     if [ "$id" = "5" -o "$id" = "f" ]; then
279         echo "yes"
280     else
281         echo "no"
282     fi
283 }
284
285 get_extended_partition() {
286     local ptable dev
287     ptable="$1"
288     dev="$(echo "$ptable" | sed -n 2p | cut -d':' -f1)"
289
290     tail -n +3 <<< "$ptable" | while read line; do
291         part_num=$(cut -d':' -f1 <<< "$line")
292         if [ $(is_extended_partition "$dev" "$part_num") == "yes" ]; then
293             echo "$line"
294             return 0
295         fi
296     done
297     echo ""
298 }
299
300 get_logical_partitions() {
301     local ptable part_num
302     ptable="$1"
303
304     tail -n +3 <<< "$ptable" | while read line; do
305         part_num=$(cut -d':' -f1 <<< "$line")
306         if [ $part_num -ge 5 ]; then
307             echo "$line"
308         fi
309     done
310
311     return 0
312 }
313
314 get_last_primary_partition() {
315     local ptable dev output
316     ptable="$1"
317     dev=$(echo "ptable" | sed -n 2p | cut -d':' -f1)
318
319     for i in 4 3 2 1; do
320         if output=$(grep "^$i:" <<< "$ptable"); then
321             echo "$output"
322             return 0
323         fi
324     done
325     echo ""
326 }
327
328 get_partition_to_resize() {
329     local dev table table_type last_part last_part_num extended last_primary \
330         ext_num prim_num
331     dev="$1"
332
333     table=$(get_partition_table "$dev")
334
335     if [ $(get_partition_count "$table") -eq 0 ]; then
336         return 0
337     fi
338
339     table_type=$(get_partition_table_type "$table")
340     last_part=$(get_last_partition "$table")
341     last_part_num=$(cut -d: -f1 <<< "$last_part")
342
343     if [ "$table_type" == "msdos" -a $last_part_num -gt 4 ]; then
344         extended=$(get_extended_partition "$table")
345         last_primary=$(get_last_primary_partition "$table")
346         ext_num=$(cut -d: -f1 <<< "$extended")
347         last_prim_num=$(cut -d: -f1 <<< "$last_primary")
348
349         if [ "$ext_num" != "$last_prim_num" ]; then
350             echo "$last_prim_num"
351         else
352             echo "$last_part_num"
353         fi
354     else
355         echo "$last_part_num"
356     fi
357 }
358
359 create_partition() {
360     local device="$1"
361     local part="$2"
362     local ptype="$3"
363
364     local fields=()
365     IFS=":;" read -ra fields <<< "$part"
366     local id="${fields[0]}"
367     local start="${fields[1]}"
368     local end="${fields[2]}"
369     local size="${fields[3]}"
370     local fs="${fields[4]}"
371     local name="${fields[5]}"
372     local flags="${fields[6]//,/ }"
373
374     $PARTED -s -m -- $device mkpart "$ptype" $fs "$start" "$end"
375     for flag in $flags; do
376         $PARTED -s -m $device set "$id" "$flag" on
377     done
378 }
379
380 enlarge_partition() {
381     local device part ptype new_end fields new_part table logical id
382     device="$1"
383     part="$2"
384     ptype="$3"
385     new_end="$4"
386
387     if [ -z "$new_end" ]; then
388         new_end=$(cut -d: -f 3 <<< "$(get_last_free_sector "$device")")
389     fi
390
391     fields=()
392     IFS=":;" read -ra fields <<< "$part"
393     fields[2]="$new_end"
394
395     new_part=""
396     for ((i = 0; i < ${#fields[*]}; i = i + 1)); do
397         new_part="$new_part":"${fields[$i]}"
398     done
399     new_part=${new_part:1}
400
401     # If this is an extended partition, removing it will also remove the
402     # logical partitions it contains. We need to save them for later.
403     if [ "$ptype" = "extended" ]; then
404         table="$(get_partition_table "$device")"
405         logical="$(get_logical_partitions "$table")"
406     fi
407
408     id=${fields[0]}
409     $PARTED -s -m "$device" rm "$id"
410     create_partition "$device" "$new_part" "$ptype"
411
412     if [ "$ptype" = "extended" ]; then
413         # Recreate logical partitions
414         echo "$logical" | while read logical_part; do
415             create_partition "$device" "$logical_part" "logical"
416         done
417     fi
418 }
419
420 get_last_free_sector() {
421     local dev unit last_line ptype
422     dev="$1"
423     unit="$2"
424
425     if [ -n "$unit" ]; then
426         unit="unit $unit"
427     fi
428
429     last_line="$($PARTED -s -m "$dev" "$unit" print free | tail -1)"
430     ptype="$(cut -d: -f 5 <<< "$last_line")"
431
432     if [ "$ptype" = "free;" ]; then
433         echo "$last_line"
434     fi
435 }
436
437 get_unattend() {
438     local target exists
439     target="$1"
440
441     # Workaround to search for $target/Unattend.xml in an case insensitive way.
442     exists=$(find "$target"/ -maxdepth 1 -iname unattend.xml)
443     if [ $(wc -l <<< "$exists") -gt 1 ]; then
444         log_error "Found multiple Unattend.xml files in the image:" $exists
445     fi
446
447     echo "$exists"
448 }
449
450 umount_all() {
451     local target mpoints
452     target="$1"
453
454     # Unmount file systems mounted under directory `target'
455     mpoints="$({ awk "{ if (match(\$2, \"^$target\")) { print \$2 } }" < /proc/mounts; } | sort -rbd | uniq)"
456
457     for mpoint in $mpoints; do
458         umount $mpoint
459     done
460 }
461
462 cleanup() {
463     # if something fails here, it shouldn't call cleanup again...
464     trap - EXIT
465
466     if [ ${#CLEANUP[*]} -gt 0 ]; then
467         LAST_ELEMENT=$((${#CLEANUP[*]}-1))
468         REVERSE_INDEXES=$(seq ${LAST_ELEMENT} -1 0)
469         for i in $REVERSE_INDEXES; do
470             # If something fails here, it's better to retry it for a few times
471             # before we give up with an error. This is needed for kpartx when
472             # dealing with ntfs partitions mounted through fuse. umount is not
473             # synchronous and may return while the partition is still busy. A
474             # premature attempt to delete partition mappings through kpartx on
475             # a device that hosts previously mounted ntfs partition may fail
476             # with a `device-mapper: remove ioctl failed: Device or resource
477             # busy' error. A sensible workaround for this is to wait for a
478             # while and then try again.
479             local cmd=${CLEANUP[$i]}
480             $cmd || for interval in 0.25 0.5 1 2 4; do
481             echo "Command $cmd failed!"
482             echo "I'll wait for $interval secs and will retry..."
483             sleep $interval
484             $cmd && break
485         done
486         if [ "$?" != "0" ]; then
487             echo "Giving Up..."
488             exit 1;
489         fi
490     done
491   fi
492 }
493
494 task_cleanup() {
495     local rc=$?
496
497     if [ $rc -eq 0 ]; then
498        report_task_end
499     else
500        report_error
501     fi
502
503     cleanup
504 }
505
506 check_if_excluded() {
507     local name exclude
508     name="$(tr [a-z] [A-Z] <<< ${PROGNAME:2})"
509     exclude="SNF_IMAGE_PROPERTY_EXCLUDE_TASK_${name}"
510     if [ -n "${!exclude}" ]; then
511         warn "Task ${PROGNAME:2} was excluded and will not run."
512         exit 0
513     fi
514
515     return 0
516 }
517
518
519 return_success() {
520     send_result_${HYPERVISOR} "SUCCESS"
521 }
522
523 trap cleanup EXIT
524 set -o pipefail
525
526 STDERR_FILE=$(mktemp)
527 add_cleanup rm -f "$STDERR_FILE"
528 exec 2> >(tee -a "$STDERR_FILE" >&2)
529
530 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :