Merge branch 'master' into xen
[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 DATE="date -u" # Time in UTC
35 EATMYDATA=eatmydata
36
37 CLEANUP=( )
38 ERRORS=( )
39 WARNINGS=( )
40
41 MSG_TYPE_TASK_START="TASK_START"
42 MSG_TYPE_TASK_END="TASK_END"
43
44 STDERR_LINE_SIZE=10
45
46 add_cleanup() {
47     local cmd=""
48     for arg; do cmd+=$(printf "%q " "$arg"); done
49     CLEANUP+=("$cmd")
50 }
51
52 close_fd() {
53     local fd=$1
54
55     exec {fd}>&-
56 }
57
58 prepare_helper() {
59         local cmdline item key val hypervisor domid
60
61         read -a cmdline  < /proc/cmdline
62         for item in "${cmdline[@]}"; do
63         key=$(cut -d= -f1 <<< "$item")
64         if [ "$key" = "hypervisor" ]; then
65             val=$(cut -d= -f2 <<< "$item")
66             hypervisor="$val"
67         fi
68         done
69
70     case "$hypervisor" in
71     kvm)
72         FLOPPY_DEV=/dev/fd0
73         exec {RESULT_FD}> /dev/ttyS1
74         add_cleanup close_fd ${RESULT_FD}
75         exec {MONITOR_FD}> /dev/ttyS2
76         add_cleanup close_fd ${MONITOR_FD}
77         ;;
78     xen-hvm|xen-pvm)
79                 mount -t xenfs xenfs /proc/xen
80                 iptables -P OUTPUT DROP
81                 ip6tables -P OUTPUT DROP
82                 ip link set eth0 up
83         FLOPPY_DEV=/dev/xvdc
84         domid=$(xenstore-read domid)
85         exec {RESULT_FD}> >(xargs -l1 xenstore-write /local/domain/0/snf-image-helper/$domid)
86         add_cleanup close_fd ${RESULT_FD}
87         exec {MONITOR_FD}> >(socat STDIN INTERFACE:eth0)
88         add_cleanup close_fd ${MONITOR_FD}
89         ;;
90     *)
91         echo "ERROR: Unknown hypervisor: \`$hypervisor'" >&2
92         exit 1
93         ;;
94     esac
95
96     export RESULT_FD MONITOR_FD
97 }
98
99 report_error() {
100     if [ ${#ERRORS[*]} -eq 0 ]; then
101         # No error message. Print stderr
102         local lines
103         lines=$(tail --lines=${STDERR_LINE_SIZE} "$STDERR_FILE" | wc -l)
104         echo -n "STDERR:${lines}:" >&${MONITOR_FD}
105         tail --lines=$lines  "$STDERR_FILE" >&${MONITOR_FD}
106     else
107         for line in "${ERRORS[@]}"; do
108             echo "ERROR:$line" >&${MONITOR_FD}
109         done
110     fi
111 }
112
113 log_error() {
114     ERRORS+=("$*")
115     echo "ERROR: $@" >&2
116     echo "ERROR: $@" >&${MONITOR_FD}
117
118     echo "ERROR" >&${RUSULT_FD}
119
120     # Use return instead of exit. The set -x options will terminate the script
121     # but will also trigger ERR traps if defined.
122     return 1
123 }
124
125 warn() {
126     echo "Warning: $@" >&2
127         echo "WARNING: $@" >&${MONITOR_FD}
128 }
129
130 report_task_start() {
131     echo "$MSG_TYPE_TASK_START:${PROGNAME:2}" >&${MONITOR_FD}
132 }
133
134 report_task_end() {
135     echo "$MSG_TYPE_TASK_END:${PROGNAME:2}" >&${MONITOR_FD}
136 }
137
138 system_poweroff() {
139     while [ 1 ]; do
140         # Credits to psomas@grnet.gr for this ...
141         echo o > /proc/sysrq-trigger
142         sleep 1
143     done
144 }
145
146 get_base_distro() {
147     local root_dir=$1
148
149     if [ -e "$root_dir/etc/debian_version" ]; then
150         echo "debian"
151     elif [ -e "$root_dir/etc/redhat-release" ]; then
152         echo "redhat"
153     elif [ -e "$root_dir/etc/slackware-version" ]; then
154         echo "slackware"
155     elif [ -e "$root_dir/etc/SuSE-release" ]; then
156         echo "suse"
157     elif [ -e "$root_dir/etc/gentoo-release" ]; then
158         echo "gentoo"
159     elif [ -e "$root_dir/etc/arch-release" ]; then
160         echo "arch"
161     else
162         warn "Unknown base distro."
163     fi
164 }
165
166 get_distro() {
167     local root_dir distro
168     root_dir=$1
169
170     if [ -e "$root_dir/etc/debian_version" ]; then
171         distro="debian"
172         if [ -e ${root_dir}/etc/lsb-release ]; then
173             ID=$(grep ^DISTRIB_ID= ${root_dir}/etc/lsb-release | cut -d= -f2)
174             if [ "x$ID" = "xUbuntu" ]; then
175                 distro="ubuntu"
176             fi
177         fi
178         echo "$distro"
179     elif [ -e "$root_dir/etc/fedora-release" ]; then
180         echo "fedora"
181     elif [ -e "$root_dir/etc/centos-release" ]; then
182         echo "centos"
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 distro."
195     fi
196 }
197
198
199 get_partition_table() {
200     local dev output
201     dev="$1"
202     # If the partition table is gpt then parted will raise an error if the
203     # secondary gpt is not it the end of the disk, and a warning that has to
204     # do with the "Last Usable LBA" entry in gpt.
205     if ! output="$("$PARTED" -s -m "$dev" unit s print | grep -E -v "^(Warning|Error): ")"; then
206         log_error "Unable to read partition table for device \`${dev}'. The image seems corrupted."
207     fi
208
209     echo "$output"
210 }
211
212 get_partition_table_type() {
213     local ptable dev field
214     ptable="$1"
215
216     dev="$(sed -n 2p <<< "$ptable")"
217     IFS=':' read -ra field <<< "$dev"
218
219     echo "${field[5]}"
220 }
221
222 get_partition_count() {
223     local ptable="$1"
224
225     expr $(echo "$ptable" | wc -l) - 2
226 }
227
228 get_partition_by_num() {
229     local ptable="$1"
230     local id="$2"
231
232     grep "^$id:" <<< "$ptable"
233 }
234
235 get_last_partition() {
236     local ptable="$1"
237
238     echo "$ptable" | tail -1
239 }
240
241 is_extended_partition() {
242     local dev="$1"
243     local part_num="$2"
244
245     id=$($SFDISK --print-id "$dev" "$part_num")
246     if [ "$id" = "5" -o "$id" = "f" ]; then
247         echo "yes"
248     else
249         echo "no"
250     fi
251 }
252
253 get_extended_partition() {
254     local ptable dev
255     ptable="$1"
256     dev="$(echo "$ptable" | sed -n 2p | cut -d':' -f1)"
257
258     tail -n +3 <<< "$ptable" | while read line; do
259         part_num=$(cut -d':' -f1 <<< "$line")
260         if [ $(is_extended_partition "$dev" "$part_num") == "yes" ]; then
261             echo "$line"
262             return 0
263         fi
264     done
265     echo ""
266 }
267
268 get_logical_partitions() {
269     local ptable part_num
270     ptable="$1"
271
272     tail -n +3 <<< "$ptable" | while read line; do
273         part_num=$(cut -d':' -f1 <<< "$line")
274         if [ $part_num -ge 5 ]; then
275             echo "$line"
276         fi
277     done
278
279     return 0
280 }
281
282 get_last_primary_partition() {
283     local ptable dev output
284     ptable="$1"
285     dev=$(echo "ptable" | sed -n 2p | cut -d':' -f1)
286
287     for i in 4 3 2 1; do
288         if output=$(grep "^$i:" <<< "$ptable"); then
289             echo "$output"
290             return 0
291         fi
292     done
293     echo ""
294 }
295
296 get_partition_to_resize() {
297     local dev table table_type last_part last_part_num extended last_primary \
298         ext_num prim_num
299     dev="$1"
300
301     table=$(get_partition_table "$dev")
302
303     if [ $(get_partition_count "$table") -eq 0 ]; then
304         return 0
305     fi
306
307     table_type=$(get_partition_table_type "$table")
308     last_part=$(get_last_partition "$table")
309     last_part_num=$(cut -d: -f1 <<< "$last_part")
310
311     if [ "$table_type" == "msdos" -a $last_part_num -gt 4 ]; then
312         extended=$(get_extended_partition "$table")
313         last_primary=$(get_last_primary_partition "$table")
314         ext_num=$(cut -d: -f1 <<< "$extended")
315         last_prim_num=$(cut -d: -f1 <<< "$last_primary")
316
317         if [ "$ext_num" != "$last_prim_num" ]; then
318             echo "$last_prim_num"
319         else
320             echo "$last_part_num"
321         fi
322     else
323         echo "$last_part_num"
324     fi
325 }
326
327 create_partition() {
328     local device="$1"
329     local part="$2"
330     local ptype="$3"
331
332     local fields=()
333     IFS=":;" read -ra fields <<< "$part"
334     local id="${fields[0]}"
335     local start="${fields[1]}"
336     local end="${fields[2]}"
337     local size="${fields[3]}"
338     local fs="${fields[4]}"
339     local name="${fields[5]}"
340     local flags="${fields[6]//,/ }"
341
342     $PARTED -s -m -- $device mkpart "$ptype" $fs "$start" "$end"
343     for flag in $flags; do
344         $PARTED -s -m $device set "$id" "$flag" on
345     done
346 }
347
348 enlarge_partition() {
349     local device part ptype new_end fields new_part table logical id
350     device="$1"
351     part="$2"
352     ptype="$3"
353     new_end="$4"
354
355     if [ -z "$new_end" ]; then
356         new_end=$(cut -d: -f 3 <<< "$(get_last_free_sector "$device")")
357     fi
358
359     fields=()
360     IFS=":;" read -ra fields <<< "$part"
361     fields[2]="$new_end"
362
363     new_part=""
364     for ((i = 0; i < ${#fields[*]}; i = i + 1)); do
365         new_part="$new_part":"${fields[$i]}"
366     done
367     new_part=${new_part:1}
368
369     # If this is an extended partition, removing it will also remove the
370     # logical partitions it contains. We need to save them for later.
371     if [ "$ptype" = "extended" ]; then
372         table="$(get_partition_table "$device")"
373         logical="$(get_logical_partitions "$table")"
374     fi
375
376     id=${fields[0]}
377     $PARTED -s -m "$device" rm "$id"
378     create_partition "$device" "$new_part" "$ptype"
379
380     if [ "$ptype" = "extended" ]; then
381         # Recreate logical partitions
382         echo "$logical" | while read logical_part; do
383             create_partition "$device" "$logical_part" "logical"
384         done
385     fi
386 }
387
388 get_last_free_sector() {
389     local dev unit last_line ptype
390     dev="$1"
391     unit="$2"
392
393     if [ -n "$unit" ]; then
394         unit="unit $unit"
395     fi
396
397     last_line="$($PARTED -s -m "$dev" "$unit" print free | tail -1)"
398     ptype="$(cut -d: -f 5 <<< "$last_line")"
399
400     if [ "$ptype" = "free;" ]; then
401         echo "$last_line"
402     fi
403 }
404
405 get_unattend() {
406     local target exists
407     target="$1"
408
409     # Workaround to search for $target/Unattend.xml in an case insensitive way.
410     exists=$(find "$target"/ -maxdepth 1 -iname unattend.xml)
411     if [ $(wc -l <<< "$exists") -gt 1 ]; then
412         log_error "Found multiple Unattend.xml files in the image:" $exists
413     fi
414
415     echo "$exists"
416 }
417
418 umount_all() {
419     local target mpoints
420     target="$1"
421
422     # Unmount file systems mounted under directory `target'
423     mpoints="$({ awk "{ if (match(\$2, \"^$target\")) { print \$2 } }" < /proc/mounts; } | sort -rbd | uniq)"
424
425     for mpoint in $mpoints; do
426         umount $mpoint
427     done
428 }
429
430 cleanup() {
431     # if something fails here, it shouldn't call cleanup again...
432     trap - EXIT
433
434     if [ ${#CLEANUP[*]} -gt 0 ]; then
435         LAST_ELEMENT=$((${#CLEANUP[*]}-1))
436         REVERSE_INDEXES=$(seq ${LAST_ELEMENT} -1 0)
437         for i in $REVERSE_INDEXES; do
438             # If something fails here, it's better to retry it for a few times
439             # before we give up with an error. This is needed for kpartx when
440             # dealing with ntfs partitions mounted through fuse. umount is not
441             # synchronous and may return while the partition is still busy. A
442             # premature attempt to delete partition mappings through kpartx on
443             # a device that hosts previously mounted ntfs partition may fail
444             # with a `device-mapper: remove ioctl failed: Device or resource
445             # busy' error. A sensible workaround for this is to wait for a
446             # while and then try again.
447             local cmd=${CLEANUP[$i]}
448             $cmd || for interval in 0.25 0.5 1 2 4; do
449             echo "Command $cmd failed!"
450             echo "I'll wait for $interval secs and will retry..."
451             sleep $interval
452             $cmd && break
453         done
454         if [ "$?" != "0" ]; then
455             echo "Giving Up..."
456             exit 1;
457         fi
458     done
459   fi
460 }
461
462 task_cleanup() {
463     local rc=$?
464
465     if [ $rc -eq 0 ]; then
466        report_task_end
467     else
468        report_error
469     fi
470
471     cleanup
472 }
473
474 check_if_excluded() {
475     local name exclude
476     name="$(tr [a-z] [A-Z] <<< ${PROGNAME:2})"
477     exclude="SNF_IMAGE_PROPERTY_EXCLUDE_TASK_${name}"
478     if [ -n "${!exclude}" ]; then
479         warn "Task ${PROGNAME:2} was excluded and will not run."
480         exit 0
481     fi
482
483     return 0
484 }
485
486
487 return_success() {
488     echo SUCCESS >&${RESULT_FD}
489 }
490
491 trap cleanup EXIT
492 set -o pipefail
493
494 STDERR_FILE=$(mktemp)
495 add_cleanup rm -f "$STDERR_FILE"
496 exec 2> >(tee -a "$STDERR_FILE" >&2)
497
498 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :