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