Cleanup disklabel.py
[snf-image] / snf-image-helper / common.sh.in
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 GROWFS_UFS=growfs.ufs
36 DUMPFS_UFS=dumpfs.ufs
37 GROWFS_OPENBSD=growfs.openbsd
38 DUMPFS_OPENBSD=dumpfs.openbsd
39 DATE="date -u" # Time in UTC
40 EATMYDATA=eatmydata
41 MOUNT="mount -n"
42
43 CLEANUP=( )
44 ERRORS=( )
45 WARNINGS=( )
46
47 MSG_TYPE_TASK_START="TASK_START"
48 MSG_TYPE_TASK_END="TASK_END"
49
50 STDERR_LINE_SIZE=10
51
52 add_cleanup() {
53     local cmd=""
54     for arg; do cmd+=$(printf "%q " "$arg"); done
55     CLEANUP+=("$cmd")
56 }
57
58 close_fd() {
59     local fd=$1
60
61     exec {fd}>&-
62 }
63
64 send_result_kvm() {
65     echo "$@" > /dev/ttyS1
66 }
67
68 send_monitor_message_kvm() {
69     echo "$@" > /dev/ttyS2
70 }
71
72 send_result_xen() {
73     xenstore-write /local/domain/0/snf-image-helper/$DOMID "$*"
74 }
75
76 send_monitor_message_xen() {
77     #Broadcast the message
78     echo "$@" | socat "STDIO" "UDP-DATAGRAM:${BROADCAST}:${MONITOR_PORT},broadcast"
79 }
80
81 prepare_helper() {
82         local cmdline item key val hypervisor domid
83
84         read -a cmdline  < /proc/cmdline
85         for item in "${cmdline[@]}"; do
86             key=$(cut -d= -f1 <<< "$item")
87             val=$(cut -d= -f2 <<< "$item")
88             if [ "$key" = "hypervisor" ]; then
89                 hypervisor="$val"
90             fi
91             if [ "$key" = "rules_dev" ]; then
92                 export RULES_DEV="$val"
93             fi
94             if [ "$key" = "helper_ip" ]; then
95                 export IP="$val"
96                 export NETWORK="$IP/24"
97                 export BROADCAST="${IP%.*}.255"
98             fi
99             if [ "$key" = "monitor_port" ]; then
100                 export MONITOR_PORT="$val"
101             fi
102         done
103
104     case "$hypervisor" in
105     kvm)
106         HYPERVISOR=kvm
107         ;;
108     xen-hvm|xen-pvm)
109         if [ -z "$IP" ]; then
110             echo "ERROR: \`helper_ip' not defined or empty" >&2
111             exit 1
112         fi
113         if [ -z "$MONITOR_PORT" ]; then
114             echo "ERROR: \`monitor_port' not defined or empty" >&2
115             exit 1
116         fi
117         $MOUNT -t xenfs xenfs /proc/xen
118         ip addr add "$NETWORK" dev eth0
119         ip link set eth0 up
120         ip route add default dev eth0
121         export DOMID=$(xenstore-read domid)
122         HYPERVISOR=xen
123         ;;
124     *)
125         echo "ERROR: Unknown hypervisor: \`$hypervisor'" >&2
126         exit 1
127         ;;
128     esac
129
130     export HYPERVISOR
131 }
132
133 report_error() {
134     msg=""
135     if [ ${#ERRORS[*]} -eq 0 ]; then
136         # No error message. Print stderr
137         local lines stderr
138         stderr="$(tail --lines=${STDERR_LINE_SIZE} "$STDERR_FILE")"
139         lines=$(wc -l <<< "$stderr")
140         msg="STDERR:${lines}:$stderr"
141     else
142         for line in "${ERRORS[@]}"; do
143             msg+="ERROR:$line"$'\n'
144         done
145     fi
146
147     send_monitor_message_${HYPERVISOR} "$msg"
148 }
149
150 log_error() {
151     ERRORS+=("$*")
152
153     send_result_${HYPERVISOR} "ERROR: $@"
154
155     # Use return instead of exit. The set -x options will terminate the script
156     # but will also trigger ERR traps if defined.
157     return 1
158 }
159
160 warn() {
161     echo "Warning: $@" >&2
162     send_monitor_message_${HYPERVISOR} "WARNING: $@"
163 }
164
165 report_task_start() {
166     send_monitor_message_${HYPERVISOR} "$MSG_TYPE_TASK_START:${PROGNAME:2}"
167 }
168
169 report_task_end() {
170     send_monitor_message_${HYPERVISOR} "$MSG_TYPE_TASK_END:${PROGNAME:2}"
171 }
172
173 system_poweroff() {
174     while [ 1 ]; do
175         # Credits to psomas@grnet.gr for this ...
176         echo o > /proc/sysrq-trigger
177         sleep 1
178     done
179 }
180
181 get_base_distro() {
182     local root_dir=$1
183
184     if [ -e "$root_dir/etc/debian_version" ]; then
185         echo "debian"
186     elif [ -e "$root_dir/etc/redhat-release" ]; then
187         echo "redhat"
188     elif [ -e "$root_dir/etc/slackware-version" ]; then
189         echo "slackware"
190     elif [ -e "$root_dir/etc/SuSE-release" ]; then
191         echo "suse"
192     elif [ -e "$root_dir/etc/gentoo-release" ]; then
193         echo "gentoo"
194     elif [ -e "$root_dir/etc/arch-release" ]; then
195         echo "arch"
196     elif [ -e "$root_dir/etc/freebsd-update.conf" ]; then
197         echo "freebsd"
198     elif [ -e "$root_dir/etc/release" ]; then
199         if grep -i netbsd "$root_dir/etc/release" &> /dev/null; then
200             echo "netbsd"
201         else
202             warn "Unknown Unix flavor."
203         fi
204     elif [ -e "$root_dir/etc/motd" ]; then
205         if grep -i ^openbsd <(head -1 "$root_dir/etc/motd") &> /dev/null; then
206             echo "openbsd"
207         else
208             warn "Unknown Unix flavor"
209         fi
210     else
211         warn "Unknown base distro."
212     fi
213 }
214
215 get_distro() {
216     local root_dir distro
217     root_dir=$1
218
219     if [ -e "$root_dir/etc/debian_version" ]; then
220         distro="debian"
221         if [ -e ${root_dir}/etc/lsb-release ]; then
222             ID=$(grep ^DISTRIB_ID= ${root_dir}/etc/lsb-release | cut -d= -f2)
223             if [ "x$ID" = "xUbuntu" ]; then
224                 distro="ubuntu"
225             fi
226         fi
227         echo "$distro"
228     elif [ -e "$root_dir/etc/fedora-release" ]; then
229         echo "fedora"
230     elif [ -e "$root_dir/etc/centos-release" ]; then
231         echo "centos"
232     elif [ -e "$root_dir/etc/redhat-release" ]; then
233         echo "redhat"
234     elif [ -e "$root_dir/etc/slackware-version" ]; then
235         echo "slackware"
236     elif [ -e "$root_dir/etc/SuSE-release" ]; then
237         echo "suse"
238     elif [ -e "$root_dir/etc/gentoo-release" ]; then
239         echo "gentoo"
240     elif [ -e "$root_dir/etc/arch-release" ]; then
241         echo "arch"
242     elif [ -e "$root_dir/etc/freebsd-update.conf" ]; then
243         echo "freebsd"
244     elif [ -e "$root_dir/etc/release" ]; then
245         if grep -in netbsd "$root_dir/etc/release" &> /dev/null; then
246             echo "netbsd"
247         else
248             warn "Unknown Unix flavor"
249         fi
250     elif [ -e "$root_dir/etc/motd" ]; then
251         if grep -i ^openbsd <(head -1 "$root_dir/etc/motd") &> /dev/null; then
252             echo "openbsd"
253         else
254             warn "Unknown Unix flavor"
255         fi
256     else
257         warn "Unknown distro."
258     fi
259 }
260
261 get_partition_table() {
262     local dev output
263     dev="$1"
264     # If the partition table is gpt then parted will raise an error if the
265     # secondary gpt is not it the end of the disk, and a warning that has to
266     # do with the "Last Usable LBA" entry in gpt.
267     if ! output="$("$PARTED" -s -m "$dev" unit s print | grep -E -v "^(Warning|Error): ")"; then
268         log_error "Unable to read partition table for device \`${dev}'. The image seems corrupted."
269     fi
270
271     echo "$output"
272 }
273
274 get_partition_table_type() {
275     local ptable dev field
276     ptable="$1"
277
278     dev="$(sed -n 2p <<< "$ptable")"
279     IFS=':' read -ra field <<< "$dev"
280
281     echo "${field[5]}"
282 }
283
284 get_partition_count() {
285     local ptable="$1"
286
287     expr $(echo "$ptable" | wc -l) - 2
288 }
289
290 get_partition_by_num() {
291     local ptable="$1"
292     local id="$2"
293
294     grep "^$id:" <<< "$ptable"
295 }
296
297 get_last_partition() {
298     local ptable="$1"
299
300     echo "$ptable" | tail -1
301 }
302
303 is_extended_partition() {
304     local dev="$1"
305     local part_num="$2"
306
307     id=$($SFDISK --force --print-id "$dev" "$part_num")
308     if [ "$id" = "5" -o "$id" = "f" ]; then
309         echo "yes"
310     else
311         echo "no"
312     fi
313 }
314
315 get_extended_partition() {
316     local ptable dev
317     ptable="$1"
318     dev="$(echo "$ptable" | sed -n 2p | cut -d':' -f1)"
319
320     tail -n +3 <<< "$ptable" | while read line; do
321         part_num=$(cut -d':' -f1 <<< "$line")
322         if [ $(is_extended_partition "$dev" "$part_num") == "yes" ]; then
323             echo "$line"
324             return 0
325         fi
326     done
327     echo ""
328 }
329
330 get_logical_partitions() {
331     local ptable part_num
332     ptable="$1"
333
334     tail -n +3 <<< "$ptable" | while read line; do
335         part_num=$(cut -d':' -f1 <<< "$line")
336         if [ $part_num -ge 5 ]; then
337             echo "$line"
338         fi
339     done
340
341     return 0
342 }
343
344 get_last_primary_partition() {
345     local ptable dev output
346     ptable="$1"
347     dev=$(echo "ptable" | sed -n 2p | cut -d':' -f1)
348
349     for i in 4 3 2 1; do
350         if output=$(grep "^$i:" <<< "$ptable"); then
351             echo "$output"
352             return 0
353         fi
354     done
355     echo ""
356 }
357
358 get_partition_to_resize() {
359     local dev table table_type last_part last_part_num extended last_primary \
360         ext_num prim_num
361     dev="$1"
362
363     table=$(get_partition_table "$dev")
364     if [ -z "$table" ]; then
365         return 0
366     fi
367
368     if [ $(get_partition_count "$table") -eq 0 ]; then
369         return 0
370     fi
371
372     table_type=$(get_partition_table_type "$table")
373     last_part=$(get_last_partition "$table")
374     last_part_num=$(cut -d: -f1 <<< "$last_part")
375
376     if [ "$table_type" == "msdos" -a $last_part_num -gt 4 ]; then
377         extended=$(get_extended_partition "$table")
378         last_primary=$(get_last_primary_partition "$table")
379         ext_num=$(cut -d: -f1 <<< "$extended")
380         last_prim_num=$(cut -d: -f1 <<< "$last_primary")
381
382         if [ "$ext_num" != "$last_prim_num" ]; then
383             echo "$last_prim_num"
384         else
385             echo "$last_part_num"
386         fi
387     else
388         echo "$last_part_num"
389     fi
390 }
391
392 create_partition() {
393     local device="$1"
394     local part="$2"
395     local ptype="$3"
396
397     local fields=()
398     IFS=":;" read -ra fields <<< "$part"
399     local id="${fields[0]}"
400     local start="${fields[1]}"
401     local end="${fields[2]}"
402     local size="${fields[3]}"
403     local fs="${fields[4]}"
404     local name="${fields[5]}"
405     local flags="${fields[6]//,/ }"
406
407     if [ "$ptype" = "primary" -o "$ptype" = "logical" -o "$ptype" = "extended" ]; then
408         $PARTED -s -m -- $device mkpart "$ptype" $fs "$start" "$end"
409         for flag in $flags; do
410             $PARTED -s -m $device set "$id" "$flag" on
411         done
412     else
413         # For gpt
414         start=${start:0:${#start}-1} # remove the s at the end
415         end=${end:0:${#end}-1} # remove the s at the end
416         $SGDISK -n "$id":"$start":"$end" -t "$id":"$ptype" "$device"
417     fi
418 }
419
420 enlarge_partition() {
421     local device part ptype new_end fields new_part table logical id
422     device="$1"
423     part="$2"
424     ptype="$3"
425     new_end="$4"
426
427     if [ -z "$new_end" ]; then
428         new_end=$(cut -d: -f 3 <<< "$(get_last_free_sector "$device")")
429     fi
430
431     fields=()
432     IFS=":;" read -ra fields <<< "$part"
433     fields[2]="$new_end"
434
435     new_part=""
436     for ((i = 0; i < ${#fields[*]}; i = i + 1)); do
437         new_part="$new_part":"${fields[$i]}"
438     done
439     new_part=${new_part:1}
440
441     # If this is an extended partition, removing it will also remove the
442     # logical partitions it contains. We need to save them for later.
443     if [ "$ptype" = "extended" ]; then
444         table="$(get_partition_table "$device")"
445         logical="$(get_logical_partitions "$table")"
446     fi
447
448     id=${fields[0]}
449     $PARTED -s -m "$device" rm "$id"
450     create_partition "$device" "$new_part" "$ptype"
451
452     if [ "$ptype" = "extended" ]; then
453         # Recreate logical partitions
454         echo "$logical" | while read logical_part; do
455             create_partition "$device" "$logical_part" "logical"
456         done
457     fi
458 }
459
460 get_last_free_sector() {
461     local dev unit last_line ptype
462     dev="$1"
463     unit="$2"
464
465     if [ -n "$unit" ]; then
466         unit="unit $unit"
467     fi
468
469     last_line="$($PARTED -s -m "$dev" "$unit" print free | tail -1)"
470     ptype="$(cut -d: -f 5 <<< "$last_line")"
471
472     if [ "$ptype" = "free;" ]; then
473         echo "$last_line"
474     fi
475 }
476
477 get_unattend() {
478     local target exists
479     target="$1"
480
481     # Workaround to search for $target/Unattend.xml in an case insensitive way.
482     exists=$(find "$target"/ -maxdepth 1 -iname unattend.xml)
483     if [ $(wc -l <<< "$exists") -gt 1 ]; then
484         log_error "Found multiple Unattend.xml files in the image:" $exists
485     fi
486
487     echo "$exists"
488 }
489
490 disklabel2linux() {
491     local partition i p
492     partition="$1"
493
494     i=4
495     # Partition 'c' traditionally used to describe the entire disk is not
496     # mapped to /dev/sda7 by the kernel
497     for p in a b {d..p}; do
498         let i++
499         if [ "$p" = "$partition" ]; then
500            echo "$i"
501            return 0
502         fi
503     done
504
505     log_error "Invalid BSD partition label: \`$partition'"
506 }
507
508 mount_all() {
509     local osfamily target fs device fstab entry duid opts types num
510     osfamily="$1"
511     device="$2"
512     target="$3"
513
514     case "$osfamily" in
515     linux)
516         fs="ext[234]|msdos|vfat|ntfs"
517         ;;
518     freebsd)
519         fs="ufs|msdosfs|ntfs"
520         ;;
521     openbsd)
522         fs="ffs|msdos|ntfs|ext2fs"
523         ;;
524     netbsd)
525         fs="ffs|ufs|msdos|ext2fs|ntfs"
526         ;;
527     *)
528         log_error "Unsupported osfamily: \`$osfamily'"
529         ;;
530     esac
531
532     fstab="$(grep -v ^\# "${target}/etc/fstab" | awk "{ if (match(\$3, \"$fs\")) { print \$2,\$1,\$3 } }" | sort -bd)"
533     # <mpoint> <device> <fs>
534     while read -ra entry; do
535         # Skip root. It is already mounted
536         if [ "${entry[0]}" = "/" ]; then
537             continue
538         fi
539
540         opts="rw"
541         types="auto"
542
543         if [ "$osfamily" = linux ]; then
544             # Linux persistent block device naming
545             if [[ ${entry[1]} =~ ^(LABEL|UUID)= ]]; then
546                 entry[1]=$(findfs "${entry[1]}")
547             else
548                 log_error "fstab contains non-persistent block device names"
549             fi
550         else
551             if [[ "$osfamily" =~ ^(open|net)bsd$ ]]; then
552                 # OpenBSD DUIDs device naming
553                 if [[ "${entry[1]}" =~ ^[a-f0-9]{16}\.[a-p]$ ]]; then
554                     duid="$(@scriptsdir@/disklabel.py --get-duid "$device")"
555
556                     if [[ ! "${entry[1]}" =~ ^$duid ]]; then
557                         warn "fstab refers to unknown DUID: \`$duid'"
558                         continue
559                     fi
560                 fi
561                 num="$(disklabel2linux "${entry[1]: -1}")"
562                 if [ "${entry[2]}" = ffs -o "$entry[2]" = ufs ]; then
563                     types="ufs"
564                     opts="ufstype=44bsd,rw"
565                 fi
566             else # FreeBSD
567                 # We do not support FreeBSD labels for now
568                 if [[ "${entry[1]}" =~ ^/dev/(ufs|label)/ ]]; then
569                     log_error "fstab contains FreeBSD labels. We currently don't support them"
570                 fi
571                 num="${entry[1]: -1}"
572                 if [ "${entry[2]}" = ufs ]; then
573                     types="ufs"
574                     opts="ufstype=ufs2,rw"
575                 fi
576             fi
577             entry[1]="${device}${num}"
578         fi
579
580         $MOUNT -t "$types" -o "$opts" "${entry[1]}" "${target}${entry[0]}"
581         # In many cases when you try to mount a UFS file system read-write, the
582         # mount command returns SUCCESS and a message like this gets printed:
583         #
584         #   mount: warning: <target> seems to be mounted read-only.
585         #
586         # remounting does the trick
587         if [ "$types" = ufs ]; then
588             $MOUNT -o remount,rw "${entry[1]}"
589         fi
590
591     done <<< "$fstab"
592 }
593
594 umount_all() {
595     local target mpoints
596     target="$1"
597
598     # Unmount file systems mounted under directory `target'
599     mpoints="$({ awk "{ if (match(\$2, \"^$target\")) { print \$2 } }" < /proc/mounts; } | sort -rbd | uniq)"
600
601     for mpoint in $mpoints; do
602         umount $mpoint
603     done
604 }
605
606 get_ufstype() {
607     local device ufs
608
609     device="$1"
610     ufs="$($DUMPFS_UFS "$device" | head -1 | awk -F "[()]" '{ for (i=2; i<NF; i+=2) print $i }')"
611
612     case "$ufs" in
613         UFS1)
614             echo 44bsd
615             ;;
616         UFS2)
617             echo ufs2
618             ;;
619         *)
620             log_error "Unsupported UFS type: \`$ufs' in device $device"
621             echo ""
622             ;;
623     esac
624 }
625
626 cleanup() {
627     # if something fails here, it shouldn't call cleanup again...
628     trap - EXIT
629
630     if [ ${#CLEANUP[*]} -gt 0 ]; then
631         LAST_ELEMENT=$((${#CLEANUP[*]}-1))
632         REVERSE_INDEXES=$(seq ${LAST_ELEMENT} -1 0)
633         for i in $REVERSE_INDEXES; do
634             # If something fails here, it's better to retry it for a few times
635             # before we give up with an error. This is needed for kpartx when
636             # dealing with ntfs partitions mounted through fuse. umount is not
637             # synchronous and may return while the partition is still busy. A
638             # premature attempt to delete partition mappings through kpartx on
639             # a device that hosts previously mounted ntfs partition may fail
640             # with a `device-mapper: remove ioctl failed: Device or resource
641             # busy' error. A sensible workaround for this is to wait for a
642             # while and then try again.
643             local cmd=${CLEANUP[$i]}
644             $cmd || for interval in 0.25 0.5 1 2 4; do
645             echo "Command $cmd failed!"
646             echo "I'll wait for $interval secs and will retry..."
647             sleep $interval
648             $cmd && break
649         done
650         if [ "$?" != "0" ]; then
651             echo "Giving Up..."
652             exit 1;
653         fi
654     done
655   fi
656 }
657
658 task_cleanup() {
659     local rc=$?
660
661     if [ $rc -eq 0 ]; then
662        report_task_end
663     else
664        report_error
665     fi
666
667     cleanup
668 }
669
670 check_if_excluded() {
671     local name exclude
672     name="$(tr [a-z] [A-Z] <<< ${PROGNAME:2})"
673     exclude="SNF_IMAGE_PROPERTY_EXCLUDE_TASK_${name}"
674     if [ -n "${!exclude}" ]; then
675         warn "Task ${PROGNAME:2} was excluded and will not run."
676         exit 0
677     fi
678
679     return 0
680 }
681
682 return_success() {
683     send_result_${HYPERVISOR} "SUCCESS"
684 }
685
686 trap cleanup EXIT
687 set -o pipefail
688
689 STDERR_FILE=$(mktemp)
690 add_cleanup rm -f "$STDERR_FILE"
691 exec 2> >(tee -a "$STDERR_FILE" >&2)
692
693 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :