Merge ~smoser/ubuntu/+source/open-iscsi:bug/1785108-xenial-net-interface-handler-runs-always into ubuntu/+source/open-iscsi:ubuntu/xenial-devel

Proposed by Scott Moser
Status: Merged
Merged at revision: ebcb7919e134ee84ce8049788e483b468adb5af1
Proposed branch: ~smoser/ubuntu/+source/open-iscsi:bug/1785108-xenial-net-interface-handler-runs-always
Merge into: ubuntu/+source/open-iscsi:ubuntu/xenial-devel
Diff against target: 2137 lines (+1518/-298)
9 files modified
debian/changelog (+8/-0)
debian/net-interface-handler (+18/-4)
debian/tests/README-boot-test.md (+139/-0)
debian/tests/control (+6/-2)
debian/tests/get-image (+123/-31)
debian/tests/patch-image (+288/-0)
debian/tests/test-open-iscsi.py (+191/-75)
debian/tests/tgt-boot-test (+494/-159)
debian/tests/xkvm (+251/-27)
Reviewer Review Type Date Requested Status
Francis Ginther Pending
git-ubuntu developers Pending
Review via email: mp+352216@code.launchpad.net

Commit message

net-interface-handler: Apply changes only for the iscsi-root interface.

net-interface-handler was effectively bringing "up" the iscsi-root
interface for all network device adds and it "down" for all the removes.
The quotes are used above because the design point of the script is to
basically fake ifupdown into believing the event already occurred.

The change here is to only operate when the INTERFACE provided by
udev is the iscsi root device.

I've also pulled back the functioning dep8 tests from cosmic.
This allows us to actually have a test that shows the failure and
passes with the changes.

LP: #1785108

Description of the change

see commit message

To post a comment you must log in.
03dc4a4... by Scott Moser

releasing package open-iscsi version 2.0.873+git0.3b4b4500-14ubuntu3.5

f5cfc6b... by Scott Moser

copy debian/tests cloud image test back from cosmic.

The open-iscsi boot test has been quite problematic in the past.
There are a number of fixes since it was last touched in 16.04 and
it is now running consistently in cosmic.

So bring back the tests and tools from cosmic.

beeb3a5... by Scott Moser

debian/tests/test-open-iscsi.py: attempt to trigger 1785108

Without the fix applied, this would cause test failure.

710ebad... by Scott Moser

update changelog pull test back

7a7fad1... by Scott Moser

ammend tests to have depends open-iscsi

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index b23a821..e6dbe18 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,11 @@
6+open-iscsi (2.0.873+git0.3b4b4500-14ubuntu3.5) xenial; urgency=medium
7+
8+ * d/tests: pull back cloud image test updates from cosmic.
9+ * d/net-interface-handler: Apply changes only for the iscsi-root
10+ (LP: #1785108)
11+
12+ -- Scott Moser <smoser@ubuntu.com> Thu, 02 Aug 2018 15:09:53 -0400
13+
14 open-iscsi (2.0.873+git0.3b4b4500-14ubuntu3.4) xenial; urgency=medium
15
16 * d/p/08_Parse-origin-value-from-iBFT.patch
17diff --git a/debian/net-interface-handler b/debian/net-interface-handler
18index 7717f61..15d35c6 100755
19--- a/debian/net-interface-handler
20+++ b/debian/net-interface-handler
21@@ -10,12 +10,23 @@
22 # ifupdown appears to have no way to do this without also running
23 # /etc/network/*.d/ scripts.
24
25+assert_interface() {
26+ # udev sets INTERFACE to the name of the currently-processed nic.
27+ [ -n "$INTERFACE" ] && return 0
28+ echo "environment variable INTERFACE not set." 1>&2;
29+ return 1
30+}
31+
32 start() {
33 CR="
34 "
35+ assert_interface || return
36 ifile=/run/initramfs/open-iscsi.interface
37- if [ -f "$ifile" ] && read iface < "$ifile" &&
38- ! grep -qs "^$iface=" /run/network/ifstate; then
39+
40+ [ -f "$ifile" ] && read iface < "$ifile" || return 0
41+ [ "$INTERFACE" = "$iface" ] || return
42+
43+ if ! grep -qs "^$iface=" /run/network/ifstate; then
44 mkdir -p /run/network
45 echo "$iface=$iface" >>/run/network/ifstate
46
47@@ -51,9 +62,12 @@ EOF
48 }
49
50 stop() {
51+ assert_interface || return
52 ifile=/run/initramfs/open-iscsi.interface
53- if [ -f "$ifile" ] && read iface < "$ifile" &&
54- grep -qs "^$iface=" /run/network/ifstate; then
55+ [ -f "$ifile" ] && read iface < "$ifile" || return 0
56+ [ "$INTERFACE" = "$iface" ] || return
57+
58+ if grep -qs "^$iface=" /run/network/ifstate; then
59 grep -v "^$iface=" /run/network/ifstate >/run/network/.ifstate.tmp || true
60 mv /run/network/.ifstate.tmp /run/network/ifstate
61
62diff --git a/debian/tests/README-boot-test.md b/debian/tests/README-boot-test.md
63new file mode 100644
64index 0000000..e45bbf0
65--- /dev/null
66+++ b/debian/tests/README-boot-test.md
67@@ -0,0 +1,139 @@
68+## open-iscsi boot test
69+The purpose of this test (`CloudImageTest`) is to test the boot of a system
70+using an iscsi root target. In order to accomplish that, the tests does
71+
72+ 1. Download Ubuntu cloud image
73+ 2. installs the open-iscsi deb inside
74+ 3. collect kernel and initramfs from inside
75+ 4. register the image as a read-only iscsi target served by tgt
76+ 5. boot kernel and initramfs with a command line to use the image as root.
77+ And additionally attach a local disk for collecting output.
78+ 6. provide user-data that executes commands, colects files and writes them
79+ to the output disk and then shuts the system down.
80+ 7. extract the collected files from the output disk and inspect them.
81+
82+The `CloudImageTest` uses qemu user networking.
83+
84+
85+## Caveats
86+
87+ 1. It depends on a cloud-image being present.
88+
89+ Cloud-images are often not available for the first few weeks of a cycle.
90+ If no cloud-image of 'REL' is available, then boot-test will skip.
91+ If 'REL' is not known in distro-info-data, then test will fail.
92+
93+ This means that uploads of open-iscsi (or its dependencies) will not
94+ be properly tested until a cloud-image is available, and will fail
95+ until distro-info-data is uploaded.
96+
97+ 2. Installation of large packages via patch-image may fail.
98+ An ubuntu image downloaded has only a small amount of extra space.
99+ Installation of a new kernel into the target would probably fail.
100+
101+ If this becomes a problem, we could grow the disk like done at
102+ https://gist.github.com/smoser/6a048a0e2795b48221fc44962202fa14
103+
104+
105+### testing manually ###
106+The test case in `debian/tests/test-open-iscsi.py` uses some helper tools.
107+
108+ * **patch-image**: this is used to install packages into the pristine image
109+ and collect the kernel and initramfs out of the image. This allows us to
110+ test the portions of open-iscsi that update the initramfs. Without
111+ using the updated initramfs we wouldn't really be testing the new
112+ open-iscsi.
113+
114+ It will upgrade any packages inside that are mentioned in
115+ ADT_TEST_TRIGGERS environment. It will also install open-iscsi if
116+ it is not in that list.
117+
118+ It installs packages into the target by copying the host system's
119+ /etc/apt content in, and also includes copying local (file://) apt
120+ repos into the target. This is necessary for the autopackage test
121+ environment that adds local package repositories to sources.list.d.
122+
123+ It may also make other changes to the image to workaround bugs.
124+ See --help output for list of bugs.
125+
126+ * **get-image**: This downloads an image from cloud-images.ubuntu.com. See
127+ its Usage for more information. it downloads via stream data and verifies
128+ download. One thing to note is that it does not overwrite existing files.
129+
130+ * **tgt-boot-test**: this registers an image in tgt locally, and then boots
131+ kernel and initramfs to mount that. It knows how to build iscsi kernel
132+ command lines.
133+
134+ By default, the xkvm process that is started will be allowed `60m` to
135+ complete. This can be adjusted by setting `BOOT_TIMEOUT` in the
136+ environment.
137+
138+ * **xkvm**: this is a helper/wrapper around qemu. It is taken from the curtin
139+ projects tools/ directory. It allows some simplified command lines, and
140+ most usefully, the '--netdev=<bridge>' argument will create a tun/tap
141+ device and attach it to the bridge.
142+
143+ If the host system is not bare metal, kvm will not be
144+ enabled in the guest that is lauched. To force use of kvm, set
145+ _USE_KVM=1 in the environment. See 'should_try_kvm' in xkvm for details.
146+
147+Testing manually looks like this:
148+
149+ ## set up path to include debian/tests directory.
150+ $ PATH=$PWD/debian/tests:$PATH
151+
152+ ## Get the image you want. This creates out.d/disk.img and disk.img.dist
153+ $ get-image xenial.d xenial
154+
155+ ## patch the image with an open-iscsi, which creates xenial.d/kernel
156+ ## and xenial.d/initrd from the kernel and initramfs inside the image.
157+ $ apt-get download open-iscsi
158+ $ deb=$(ls open-iscsi_*.deb | tail -n 1)
159+ $ sudo ./debian/tests/patch-image \
160+ --kernel=xenial.d/kernel --initrd=xenial.d/initrd
161+ xenial.d/disk.img "$deb"
162+
163+ ## Boot the system, log in, look around.
164+ $ tgt-boot-test -v xenial.d/disk.img xenial.d/kernel xenial.d/initrd
165+
166+
167+### Features of tgt-boot-test ###
168+
169+tgt-boot-test does a number of useful things.
170+
171+ * determines the host address that the guest will use.
172+ This should support ipv6 and ipv4 addresses on bridges, and
173+ knows values that qemu's user networking uses. Flags passed to `--netdev`
174+ are read intelligently. This can be overriden with `--host-addr`, but
175+ it does a good job of determining what the right values are.
176+
177+ * provides a nocloud metadata service with a python web server that
178+ supports ipv4 and ipv6.
179+
180+ * provides the ability to provide additional kernel command line options
181+ or to provide a 'template' that references variables it knows such as
182+ {iserver} (iscsi server) or {seed_url}.
183+
184+ * Sets ubuntu (passw0rd) and root password (root) and imports users
185+ ssh keys to the ubuntu user.
186+
187+One thing to note is that yakkety's version of qemu does not run an ipv6
188+dhcp server on its user-network, so a stateful dhclient request will not
189+work.
190+
191+In order to create a bridge easily with a ipv6 dhcp server, you can use
192+lxd at sufficent version (https://github.com/lxc/lxd/issues/2481).
193+Assuming that bug is fixed, to create an ipv6 only bridge:
194+
195+ $ netname="ipv6-only"
196+ $ lxc network create $netname
197+ $ lxc network unset $netname ipv4.address
198+ $ lxc network unset $netname ipv4.nat
199+ $ lxc network set $netname ipv6.dhcp.stateful true
200+
201+Then, you can use tgt-boot-test with that:
202+
203+ $ PATH=$PWD/debian/tests:$PATH
204+ $ ./debian/tests/tgt-boot-test -vv --netdev=ipv6-only \
205+ --cmdline-ip="ip=off ip6=dhcp" \
206+ disk.img kernel initramfs
207diff --git a/debian/tests/control b/debian/tests/control
208index 320757e..3662819 100644
209--- a/debian/tests/control
210+++ b/debian/tests/control
211@@ -1,3 +1,7 @@
212-Tests: install sysvinit-install daemon testsuite
213+Tests: install sysvinit-install daemon
214 Restrictions: needs-root isolation-machine breaks-testbed
215-Depends: open-iscsi, python, tgt, qemu-system, ubuntu-cloudimage-keyring, simplestreams, python-netifaces, distro-info, cloud-image-utils
216+Depends: open-iscsi
217+
218+Tests: testsuite
219+Restrictions: needs-root isolation-machine breaks-testbed
220+Depends: open-iscsi, python, tgt, qemu-system, ubuntu-cloudimage-keyring, simplestreams, python-netifaces, distro-info, cloud-image-utils, dctrl-tools, rsync
221diff --git a/debian/tests/get-maas-eph b/debian/tests/get-image
222index 660a72d..41b903c 100755
223--- a/debian/tests/get-maas-eph
224+++ b/debian/tests/get-image
225@@ -2,23 +2,38 @@
226 Usage() {
227 cat <<EOF
228 ${0##*/} out-directory release
229+
230+By default, the cloud image for 'release' is downloaded from the
231+daily cloud image stream.
232+
233+Additional arguments can change behavior of what is downloaded.
234+Some supported arguments:
235+ * cloud-daily: use daily stream [default]
236+ * cloud-release: use release stream rather than daily stream.
237+ * maas-release, maas-daily: download maas images instead of cloud-image
238+ * hwe-N: download the hwe kernel from release 'N' instead of default
239+ this is only respected if maas stream is used.
240+ * YYYYMMDD[.X] : download the specific build version
241+
242 example:
243- ${0##*/} trusty_dir trusty daily
244+ ${0##*/} xenial_dir xenial cloud-daily
245 EOF
246 }
247 TEMP_D=""
248
249+NO_IMAGE_AVAILABLE_RC=3
250+
251 cleanup() {
252 [ ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
253 }
254-VERBOSITY=0
255+VERBOSITY=1
256 inargs() {
257 local n=$1 x="";
258 shift;
259 for x in "$@"; do [ "$n" = "$x" ] && return 0; done;
260 return 1
261 }
262-debug() { [ $1 -lt $VERBOSITY ] || return 0; shift; echo "$@" 1>&2; }
263+debug() { [ $1 -ge $VERBOSITY ] || return 0; shift; echo "$@" 1>&2; }
264 error() { echo "$@" 1>&2; }
265 fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
266
267@@ -29,8 +44,11 @@ trap cleanup EXIT
268 arches=( ppc64el i386 amd64 )
269 releases=( $(ubuntu-distro-info --all) )
270 mbase="http://maas.ubuntu.com/images/ephemeral-v2"
271-def_stream="${mbase}/daily"
272-def_rel="trusty"
273+cbase="http://cloud-images.ubuntu.com"
274+def_stream_type="cloud"
275+stream_src=""
276+curbase="$mbase"
277+def_rel="xenial"
278 vername=""
279
280 [ "$1" = "-h" -o "$1" = "--help" ] && { Usage; exit 0; }
281@@ -50,28 +68,52 @@ pt=( )
282 for x in "$@"; do
283 inargs "$x" "${arches[@]}" && pt[${#pt[@]}]=arch=$x && arch="$x" && continue
284 inargs "$x" "${releases[@]}" && pt[${#pt[@]}]="release=$x" && release=$x && continue
285+ [ "$x" = "released" -o "$x" = "release" ] && x="releases"
286 case "$x" in
287- daily) stream="${mbase}/daily";;
288- released|release|releases) stream="${mbase}/releases";;
289+ cloud-released|cloud-release) x="cloud-releases";;
290+ maas-released|maas-release) x="maas-releases";;
291+ esac
292+
293+ case "$x" in
294+ maas|cloud) stream_type="$x";;
295+ cloud-daily|maas-daily|cloud-releases|maas-releases)
296+ stream_type="${x%-*}"
297+ stream_sub=${x#*-};;
298 http://*) stream=$x;;
299- hwe-*) subarch="$x"; pt[${#pt[@]}]="subarch=$x";;
300- subarch=*) pt[${#pt[@]}]="$x"; subarch=${x#*=};;
301+ hwe-*) subarch="$x";;
302+ subarch=*) subarch=${x#*=};;
303 *=*) pt[${#pt[@]}]="$x";;
304 *~*) pt[${#pt[@]}]="$x";;
305 [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]|[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].[0-9])
306 pt[${#pt[@]}]="version_name=$x"
307 vername="$x";;
308+ *) fail "Confused by input token '$x'";;
309 esac
310 done
311
312-[ -n "$stream" ] || stream="$def_stream"
313+[ -n "$stream_type" ] || stream_type="${def_stream_type}"
314+if [ -z "$stream" ]; then
315+ stream_sub=${stream_sub:-daily}
316+ case "$stream_type" in
317+ maas) stream="$mbase/$stream_sub";;
318+ cloud) stream="$cbase/$stream_sub";;
319+ *) fail "unknown stream_type '$stream_type'";;
320+ esac
321+fi
322+
323 [ -n "$arch" ] || pt[${#pt[@]}]="arch=$def_arch"
324-if [ -z "$subarch" ]; then
325- t=${release#?}
326- first_letter=${release%${t}}
327- subarch="hwe-${first_letter}"
328- pt[${#pt[@]}]="subarch=$subarch"
329- error "selected subarch=${subarch} for $release"
330+
331+if [ "$stream_type" = "maas" ]; then
332+ if [ -z "$subarch" ]; then
333+ t=${release#?}
334+ first_letter=${release%${t}}
335+ subarch="hwe-${first_letter}"
336+ error "selected subarch=${subarch} for $release"
337+ fi
338+ pt[${#pt[@]}]="subarch=$subarch"
339+ ofmt="%(sha256)s %(ftype)s %(subarch)s %(item_url)s"
340+else
341+ ofmt="%(sha256)s %(ftype)s %(item_url)s"
342 fi
343
344 OIFS="$IFS"
345@@ -97,39 +139,89 @@ if [ -n "${needs# }" ]; then
346 fi
347
348 qcmd=( sstream-query ${keyring:+"--keyring=$keyring"} ${vername:---max=1}
349- --output-format="%(sha256)s %(subarch)s %(ftype)s %(item_url)s" "${stream}" "${pt[@]}" )
350+ --output-format="$ofmt" "${stream}" datatype="image-downloads"
351+ "${pt[@]}" )
352 echo "${qcmd[@]}"
353 "${qcmd[@]}" > "${TEMP_D}/qout"
354-roots=$(awk '$3 == "root-image.gz" { print $2 }' "${TEMP_D}/qout" | sort -u | wc -l)
355-[ "$roots" = "1" ] || {
356+roots=$(awk '$2 == "root-image.gz" || $2 == "disk1.img" { print $2 }' \
357+ "${TEMP_D}/qout" | sort -u | wc -l)
358+if [ "$roots" = "0" ]; then
359+ error "No image available for $release in $stream${pt:+ (${pt[*]})}"
360+ exit ${NO_IMAGE_AVAILABLE_RC}
361+elif [ "$roots" != "1" ]; then
362 error "query resulted in '$roots' root images. expect 1 and only 1"
363+ error "cmd was ${qcmd[*]}"
364 cat "${TEMP_D}/qout"
365 fail
366-}
367-image="$out_d/root-image"
368+fi
369+
370 set -f
371 while read line; do
372 set -- $line
373- sha256="$1"; subarch="$2"; ftype="$3"; url="$4"
374- [ "$ftype" = "di-kernel" -o "$ftype" = "di-initrd" ] && continue
375+ if [ "$stream_type" = "maas" ]; then
376+ sha256="$1"; ftype="$2"; subarch="$3"; url="$4"
377+ else
378+ sha256="$1"; ftype="$2"; url="$3"
379+ fi
380+ case "$ftype" in
381+ boot-kernel|boot-initrd|root-image.gz|disk1.img) :;;
382+ *) continue
383+ esac
384 if [ "$ftype" = "root-image.gz" ]; then
385 fname="$ftype"
386- [ -f "$image" ] && continue
387+ image_src="$out_d/$fname"
388+ image="$out_d/root-image"
389+ elif [ "$ftype" = "disk1.img" ]; then
390+ fname="disk.img.dist"
391+ image_src="$out_d/$fname"
392+ image="$out_d/disk.img"
393 else
394 fname="$subarch/$ftype"
395 fi
396 out_f="${out_d}/$fname"
397- [ -f "$out_f" ] && continue
398+ if [ -f "$out_f" ]; then
399+ debug 1 "reusing existing '$out_f' rather than downloading $url."
400+ continue
401+ fi
402 [ -d "${out_f%/*}" ] || mkdir -p "${out_f%/*}"
403- wget -c "$url" -O "$out_f.tmp" && mv "$out_f.tmp" "$out_f" || { rm -f "$out_f.tmp"; exit 3; }
404- echo "$out_d/$fname" "$url" >> "$out_d/contents"
405+ tmp="${out_f}.tmp"
406+ wget --progress=dot:mega -O "$tmp" "$url" || {
407+ error "Failed to download $url to $tmp."
408+ rm -f "$tmp"
409+ exit 3
410+ }
411+ debug 1 "checksumming $tmp from $url"
412+ out=$(sha256sum "$tmp") || {
413+ error "Failed to checksum $tmp"
414+ rm -f "$tmp"
415+ exit 3;
416+ }
417+ found=${out% *}
418+ if [ "$found" != "$sha256" ]; then
419+ error "$tmp from $url found sha256 of $found. expected $sha256."
420+ ls -l "$tmp" 1>&2
421+ rm -f "$tmp"
422+ exit 3
423+ fi
424+ mv "$tmp" "$out_f" || fail "failed renaming $tmp to $out_f"
425+ echo "$fname" "$url" >> "$out_d/contents"
426 done < "${TEMP_D}/qout"
427
428 if [ ! -f "$image" ]; then
429- debug 1 "decompressing $image.gz"
430- zcat "$image.gz" > "$image.tmp" &&
431- mv "$image.tmp" "$image" ||
432- { rm -f "$image.tmp"; exit 1; }
433+ case "$image_src" in
434+ */root-image.gz)
435+ debug 1 "decompressing $image_src"
436+ zcat "$image_src" > "$image.tmp" &&
437+ mv "$image.tmp" "$image" ||
438+ { rm -f "$image.tmp"; exit 1; }
439+ ;;
440+ */disk.img.dist)
441+ debug 1 "converting qcow2 in $image_src -> raw in $image"
442+ qemu-img convert -O raw "$image_src" "$image.tmp" &&
443+ mv "$image.tmp" "$image" ||
444+ { rm -f "$image.tmp"; exit 1; }
445+ ;;
446+ esac
447 fi
448
449 # vi: ts=4 expandtab
450diff --git a/debian/tests/patch-image b/debian/tests/patch-image
451new file mode 100755
452index 0000000..b9b34e3
453--- /dev/null
454+++ b/debian/tests/patch-image
455@@ -0,0 +1,288 @@
456+#!/bin/bash
457+
458+VERBOSITY=1
459+TEMP_D=""
460+
461+error() { echo "$@" 1>&2; }
462+fail() { local r=$?; [ $r -eq 0 ] && r=1; failrc "$r" "$@"; }
463+failrc() { local r=$1; shift; [ $# -eq 0 ] || error "$@"; exit $r; }
464+
465+Usage() {
466+ cat <<EOF
467+Usage: ${0##*/} [options] image [packages]
468+ Patch image, upgrading [packages].
469+ --kernel FILE copy kernel out to FILE
470+ --initrd FILE copy initrd out to FILE
471+
472+ --krd-only only copy out kernel/initrd do not change image.
473+ this is incompatible with 'packages'
474+ it is a short-cut to specifying all the '--no-*'
475+ options below.
476+
477+ --no-copy-apt do not copy host's apt repos in.
478+
479+ image modifications:
480+ --no-update-fstab do not modify fstab (LP: #1732028)
481+
482+ if no packages are provided, and ADT_TEST_TRIGGERS is set
483+ in environment, then it will be read for the list of packages.
484+ to override that behavior pass package 'none'.
485+EOF
486+
487+}
488+
489+bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
490+cleanup() {
491+ [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
492+}
493+
494+debug() {
495+ local level=${1}; shift;
496+ [ "${level}" -gt "${VERBOSITY}" ] && return
497+ error "${@}"
498+}
499+
500+adt_test_triggers_to_bin_pkgs() {
501+ # ADT_TEST_TRIGGERS is space delimited <src>/version
502+ # returns a scalar '_RET', caller has to expand
503+ local tok src="" ver="" binpkgs=""
504+ for tok in "$@"; do
505+ src=${tok%/*}
506+ ver=${tok#*/}
507+ bin_packages_from_source_pkg "$src" "$ver" || return
508+ if [ -z "${_RET}" ]; then
509+ error "$tok: (source=$src ver=$ver) No packages available!"
510+ return 1
511+ fi
512+ binpkgs="${binpkgs} ${_RET}"
513+ done
514+ _RET=${binpkgs# }
515+}
516+
517+bin_packages_from_source_pkg() {
518+ local pkg=$1 ver=${2} cmd="" out="" p="" ret=""
519+ if ! command -v grep-aptavail >/dev/null; then
520+ error "No command grep-aptavail: apt-get install dctrl-tools"
521+ return 1
522+ fi
523+ cmd=( grep-aptavail --show-field=Package
524+ --exact-match --field=Source:Package "$pkg" )
525+ if [ -n "$ver" ]; then
526+ cmd=( "${cmd[@]}" --and --field=Version "$ver" )
527+ fi
528+ out=$("${cmd[@]}" | sed 's,Package: ,,')
529+ ret=""
530+ for p in $out; do
531+ ret="$ret ${p#Package: }${ver:+=${ver}}"
532+ done
533+ # if no version was provided, we could have multiple results
534+ _RET=$(set -f; for i in ${ret}; do echo "$i"; done | sort -u)
535+}
536+
537+update_fstab() {
538+ # update_fstab(path) modify the fstab at path
539+ # to remove problematic entries (LP: #1732028)
540+ local fstab="$1"
541+ if [ ! -e "$fstab.patch-image-dist" ]; then
542+ cp "$fstab" "$fstab.patch-image-dist" ||
543+ { error "failed backing up $fstab to $fstab.dist"; return 1; }
544+ fi
545+ sed -i '/^LABEL=UEFI/s/^/#/' "$fstab"
546+}
547+
548+main() {
549+ local short_opts="hv"
550+ local long_opts="help,no-copy-apt,initrd:,kernel:,verbose"
551+ local getopt_out="" orig_args=""
552+ orig_args=( "$@" )
553+ getopt_out=$(getopt --name "${0##*/}" \
554+ --options "${short_opts}" --long "${long_opts}" -- "$@") &&
555+ eval set -- "${getopt_out}" ||
556+ { bad_Usage; return; }
557+
558+ local cur="" next=""
559+ local kernel="" initrd="" copy_apt=true krd_only=false
560+ local update_fstab=true
561+
562+ while [ $# -ne 0 ]; do
563+ cur="$1"; next="$2";
564+ case "$cur" in
565+ -h|--help) Usage ; exit 0;;
566+ --krd-only) krd_only=true; shift;;
567+ --no-copy-apt) copy_apt=false; shift;;
568+ --no-update-fstab) update_fstab=false; shift;;
569+ --kernel) kernel=$next; shift;;
570+ --initrd) initrd=$next; shift;;
571+ -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
572+ --) shift; break;;
573+ esac
574+ shift;
575+ done
576+
577+ [ $# -ne 0 ] || { bad_Usage "must provide at least image"; return; }
578+
579+ [ "$(id -u)" = "0" ] || fail "not root"
580+ target="$1"
581+ shift
582+ if [ -f "$target" ]; then
583+ local nargs=""
584+ nargs=( "${orig_args[@]}" )
585+ # replace the first occurance of target in orig args with _MOUNTPOINT_
586+ for((i=0;i<${#nargs[@]};i++)); do
587+ [ "${nargs[$i]}" = "$target" ] && nargs[$i]=_MOUNTPOINT_ && break
588+ done
589+ debug 1 "mic $target -- $0 ${nargs[*]}"
590+ IMAGE="$target" exec mount-image-callback \
591+ --system-resolvconf "$target" -- "$0" "${nargs[@]}"
592+ fi
593+
594+ if [ ! -d "$target" ]; then
595+ fail "$target: not a directory or file"
596+ fi
597+
598+ local tsrc=${IMAGE:-${target}}
599+ local packages=( )
600+ packages=( "$@" )
601+
602+ if $krd_only; then
603+ if [ "${#packages[@]}" != 0 -a "${packages[*]}" != "none" ]; then
604+ error "--krd-only is incompatible with packages."
605+ return 1
606+ fi
607+ debug 1 "--krd-only provided no changes will be done."
608+ copy_apt=false
609+ update_fstab=false
610+ packages=( "none" )
611+ fi
612+
613+ if [ "${#packages[@]}" = "1" -a "${packages[0]}" = "none" ]; then
614+ packages=( )
615+ elif [ "${#packages[@]}" -eq 0 -a -n "${ADT_TEST_TRIGGERS}" ]; then
616+ adt_test_triggers_to_bin_pkgs ${ADT_TEST_TRIGGERS} ||
617+ fail "failed to find binary packages " \
618+ "from ADT_TEST_TRIGGERS=$ADT_TEST_TRIGGERS"
619+ packages=( $_RET )
620+ debug 1 "read ADT_TEST_TRIGGERS=$ADT_TEST_TRIGGERS to set" \
621+ "packages=${packages[*]}"
622+ else
623+ local opackages="" debs="" pkg=""
624+ opackages=( "${packages[@]}" )
625+ packages=( )
626+ debs=( )
627+ for pkg in "${opackages[@]}"; do
628+ [ -f "${pkg}" ] && debs[${#debs[@]}]="$pkg" ||
629+ packages[${packages[@]}]="$pkg"
630+ done
631+ fi
632+
633+ # if open-iscsi is not in the packages list above, we handle it specifically
634+ # so that even if the image did not have open-iscsi, it will get it.
635+ local pkg="" mypkg="open-iscsi"
636+ for pkg in "${packages[@]}"; do
637+ case "$pkg" in
638+ open-iscsi|open-iscsi/*) mypkg="none";;
639+ esac
640+ done
641+
642+ TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
643+ fail "failed to make tempdir"
644+ trap cleanup EXIT
645+
646+ if $copy_apt; then
647+ local distdir="${target}/etc/apt.dist.${0##*/}"
648+ if [ ! -d "$distdir" ]; then
649+ mv "$target/etc/apt" "$distdir" ||
650+ fail "failed to backup /etc/apt in $tsrc"
651+ fi
652+ cp -a /etc/apt "$target/etc/" ||
653+ fail "failed to replace /etc/apt in $tsrc"
654+
655+ # find file:// repos in apt sources and copy them in
656+ local file_repos=""
657+ file_repos=$(
658+ fm='s,.*[[:space:]]file:///*\([^[[:space:]]*\).*,/\1,p'
659+ shopt -s nullglob
660+ for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do
661+ sed -n -e 's/#.*//' -e "$fm" "$f"
662+ done
663+ )
664+ local dir="" bdir="" tdir=""
665+ for dir in ${file_repos}; do
666+ tdir="${target}$(dirname $dir)/$(basename ${dir})"
667+ if [ -d "$dir" ]; then
668+ debug 1 "copying $dir -> $tdir"
669+ mkdir -p "$tdir" ||
670+ fail "failed to create $tdir"
671+ rsync --delete -a "$dir/" "$tdir" ||
672+ fail "failed copy from $dir/ to $tdir"
673+ fi
674+ done
675+ fi
676+
677+ if $update_fstab; then
678+ update_fstab "$target/etc/fstab" ||
679+ { error "failed updating /etc/fstab in target"; return 1; }
680+ fi
681+
682+ mount -o bind /sys "$target"/sys || :
683+ mount -o bind /proc "$target"/proc || :
684+ mount -o bind /dev "$target"/dev || :
685+ mount -o bind /dev/pts "$target"/dev/pts || :
686+
687+ if [ "${#packages[@]}" != "0" ]; then
688+ DEBIAN_FRONTEND=noninteractive chroot "$target" sh -exc '
689+ mypkg=$1
690+ shift
691+ apt-get update -q
692+ if [ "$mypkg" != "none" ]; then
693+ apt-get install -qy "$mypkg"
694+ fi
695+ apt-get install -qy --only-upgrade "$@"' \
696+ chroot-inst "$mypkg" "${packages[@]}" ||
697+ fail "failed to install ${packages[*]} in $target"
698+ fi
699+
700+ if [ "${#debs[@]}" != "0" ]; then
701+ local tmpd=""
702+ tmpd=$(mktemp -d "${target}/tmp/${0##*/}.XXXXXX")
703+ cp "${debs[@]}" "$tmpd"
704+ DEBIAN_FRONTEND=noninteractive chroot "$target" sh -exc \
705+ 'cd "$1"; shift; dpkg -i *.deb' debinstalls "${tmpd#${target}}"
706+ rm -Rf "${tmpd}"
707+ fi
708+
709+ umount "$target"/dev/pts || :
710+ umount "$target"/dev || :
711+ umount "$target"/proc || :
712+ umount "$target"/sys || :
713+
714+ local kpath="" ipath=""
715+ for f in "$target/boot/"*; do
716+ case "${f##*/}" in
717+ *.signed) continue;;
718+ vmlinu?-*) kpath="$f";;
719+ initrd.img-*) ipath="$f";;
720+ esac
721+ done
722+
723+ if [ -n "$kernel" ]; then
724+ [ -n "$kpath" ] || fail "unable to find kernel in $tsrc"
725+ debug 1 "using kernel ${kpath#${target}}"
726+ cp "$kpath" "$kernel" ||
727+ fail "failed copy $kpath to $kernel"
728+ [ -z "$IMAGE" ] || chown "--reference=$IMAGE" "$kernel"
729+ fi
730+
731+ if [ -n "$initrd" ]; then
732+ [ -n "$ipath" ] || fail "unable to find initrd in $tsrc"
733+ debug 1 "using initrd ${ipath#${target}}"
734+ cp "$ipath" "$initrd" ||
735+ fail "failed copy $ipath to $initrd"
736+ [ -z "$IMAGE" ] || chown "--reference=$IMAGE" "$initrd"
737+ fi
738+
739+}
740+
741+main "$@"
742+
743+# vi: ts=4 expandtab
744diff --git a/debian/tests/test-open-iscsi.py b/debian/tests/test-open-iscsi.py
745index 5358a9d..4da193e 100644
746--- a/debian/tests/test-open-iscsi.py
747+++ b/debian/tests/test-open-iscsi.py
748@@ -44,8 +44,10 @@
749
750
751 from netifaces import gateways, AF_INET
752-import unittest, subprocess, sys, os
753+import unittest, subprocess, sys, os, glob
754+import shutil
755 import testlib
756+import textwrap
757 from tempfile import mkdtemp
758 import time
759
760@@ -57,6 +59,46 @@ username_in = 'ubuntu'
761 password_in = 'ubuntupasswd'
762 initiatorname = 'iqn.2009-10.com.example.hardy-multi:iscsi-01'
763
764+COLLECT_USER_DATA = """\
765+#cloud-config
766+bukket:
767+ - &get_resolved_status |
768+ if [ "${1:--}" != "-" ]; then
769+ exec >"$1" 2>&1
770+ fi
771+ if ! command -v systemd-resolve >/dev/null 2>&1; then
772+ echo "systemd-resolve: not available."
773+ exit
774+ fi
775+ if ! ( systemd-resolve --help | grep -q -- --status ); then
776+ echo "systemd-resolve: no --status."
777+ exit
778+ fi
779+ systemd-resolve --status --no-pager
780+ - &add_and_remove_tuntap |
781+ #!/bin/sh
782+ # LP: #1785108 would break dns when any device was removed.
783+ tapdev="mytap0"
784+ echo ==== Adding $tapdev ====
785+ ip tuntap add mode tap user root $tapdev
786+ udevadm settle
787+ echo ==== Removing $tapdev ====
788+ ip tuntap del mode tap $tapdev
789+ udevadm settle
790+
791+runcmd:
792+ - [ sh, -c, *add_and_remove_tuntap ]
793+ - [ mkdir, -p, /output ]
794+ - [ cp, /etc/resolv.conf, /output]
795+ - [ sh, -c, *get_resolved_status, --, /output/systemd-resolve-status.txt]
796+ - [ tar, -C, /output, -cf, /dev/disk/by-id/virtio-output-disk, . ]
797+
798+power_state:
799+ mode: poweroff
800+ message: cloud-init finished. Shutting down.
801+ timeout: 60
802+"""
803+
804 try:
805 from private.qrt.OpenIscsi import PrivateOpenIscsiTest
806 except ImportError:
807@@ -158,89 +200,163 @@ discovery.sendtargets.auth.password_in = %s
808 nih_path = '/lib/open-iscsi/net-interface-handler'
809 self.assertTrue(os.access(nih_path, os.X_OK))
810
811-reason = (
812- "Skipped MAAS ephemeral test in ppc64el because it lacks nested "
813- "virtualization support.")
814-@unittest.skipIf(testlib.manager.dpkg_arch == "ppc64el", reason)
815-class MAASEphemeralTest(testlib.TestlibCase, PrivateOpenIscsiTest):
816- '''Test MAAS ephemeral image can boot.
817-
818- Downloads MAAS ephemeral image, patches the image to use dep8's built
819- open-iscsi package, sets the image to be served by tgt and then boot that
820- image under (nested) kvm. It runs cloud-init in the booted VM to collect
821- some data and verify the image worked as expected.
822+class CloudImageTest(testlib.TestlibCase, PrivateOpenIscsiTest):
823+ '''Test the cloud image can boot on iscsi root.
824+
825+ See README-boot-test.md for more information.
826 '''
827
828- def setUp(self):
829- self.here = os.path.dirname(os.path.abspath(__file__))
830- self.release = testlib.ubuntu_release()
831- self.subarch = 'hwe-{}'.format(self.release[0])
832+ @classmethod
833+ def setUpClass(cls):
834+ reason = (
835+ "Skipped Cloud Image test on {arch}. whitelisted are: {whitelist}")
836+ whitelisted = os.environ.get('WHITELIST_ARCHES', 'amd64').split(",")
837+ curarch = testlib.manager.dpkg_arch
838+ if testlib.manager.dpkg_arch not in whitelisted:
839+ raise unittest.SkipTest(
840+ reason.format(arch=curarch, whitelist=whitelisted))
841+
842+ here = os.path.dirname(os.path.abspath(__file__))
843+ os.environ['PATH'] = "%s:%s" % (here, os.environ['PATH'])
844+ release = os.environ.get(
845+ "ISCSI_TEST_RELEASE", testlib.ubuntu_release())
846+ image_d = os.path.join(here, '{}.d'.format(release))
847 # Download MAAS ephemeral image.
848- self.get_maas_ephemeral()
849- # Patch the image to use the dep8 built open-iscsi.
850- self.patch_image()
851- self.output_disk_path = self.create_output_disk(self.here)
852-
853- def create_output_disk(self, base_path):
854- output_disk_path = os.path.join(base_path, 'output_disk.img')
855- subprocess.call([
856- 'qemu-img', 'create', '-f', 'raw', output_disk_path, '10M'])
857- return output_disk_path
858-
859- def get_maas_ephemeral(self):
860- get_eph_cmd = os.path.join(self.here, 'get-maas-eph')
861- subprocess.call([
862- get_eph_cmd, os.path.join(self.here, '{}.d'.format(self.release)),
863- self.release, self.subarch])
864-
865- def patch_image(self):
866- '''Patch root-image with dep8 built open-iscsi package.'''
867- root_image_path = os.path.join(
868- self.here, '{}.d'.format(self.release), 'root-image')
869- open_iscsi_deb_path = os.path.join(
870- # XXX: matsubara fix this path hackery.
871- # os.environ['ADTTMP'], 'binaries', 'open-iscsi.deb')
872- self.here, '..', '..', '..', '..', 'binaries', 'open-iscsi.deb')
873- open_iscsi_deb = open(open_iscsi_deb_path)
874- install_cmd = (
875- 'cat >/tmp/my.deb && dpkg -i /tmp/my.deb; ret=$?;'
876- 'rm -f /tmp/my.deb; exit $ret')
877- subprocess.check_output([
878- 'mount-image-callback', '--system-mounts',
879- root_image_path, '--', 'chroot', '_MOUNTPOINT_', '/bin/sh', '-c',
880- install_cmd], stdin=open_iscsi_deb)
881+ info = {'release': release,
882+ 'image_d': image_d,
883+ 'root_image': os.path.join(image_d, 'disk.img'),
884+ 'kernel': os.path.join(image_d, 'kernel'),
885+ 'initrd': os.path.join(image_d, 'initrd')}
886+ try:
887+ get_image(info['image_d'], release)
888+ except CalledProcessError as e:
889+ if e.return_code != 3:
890+ raise e
891+ raise unittest.SkipTest(
892+ "Cloud Image not available for release '%s'." % release)
893+ if os.environ.get("NO_PATCH_IMAGE", "0") == "0":
894+ patch_image(info['root_image'],
895+ kernel=info['kernel'], initrd=info['initrd'])
896+ cls.info = info
897+ cls.here = here
898+
899+ def tearDown(self):
900+ super(CloudImageTest, self).tearDown()
901+ if os.path.exists(self.tmpdir):
902+ shutil.rmtree(self.tmpdir)
903+
904+ def setUp(self):
905+ super(CloudImageTest, self).setUp()
906+ self.tmpdir = mkdtemp()
907+ udp = os.path.join(self.tmpdir, 'user-data')
908+ with open(udp, "w") as fp:
909+ fp.write(COLLECT_USER_DATA)
910+ self.info['user-data'] = udp
911+
912+ def create_output_disk(self):
913+ path = os.path.join(self.tmpdir, 'output-disk.img')
914+ subprocess.check_call([
915+ 'qemu-img', 'create', '-f', 'raw', path, '10M'])
916+ return path
917
918 def extract_files(self, path):
919+ # get contents in a dictionary of tarball at 'path'
920 tmpdir = mkdtemp()
921- subprocess.call(['tar', '-C', tmpdir, '-xf', path])
922- return [os.path.join(tmpdir, f) for f in os.listdir(tmpdir)]
923-
924- def get_default_route_ip(self):
925- gws = gateways()
926- return gws['default'][AF_INET][0]
927+ try:
928+ subprocess.check_call(['tar', '-C', tmpdir, '-xf', path])
929+ flist = {}
930+ prefix = len(tmpdir) + 1
931+ for root, dirs, files in os.walk(tmpdir):
932+ for fname in files:
933+ fpath = os.path.join(root, fname)
934+ key = fpath[prefix:]
935+ with open(fpath, "r") as fp:
936+ flist[key] = fp.read()
937+ return flist
938+ finally:
939+ shutil.rmtree(tmpdir)
940
941 def test_tgt_boot(self):
942 tgt_boot_cmd = os.path.join(self.here, 'tgt-boot-test')
943- env = os.environ
944- env['HOST_IP'] = self.get_default_route_ip()
945 # Add self.here to PATH so xkvm will be available to tgt-boot-test
946- env['PATH'] = env['PATH'] + ":%s" % self.here
947- env['OUTPUT_DISK'] = self.output_disk_path
948- rel_dir = '{}.d'.format(self.release)
949- subprocess.call([
950- tgt_boot_cmd,
951- os.path.join(self.here, rel_dir, 'root-image'),
952- os.path.join(self.here, rel_dir, self.subarch, 'boot-kernel'),
953- os.path.join(self.here, rel_dir, self.subarch, 'boot-initrd')],
954- env=env)
955- files = self.extract_files(self.output_disk_path)
956- for f in files:
957- if f.endswith('resolv.conf'):
958- resolvconf_contents = open(f).read()
959- self.assertIn(
960- '10.0.2.3', resolvconf_contents,
961- msg="10.0.2.3 not in resolvconf contents: \n{}".format(
962- resolvconf_contents))
963+ dns_addr = '10.1.1.4'
964+ rel_dir = '{}.d'.format(self.info['release'])
965+ dns_search = 'example.com'
966+ info = {'host': '10.1.1.2', 'dns': dns_addr,
967+ 'dnssearch': dns_search, 'network': '10.1.1.0/24'}
968+ netdev = ("--netdev=user,net={network},host={host},dns={dns},"
969+ "dnssearch={dnssearch}").format(**info)
970+
971+ output_disk = self.create_output_disk()
972+ cmd = [
973+ tgt_boot_cmd, '-v', netdev,
974+ '--disk=%s,serial=output-disk' % output_disk,
975+ '--user-data-add=%s' % self.info['user-data'],
976+ self.info['root_image'], self.info['kernel'],
977+ self.info['initrd']]
978+ sys.stderr.write(' '.join(cmd) + "\n")
979+
980+ env = os.environ.copy()
981+ env['BOOT_TIMEOUT'] = env.get('BOOT_TIMEOUT', '60m')
982+ subprocess.check_call(cmd, env=env)
983+
984+ files = self.extract_files(output_disk)
985+ print("files: %s" % files)
986+ resolvconf = files.get('resolv.conf', "NO_RESOLVCONF_FOUND")
987+ resolve_status = files.get('systemd-resolve-status.txt')
988+
989+ resolvconf_id = 'generated by resolvconf'
990+ resolved_addr = "127.0.0.53"
991+ if resolvconf_id in resolvconf:
992+ print("resolvconf manages resolvconf.\n")
993+ self.assertIn(
994+ dns_addr, resolvconf,
995+ msg = ("%s not in resolvconf contents: \n%s" %
996+ (dns_addr, resolvconf)))
997+ if dns_search in resolvconf:
998+ print("%s was found in resolv.conf." % dns_search)
999+ elif resolved_addr in resolvconf and dns_search in resolve_status:
1000+ # zesty has resolvconf and systemd-resolved.
1001+ print("%s was in resolve_status and %s in resolv.conf" %
1002+ (resolved_addr, dns_search))
1003+ else:
1004+ raise AssertionError(
1005+ "%s domain is not being searched." % dns_search)
1006+
1007+ else:
1008+ print("systemd-resolved managing resolve.conf\n")
1009+ self.assertIn(
1010+ resolved_addr, resolvconf,
1011+ msg="%s not in resolved resolv.conf: %s" % (resolved_addr,
1012+ resolvconf))
1013+ self.assertIn(dns_addr, resolve_status,
1014+ msg=("%s not in systemd-resolve status: %s" %
1015+ (dns_addr, resolve_status)))
1016+ self.assertIn(dns_search, resolve_status,
1017+ msg=("search addr '%s' not in systemd-resolve status: %s" %
1018+ (dns_search, resolve_status)))
1019+
1020+
1021+def get_image(top_d, release):
1022+ cmd = ['get-image', top_d, 'cloud-daily', release]
1023+ subprocess.check_call(cmd)
1024+
1025+
1026+def patch_image(image, packages=None, kernel=None, initrd=None):
1027+ '''Patch root-image with dep8 built open-iscsi package.'''
1028+
1029+ if packages is None:
1030+ # an empty 'packages' to patch-image still installs open-iscsi
1031+ packages = []
1032+
1033+ cmd = ['patch-image', image]
1034+ if kernel:
1035+ cmd.append('--kernel=%s' % kernel)
1036+ if initrd:
1037+ cmd.append('--initrd=%s' % initrd)
1038+ cmd.extend(packages)
1039+
1040+ subprocess.check_call(cmd)
1041
1042
1043 if __name__ == '__main__':
1044@@ -283,7 +399,7 @@ if __name__ == '__main__':
1045 suite = unittest.TestSuite()
1046 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(OpenIscsiTest))
1047 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
1048- MAASEphemeralTest))
1049+ CloudImageTest))
1050 rc = unittest.TextTestRunner(verbosity=2).run(suite)
1051 if not rc.wasSuccessful():
1052 sys.exit(1)
1053diff --git a/debian/tests/tgt-boot-test b/debian/tests/tgt-boot-test
1054index 538477c..6ed7a9b 100755
1055--- a/debian/tests/tgt-boot-test
1056+++ b/debian/tests/tgt-boot-test
1057@@ -1,27 +1,27 @@
1058 #!/bin/bash
1059
1060-## we run a python simple web server on this port.
1061-WPORT=${WPORT:-32600}
1062-## HOST_IP has to be an IP address that the vm will be able to access
1063-## ie, it can't be "127.0.0.1". If empty (default) then the ip from
1064-## the device HOST_IP_NIC (default=eth0) will be used.
1065-HOST_IP=${HOST_IP:-""}
1066-HOST_IP_NIC="eth0"
1067-## set OVERLAY_DISK in environment to get this.
1068-## set to 'temp' to just get a temp file to excercise that path
1069-## set to some other path and it will be created if non-existant
1070-## the overlay will then be put on this drive and read from there
1071-## instead of a tmpfs
1072-OVERLAY_DISK=${OVERLAY_DISK:-""}
1073-##
1074-## XKVM_BRIDGE: set to 'user' to use qemu user networking
1075-## set to name of a bridge such as virbr0 to use tap on that bridge
1076-XKVM_BRIDGE=${XKVM_BRIDGE:-"user"}
1077+VERBOSITY=0
1078+TEMP_D=""
1079+DEF_WPORT=32600
1080+IPV4_NET="10.0.12"
1081+DEF_NETDEV="user,net=${IPV4_NET}.0/24,host=${IPV4_NET}.2"
1082
1083+ISCSI_PORT=3260
1084 TGT_CONF=""
1085 HTTP_PID=""
1086 TGT_NAME=""
1087
1088+_DEF_CMDLINE_TMPL=(
1089+ nomodeset {icmdline_parms} {cmdline_ip} ro
1090+ net.ifnames=0 BOOTIF_DEFAULT=eth0
1091+ root={root} {overlay_drive}
1092+ console=ttyS0 {cmdline_ds}
1093+)
1094+DEF_CMDLINE_TMPL="${_DEF_CMDLINE_TMPL[*]}"
1095+
1096+error() { echo "$@" 1>&2; }
1097+fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
1098+
1099 cleanup() {
1100 [ ! -d "$TEMP_D" ] || rm -Rf "$TEMP_D"
1101 if [ -f "$TGT_CONF" ]; then
1102@@ -33,165 +33,500 @@ cleanup() {
1103 kill $HTTP_PID
1104 fi
1105 }
1106+
1107 Usage() {
1108- cat <<EOF
1109-${0##*/} image kernel initrd
1110+ cat <<EOF
1111+Usage: ${0##*/} [ options ] image kernel initrd
1112+
1113+ put 'image' into tgt, boot 'kernel' and 'initrd' in a kvm
1114+ guest pointing to that iscsi target.
1115
1116-puts 'image' into tgt, then boots kernel and initrd in kvm to use it as root.
1117+ options:
1118+ -p | --webserv-port PORT serve cloud-init seed on port PORT
1119+ default: ${DEF_WPORT}
1120+ -O | --overlay-disk DISK attach DISK to guest and use it for the overlayroot
1121+ target. Use 'temp' to use a temp file.
1122+ use an existing file to use that. Use a
1123+ non-existing file to create.
1124+ -n | --netdev DEV passed through to xkvm.
1125+ if none passed, default is:
1126+ $DEF_NETDEV
1127+ -a | --host-addr ADDR will be used for iscsi host address and
1128+ cloud-init seed address. Default is read
1129+ from 'host=' in --netdev arg.
1130+ -l | --serial-log FILE write serial log to FILE default is to let output
1131+ go to console.
1132+ --cmdline TMPL template for kernel command line.
1133+ default: ${DEF_CMDLINE_TMPL}
1134+ --cmdline-add ARGS add the args to the kernel command line. default: ""
1135+ --user-data FILE user-data to provide to instance.
1136+ default: builtin
1137+ --user-data-add FILE append FILE to default user-data.
1138 EOF
1139 }
1140-error() { echo "$@" 1>&2; }
1141-fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
1142
1143+bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
1144+
1145+debug() {
1146+ local level=${1}; shift;
1147+ [ "${level}" -gt "${VERBOSITY}" ] && return
1148+ error "${@}"
1149+}
1150
1151-[ "$1" = "-h" -o "$1" = "--help" ] && { Usage ; exit 0; }
1152-image_in=$1
1153-kernel_in=$2
1154-initrd_in=$3
1155-[ $# -ge 3 ] || { Usage 1>&2; fail "must give image, kernel, initrd"; }
1156-
1157-image=$(readlink -f "$image_in") ||
1158- fail "cannot get full path to $image_in"
1159-kernel=$(readlink -f "$kernel_in") ||
1160- fail "cannot get full path to $kernel_in"
1161-initrd=$(readlink -f "$initrd_in") ||
1162- fail "cannot get full path to $initrd_in"
1163-
1164-TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}-XXXXXX") || exit 1
1165-TEMP_D=$(cd "$TEMP_D" && pwd)
1166-trap cleanup EXIT
1167-
1168-t_image="${TEMP_D}/image"
1169-t_kernel="${TEMP_D}/kernel"
1170-t_initrd="${TEMP_D}/initrd"
1171-
1172-# this is the IP address of the tgtd
1173-if [ -n "$HOST_IP" ]; then
1174- ipaddr=${HOST_IP}
1175-else
1176- ipaddr=$(ifconfig ${HOST_IP_NIC} |
1177- sed -n '/inet addr/s/\([^:]*:\)\([^ ]*\)\( .*\)/\2/p')
1178- [ $? -eq 0 -a -n "$ipaddr" ] ||
1179- fail "failed to get ipaddr from '$HOST_IP_NIC'. set HOST_IP or HOST_IP_NIC"
1180-fi
1181-
1182-## set up a user-data and meta-data for cloud-init
1183-## and we'll seed that through the kernel command line
1184-##
1185-## The user data we provide will allow us to log in
1186-## as the 'ubuntu' user with 'passw0rd'.
1187-keys=$( (ssh-add -L 2>/dev/null ;
1188- [ -f ~/.ssh/id_rsa.pub ] && cat ~/.ssh/id_rsa.pub ) | sort -u )
1189-if [ -n "$keys" ]; then
1190- d=$(IFS=$'\n'; for i in ${keys}; do echo "'$i',"; done)
1191- keys="[${d%,}]"
1192-else
1193- keys="[]"
1194-fi
1195-mkdir "${TEMP_D}/ci-seed"
1196-cat > "${TEMP_D}/ci-seed/user-data" <<EOF
1197+write_userdata() {
1198+ ## set up a user-data and meta-data for cloud-init
1199+ ## and we'll seed that through the kernel command line
1200+ ##
1201+ ## The user data we provide will allow us to log in
1202+ ## as the 'ubuntu' user with 'passw0rd'.
1203+ local out="$1" keys="${2:-[]}"
1204+ cat > "$out" <<"EOF"
1205 #cloud-config
1206-password: passw0rd
1207-chpasswd: { expire: False }
1208+chpasswd:
1209+ expire: False
1210+ list: |
1211+ ubuntu:passw0rd
1212+ root:root
1213 ssh_pwauth: True
1214-ssh_authorized_keys: $keys
1215 EOF
1216+}
1217
1218-### XXX: matsubara fix user data to collect stuff
1219-## just an example of what you could do here.
1220-## added when debugging resolvconf LP: #1501033
1221-if true; then
1222- cat >> "${TEMP_D}/ci-seed/user-data" <<EOF
1223-runcmd:
1224- - [ mkdir, -p, /mnt/output ]
1225- - [ cp, /etc/resolv.conf, /mnt/output]
1226- - [ tar, -C, /mnt/output, -cf, /dev/vda, . ]
1227-power_state:
1228- mode: poweroff
1229- message: cloud-init finished. Shutting down.
1230- timeout: 60
1231-EOF
1232-fi
1233+get_ssh_pubkeys() {
1234+ # get users ssh pubkeys in a yaml list format
1235+ local keys="" d=""
1236+ keys=$( (ssh-add -L 2>/dev/null ;
1237+ [ -f ~/.ssh/id_rsa.pub ] && cat ~/.ssh/id_rsa.pub ) | sort -u )
1238+ if [ -n "$keys" ]; then
1239+ d=$(IFS=$'\n'; for i in ${keys}; do echo "'$i',"; done)
1240+ keys="[${d%,}]"
1241+ else
1242+ keys="[]"
1243+ fi
1244+ _RET="$keys"
1245+}
1246
1247-iid=$(uuidgen 2>/dev/null || echo i-abcdefg)
1248-cat > "${TEMP_D}/ci-seed/meta-data" <<EOF
1249-instance-id: $iid
1250+write_metadata() {
1251+ local iid="" out="$1" pubkeys="$2"
1252+ iid=$(uuidgen 2>/dev/null || echo i-abcdefg)
1253+ {
1254+ echo "instance-id: $iid"
1255+ if [ -n "$pubkeys" ]; then
1256+ echo "public-keys: $pubkeys"
1257+ fi
1258+ } > "$out"
1259+}
1260+
1261+register_image() {
1262+ local conf="$1" name="$2" image="$3"
1263+ sudo tee "$conf" >/dev/null <<EOF
1264+<target ${name}>
1265+ readonly 1
1266+ backing-store "$image"
1267+ allow-in-use yes
1268+</target>
1269 EOF
1270+ [ $? -eq 0 ] || { error "failed to write '$conf'"; return 1; }
1271
1272-## run a python web server to seed this
1273-( cd "${TEMP_D}/ci-seed" && exec python -m SimpleHTTPServer $WPORT ) &
1274-HTTP_PID=$!
1275+ sudo tgt-admin "--update=$name" ||
1276+ { error "failed tgt-admin update=$name"; return 1; }
1277+}
1278
1279-if [ "${image%.gz}" != "$image" ]; then
1280- error "uncompressing $image_in -> temp-dir"
1281- gzcat "$image" > "${TEMP_D}/image" ||
1282- fail "failed uncompress of $image_in"
1283-else
1284- ln -s "$image" "$t_image" || fail "symlink to $image failed"
1285-fi
1286+address_on_dev() {
1287+ local dev="$1" atype="${2:-inet}" out="" addr=""
1288+ out=$(ip address show dev "$dev" 2>/dev/null) ||
1289+ { error "could not get address on $1"; return 1; }
1290+ addr=$(echo "$out" |
1291+ awk '$1 == atype && $4 == "global" {
1292+ sub(/\/.*/, "", $2); print $2; exit(0); }' "atype=$atype" )
1293+ [ -n "$addr" ] || return
1294+ _RET="$addr"
1295+}
1296
1297-ln -s "$kernel" "$t_kernel" &&
1298- ln -s "$initrd" "$t_initrd" ||
1299- fail "failed link to kernel or initrd"
1300+find_host_addr() {
1301+ # run through --netdev args and fine 'host=' or make one up base
1302+ # on there being a net= and assume .2 for host.
1303+ # example: type=user,id=net00,net=10.0.12.0/24,host=10.0.12.2
1304+ # ipv6-net=fec0::/64,ipv6-host=fec0::2
1305+ # lxcbr0
1306+ local nd="" atype="" bridge=""
1307+ local oifs="$IFS"
1308+ for nd in "$@"; do
1309+ local host="" netaddr="" cur="" ipv4="x" ipv6="x" ntype=""
1310+ bridge=${nd%%,*}
1311+ if [ "$bridge" != "user" -a -e "/sys/class/net/$bridge" ]; then
1312+ for atype in inet inet6; do
1313+ address_on_dev "$bridge" "$atype" || continue
1314+ debug 2 "took $atype address on bridge '$bridge': $_RET";
1315+ return 0;
1316+ done
1317+ fi
1318+ cur=${nd}
1319+ while :; do
1320+ first=${cur%%,*}
1321+ rest=${cur#*,}
1322+ case "$first" in
1323+ user) ntype=user;;
1324+ ipv4=on|ipv4) ipv4=on;;
1325+ ipv4=off) ipv4=off;;
1326+ ipv6=on|ipv6) ipv6=on;;
1327+ ipv6=off) ipv6=off;;
1328+ ipv6-net=*) tmp=${first#*=};
1329+ tmp=${tmp%/*}
1330+ netaddr="${tmp%.*}:2"
1331+ ;;
1332+ net=*) tmp=${first#*=};
1333+ tmp=${tmp%/*}
1334+ netaddr="${tmp%.*}.2"
1335+ ;;
1336+ host=*|ipv6-host=) host=${first#*=};;
1337+ esac
1338+ if [ "$first" = "$rest" ]; then
1339+ # there was no ','
1340+ break
1341+ fi
1342+ cur=$rest
1343+ done
1344+ local found="" msg=""
1345+ if [ -n "$host" ]; then
1346+ found="$host"
1347+ msg="host= in '$nd'"
1348+ elif [ -n "$netaddr" ]; then
1349+ found="$netaddr"
1350+ msg="second address on net= in '$nd'";
1351+ elif [ "$ntype" = "user" ]; then
1352+ case "$ipv4/$ipv6" in
1353+ */on)
1354+ msg="known user net default host in ipv6 in '$nd'"
1355+ found="fec0::2";;
1356+ x/x|on/*)
1357+ msg="known user net default host in ipv4 in '$nd'"
1358+ found="10.0.2.2";;
1359+ esac
1360+ fi
1361+ if [ -n "$found" ]; then
1362+ debug 2 "found host address '$found' via $msg"
1363+ _RET="$found"
1364+ return 0
1365+ fi
1366+ done
1367+ return 1
1368+}
1369
1370
1371-TGT_NAME=${TEMP_D##*/}
1372-TGT_CONF="/etc/tgt/conf.d/$TGT_NAME.conf"
1373+get_rfc4173() {
1374+ # protocol would normally be '6' (tcp but empty is fine)
1375+ # https://tools.ietf.org/html/rfc4173
1376+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=804162
1377+ local server="$1" name="$2" port="$3" lun="$4" user="$5" pass="$6" proto="$7"
1378+ local rfc="" auth="${user:+${user}${pass:+:${pass}}}"
1379+ rfc="iscsi:"
1380+ if [ -n "${user}" ]; then
1381+ rfc="$rfc:${user}${pass:+:${pass}}"
1382+ fi
1383+ case "$server" in
1384+ \[*\]) :;;
1385+ *:*) server="[$server]";;
1386+ esac
1387+ rfc="${server}:$proto:${port}:${lun}:${name}"
1388+ _RET="$rfc"
1389+}
1390
1391-sudo tee "$TGT_CONF" >/dev/null <<EOF
1392-<target ${TGT_NAME}>
1393- readonly 1
1394- backing-store "$t_image"
1395- allow-in-use yes
1396-</target>
1397+get_root_parm() {
1398+ local server="$1" name="$2" port="${3:-$ISCSI_PORT}"
1399+ local lun="$4" user="$5" pass="$6" proto="$7" part="$8"
1400+ local ret=""
1401+ # for ipv6 addresses, path just contains : for the server
1402+ _RET="/dev/disk/by-path/ip-$server:$port-iscsi-${name}-lun-${lun}${part:+-part$part}"
1403+}
1404+
1405+get_cmdline_params() {
1406+ local server="$1" name="$2" port="$3" lun="$4" user="$5" pass="$6"
1407+ local proto="$7" initiator="$8" ret=""
1408+ ret="${initiator:+iscsi_initiator=${initiator} }"
1409+ ret="${ret}iscsi_target_name=$name "
1410+ ret="${ret}iscsi_target_ip=$server "
1411+ ret="${ret}${port:+iscsi_target_port=$port }"
1412+ ret="${ret}${initiator:+iscsi_initiator=$initiator }"
1413+ _RET=${ret% }
1414+}
1415+
1416+get_iscsi_cmd() {
1417+ local server="$1" name="$2" port="$3" lun="$4" user="$5" pass="$6"
1418+ local proto="$7" initiator="$8" ret="" group=${9:-1}
1419+ _RET=( iscsistart -i "$initiator"
1420+ -t "$name" -g "$group" -a "$server" -p "$port"
1421+ ${user:+-u "$user"} ${pass:+-w "${pass}"} )
1422+}
1423+
1424+serv_http() {
1425+ local dir="$1" port="$2" check="$3"
1426+ local python="" kid=""
1427+ command -v python3 >/dev/null 2>&1 && python=python3 || python=python
1428+ cat > "$TEMP_D/webserv" <<EOF
1429+import os, socket, sys
1430+try:
1431+ from BaseHTTPServer import HTTPServer
1432+ from SimpleHTTPServer import SimpleHTTPRequestHandler
1433+except ImportError:
1434+ from http.server import HTTPServer, SimpleHTTPRequestHandler
1435+
1436+class HTTPServerV6(HTTPServer):
1437+ address_family = socket.AF_INET6
1438+
1439+if __name__ == "__main__":
1440+ dir = sys.argv[1]
1441+ port = int(sys.argv[2])
1442+ os.chdir(dir)
1443+ server = HTTPServerV6(("::", port), SimpleHTTPRequestHandler)
1444+ server.serve_forever()
1445 EOF
1446-[ $? -eq 0 ] || fail "failed to write '$TGT_CONF'"
1447-
1448-sudo tgt-admin "--update=${TGT_NAME}" || fail "failed tgt-admin update=$TGT_NAME"
1449-
1450-overlay_disk=${OVERLAY_DISK}
1451-overlay_drive_kernel="overlayroot=tmpfs"
1452-overlay_drive_xkvm=""
1453-if [ -n "$overlay_disk" ]; then
1454- if [ "${overlay_disk}" = "temp" ]; then
1455- overlay_disk="${TEMP_D}/overlay.img"
1456- fi
1457- if [ ! -f "$overlay_disk" ]; then
1458- qemu-img create -f raw "$overlay_disk" 1G
1459- mkfs.ext2 -L "delta" -F "$overlay_disk" >/dev/null 2>&1 ||
1460- fail "failed mkfs on $overlay_disk"
1461- overlay_drive_kernel="overlayroot=device:dev=LABEL=delta"
1462- overlay_drive_xkvm="--disk=${overlay_disk},format=raw"
1463- fi
1464-fi
1465-
1466-## Now boot a kvm from it
1467-iport="3260"
1468-root=LABEL=cloudimg-rootfs
1469-root=/dev/disk/by-path/ip-$ipaddr:$iport-iscsi-${TGT_NAME}-lun-1
1470-parms=(
1471- nomodeset
1472- iscsi_target_name=${TGT_NAME} iscsi_target_ip=$ipaddr
1473- iscsi_target_port=3260
1474- iscsi_initiator=maas-enlist
1475- ip=::::maas-enlist:BOOTIF ro
1476- net.ifnames=0
1477- BOOTIF_DEFAULT=eth0
1478- "root=${root}"
1479- ${overlay_drive_kernel}
1480- console=ttyS0 console=tty1
1481- "ds=nocloud-net;seedfrom=http://$ipaddr:$WPORT/"
1482-)
1483+ $python "$TEMP_D/webserv" "$dir" "$port" &
1484+ HTTP_PID=$!
1485+ local i=0 site="127.0.0.1"
1486+ local url="http://$site:$port/$check"
1487+ while [ -d /proc/$HTTP_PID -a "$i" -lt 10 ] && i=$(($i+1)); do
1488+ no_proxy=$site wget -q -O /dev/null "$url" && return 0
1489+ sleep .5
1490+ done
1491+ return 1
1492+}
1493+
1494+render_templ() {
1495+ local tmpl="$1"
1496+ shift
1497+ local key val i="" cur="$tmpl"
1498+ for i in "$@"; do
1499+ key=${i%%=*}
1500+ val=${i#*=}
1501+ cur=${cur//\{${key}\}/${val}}
1502+ done
1503+ _RET="$cur"
1504+}
1505+
1506+get_partition() {
1507+ # return in _RET the 'auto' partition for a image.
1508+ # return partition number for a partitioned image
1509+ # return 0 for unpartitioned
1510+ # return 0 if image is partitioned, 1 if not
1511+ local img="$1"
1512+ out=$(LANG=C sfdisk --list -uS "$img" 2>&1) || {
1513+ error "failed determining if partitioned: $out";
1514+ return 1;
1515+ }
1516+ if echo "$out" | grep -q 'Device.*Start.*End'; then
1517+ _RET=1
1518+ else
1519+ _RET=0
1520+ fi
1521+}
1522+
1523+main() {
1524+ local short_opts="a:hn:O:p:l:v"
1525+ local long_opts="help,cmdline:,cmdline-add:,cmdline-ip:,disk:,host-addr:,overlay-disk:,netdev:,serial-log:,user-data:,user-data-add:,verbose,webserv-port:"
1526+ local getopt_out=""
1527+
1528+ getopt_out=$(getopt --name "${0##*/}" \
1529+ --options "${short_opts}" --long "${long_opts}" -- "$@") &&
1530+ eval set -- "${getopt_out}" ||
1531+ { bad_Usage; return; }
1532+
1533+ local cur="" next=""
1534+ local overlay_disk="" webserv_port="${DEF_WPORT}" netdevs="" host_addr=""
1535+ local serial_log="" cmdline_tmpl="${DEF_CMDLINE_TMPL}" user_data_file=""
1536+ local cmdline_add="" user_data_add="" pt=""
1537+ # The string 'BOOTIF' is replaced at runtime with the interface
1538+ # name by cloud-initramfs-dyn-netconf.
1539+ local cmdline_ip="ip=::::{iinitiator}:BOOTIF"
1540+ netdevs=( )
1541+ pt=( )
1542+
1543+ while [ $# -ne 0 ]; do
1544+ cur="$1"; next="$2";
1545+ case "$cur" in
1546+ -h|--help) Usage ; exit 0;;
1547+ --cmdline) cmdline_tmpl=$next; shift;;
1548+ --cmdline-add) cmdline_add=$next; shift;;
1549+ --cmdline-ip) cmdline_ip=$next; shift;;
1550+ --disk) pt[${#pt[@]}]="$1=$2"; shift;;
1551+ -a|--host-addr) host_addr=$next; shift;;
1552+ -O|--overlay-disk) overlay_disk=$next; shift;;
1553+ -n|--netdev) netdevs[${#netdevs[@]}]="$next"; shift;;
1554+ -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
1555+ -l|--serial-log) serial_log="$next"; shift;;
1556+ --user-data-file) user_data_file="$next"; shift;;
1557+ --user-data-add) user_data_add="$next"; shift;;
1558+ -p|--webserv-port) webserv_port=$port; shift;;
1559+ --) shift; break;;
1560+ esac
1561+ shift;
1562+ done
1563+
1564+ [ $# -eq 3 ] || { bad_Usage "expected 3 args, got $# ($*)"; return 1; }
1565+ local image_in=$1 kernel_in=$2 initrd_in=$3
1566+ for f in "$image_in" "$kernel_in" "$initrd_in"; do
1567+ [ -f "$f" ] || fail "$f: not a file"
1568+ done
1569+
1570+ local image="" kernel="" initrd=""
1571+ image=$(readlink -f "$image_in") ||
1572+ fail "cannot get full path to $image_in"
1573+ kernel=$(readlink -f "$kernel_in") ||
1574+ fail "cannot get full path to $kernel_in"
1575+ initrd=$(readlink -f "$initrd_in") ||
1576+ fail "cannot get full path to $initrd_in"
1577+
1578+ TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
1579+ fail "failed to make tempdir"
1580+ TEMP_D=$(cd "$TEMP_D" && pwd)
1581+ trap cleanup EXIT
1582+
1583+ local t_image t_kernel t_initrd
1584+ t_image="${TEMP_D}/image"
1585+ t_kernel="${TEMP_D}/kernel"
1586+ t_initrd="${TEMP_D}/initrd"
1587+
1588+ if [ "${#netdevs[@]}" -eq 0 ]; then
1589+ netdevs=( "$DEF_NETDEV" )
1590+ fi
1591+
1592+ local keys=""
1593+ mkdir -p "$TEMP_D/ci-seed"
1594+ if [ -n "${user_data_file}" ]; then
1595+ cat "${user_data_file}" > "$TEMP_D/ci-seed/user-data" ||
1596+ fail "failed to copy '$user_data_file' to ci-seed/user-data"
1597+ else
1598+ write_userdata "$TEMP_D/ci-seed/user-data" ||
1599+ fail "failed to write user-data"
1600+ fi
1601+ if [ -n "$user_data_add" ]; then
1602+ cat "$user_data_add" >> "$TEMP_D/ci-seed/user-data" ||
1603+ fail "failed to write additional user-data"
1604+ fi
1605+
1606+ get_ssh_pubkeys || fail "failed getting pubkeys"
1607+ keys="$_RET"
1608+ write_metadata "$TEMP_D/ci-seed/meta-data" "$keys" ||
1609+ fail "failed writing metadata"
1610+
1611+ if [ -z "$host_addr" ]; then
1612+ find_host_addr "${netdevs[@]}" ||
1613+ fail "failed to find host address. try --host-addr."
1614+ host_addr="$_RET"
1615+ fi
1616+ debug 1 "using host address: '${host_addr}'"
1617+
1618+ ## run a python web server to seed this
1619+ serv_http "$TEMP_D/ci-seed" "$webserv_port" "user-data" ||
1620+ fail "failed to web serv $TEMP_D/ci-seed on $webserv_port"
1621+ debug 1 "web server in pid $HTTP_PID serving ${TEMP_D}/ci-seed/ on port $webserv_port"
1622+ local seed_url="http://$host_addr:$webserv_port/"
1623+ if [ "${host_addr#*:}" != "$host_addr" ]; then
1624+ seed_url="http://[$host_addr]:$webserv_port/"
1625+ fi
1626+ debug 1 " ${seed_url}meta-data"
1627+
1628+ if [ "${image%.gz}" != "$image" ]; then
1629+ error "uncompressing $image_in -> temp-dir"
1630+ gzcat "$image" > "${TEMP_D}/image" ||
1631+ fail "failed uncompress of $image_in"
1632+ else
1633+ ln -s "$image" "$t_image" || fail "symlink to $image failed"
1634+ fi
1635+
1636+ ln -s "$kernel" "$t_kernel" &&
1637+ ln -s "$initrd" "$t_initrd" ||
1638+ fail "failed link to kernel or initrd"
1639+
1640+ local partition=""
1641+ get_partition "$t_image" ||
1642+ fail "failed to read partition from $image"
1643+ partition=${_RET}
1644+ [ "$part" = "0" ] && partition=""
1645+
1646+ TGT_NAME=${TEMP_D##*/}
1647+ TGT_NAME=${TGT_NAME//./-}
1648+ TGT_CONF="/etc/tgt/conf.d/$TGT_NAME.conf"
1649+ register_image "$TGT_CONF" "$TGT_NAME" "$t_image" ||
1650+ fail "failed to register image $t_image in tgt"
1651+
1652+ overlay_drive_kernel="overlayroot=tmpfs"
1653+ overlay_drive_xkvm=""
1654+ if [ -n "${overlay_disk}" ]; then
1655+ if [ "${overlay_disk}" = "temp" ]; then
1656+ overlay_disk="${TEMP_D}/overlay.img"
1657+ fi
1658+ if [ ! -f "$overlay_disk" ]; then
1659+ qemu-img create -f raw "$overlay_disk" 1G
1660+ mkfs.ext2 -L "delta" -F "$overlay_disk" >/dev/null 2>&1 ||
1661+ fail "failed mkfs on $overlay_disk"
1662+ overlay_drive_kernel="overlayroot=device:dev=LABEL=delta"
1663+ overlay_drive_xkvm="--disk=${overlay_disk},format=raw"
1664+ fi
1665+ fi
1666+
1667+ local iserver="${host_addr}" iname="$TGT_NAME" iport="$ISCSI_PORT"
1668+ local ilun="1" iuser="" ipass="" iproto="" iinitiator="maas-enlist"
1669+ local rfc="" root="" iscsi_cmdline_parms="" iscsi_start_cmd=""
1670+ local cmdline=""
1671+ get_rfc4173 "${iserver}" "$iname" "$iport" "$ilun" \
1672+ "$iuser" "$ipass" "$iproto"
1673+ rfc="$_RET"
1674+
1675+ get_cmdline_params "${iserver}" "$iname" "$iport" "$ilun" \
1676+ "$iuser" "$ipass" "$iproto" "$iinitiator"
1677+ iscsi_cmdline_parms="$_RET"
1678+
1679+ get_root_parm "$iserver" "$iname" "$iport" "$ilun" \
1680+ "$iuser" "$ipass" "$iproto" "$partition"
1681+ root="$_RET"
1682+
1683+ get_iscsi_cmd "$iserver" "$iname" "$iport" "$ilun" \
1684+ "$iuser" "$ipass" "$iproto" "$iinitiator"
1685+ iscsi_start_cmd=( "${_RET[@]}" )
1686+
1687+ render_templ "${cmdline_ip}" iinitiator="$iinitiator" ||
1688+ { error "failed rendering cmdline ip: ${cmdline_ip}"; return 1; }
1689+ cmdline_ip="$_RET"
1690+
1691+ render_templ "$cmdline_tmpl${cmdline_add:+ ${cmdline_add}}" \
1692+ iserver="$host_addr" iname="$TGT_NAME" iport="$ISCSI_PORT" \
1693+ ilun="$ilun" iuser="$iuser" ipass="$ipass" iproto="$iproto" \
1694+ iinitiator="$iinitiator" "rfc4173=$rfc" "root=$root" \
1695+ "seed_url=$seed_url" icmdline_parms="$iscsi_cmdline_parms" \
1696+ "overlay_drive=${overlay_drive_kernel}" \
1697+ "cmdline_ds=ds=nocloud-net;seedfrom=${seed_url}" \
1698+ "cmdline_ip=${cmdline_ip}" ||
1699+ { error "failed rendering cmdline"; return 1; }
1700+ cmdline="$_RET"
1701+
1702+ debug 1 "rfc4173: $rfc"
1703+ debug 1 "iscsi_cmdline_parms: $iscsi_cmdline_parms"
1704+ debug 1 "root: $root"
1705+ debug 1 "iscsi_start_cmd: ${iscsi_start_cmd[*]}"
1706+ debug 1 "cmdline=$cmdline"
1707+
1708+ local netdev_args=""
1709+ netdev_args=( )
1710+ for i in "${netdevs[@]}"; do
1711+ netdev_args[${#netdev_args[@]}]="--netdev=$i"
1712+ done
1713+
1714+ local mem="512" start="$SECONDS" ret=""
1715+ local cmd=""
1716+ cmd=(
1717+ ${BOOT_TIMEOUT:+timeout --kill-after=1m --signal=TERM $BOOT_TIMEOUT}
1718+ xkvm -v -v "${netdev_args[@]}" ${overlay_drive_xkvm} "${pt[@]}" --
1719+ -echr 0x5
1720+ -m $mem ${serial_log:+-serial "file:$serial_log"} -nographic
1721+ -kernel "${t_kernel}" -initrd "${t_initrd}"
1722+ -append "${cmdline}"
1723+ )
1724+ debug 1 "executing: ${cmd[*]}"
1725+ "${cmd[@]}"
1726+ ret=$?
1727+ debug 1 "xkvm returned $ret in $((SECONDS-start))s"
1728+ return $ret
1729+}
1730
1731-## The console is set to ttyS0, and serial output is
1732-## logged to the file 'serial.log'
1733-## in the ncurses path, you'll eventually get a login prompt
1734-## but you can also ssh in.
1735-[ "${NO_LOG:-0}" = "0" ] && serial_log="serial" || serial_log=""
1736-mem="1024"
1737-xkvm --netdev ${XKVM_BRIDGE} \
1738- ${overlay_drive_xkvm} \
1739- ${OUTPUT_DISK:+--disk "$OUTPUT_DISK"} --\
1740- -m 1024 ${serial_log:+-serial "file:$serial_log"} -nographic \
1741- -kernel "${t_kernel}" -initrd "${t_initrd}" \
1742- -append "${parms[*]}"
1743+main "$@"
1744+# vi: ts=4 expandtab
1745diff --git a/debian/tests/xkvm b/debian/tests/xkvm
1746index e260988..9513723 100755
1747--- a/debian/tests/xkvm
1748+++ b/debian/tests/xkvm
1749@@ -11,6 +11,7 @@ TAPDEVS=( )
1750 # OVS_CLEANUP gets populated with bridge:devname pairs used with ovs
1751 OVS_CLEANUP=( )
1752 MAC_PREFIX="52:54:00:12:34"
1753+KVM="kvm"
1754 declare -A KVM_DEVOPTS
1755
1756 error() { echo "$@" 1>&2; }
1757@@ -27,7 +28,10 @@ randmac() {
1758
1759 cleanup() {
1760 [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
1761- [ -z "${KVM_PID}" ] || kill "$KVM_PID"
1762+ if [ -n "${KVM_PID}" ]; then
1763+ debug 1 "${0##*/} cleanup killing kvm pid '$KVM_PID'"
1764+ kill "$KVM_PID"
1765+ fi
1766 if [ ${#TAPDEVS[@]} -ne 0 ]; then
1767 local name item
1768 for item in "${TAPDEVS[@]}"; do
1769@@ -74,6 +78,10 @@ Usage: ${0##*/} [ options ] -- kvm-args [ ... ]
1770 -d | --disk DISK.img attach DISK.img as a disk (via virtio)
1771 --dry-run only report what would be done
1772
1773+ --uefi boot with efi
1774+ --uefi-nvram=FILE boot with efi, using nvram settings in FILE
1775+ if FILE not present, copy from defaults.
1776+
1777 NETDEV:
1778 Above, 'NETDEV' is a comma delimited string
1779 The first field must be
1780@@ -102,13 +110,13 @@ isdevopt() {
1781 local model="$1" input="${2%%=*}"
1782 local out="" opt="" opts=()
1783 if [ -z "${KVM_DEVOPTS[$model]}" ]; then
1784- out=$(kvm -device "$model,?" 2>&1) &&
1785- out=$(echo "$out" | sed -e "s,${model}[.],," -e 's,=.*,,') &&
1786+ out=$($KVM -device "$model,?" 2>&1) &&
1787+ out=$(echo "$out" | sed -e "s,[^.]*[.],," -e 's,=.*,,') &&
1788 KVM_DEVOPTS[$model]="$out" ||
1789 { error "bad device model $model?"; exit 1; }
1790 fi
1791 opts=( ${KVM_DEVOPTS[$model]} )
1792- for opt in "${KVM_DEVOPTS[@]}"; do
1793+ for opt in "${opts[@]}"; do
1794 [ "$input" = "$opt" ] && return 0
1795 done
1796 return 1
1797@@ -223,19 +231,126 @@ ovs_cleanup() {
1798 return $errors
1799 }
1800
1801+quote_cmd() {
1802+ local quote='"' x="" vline=""
1803+ for x in "$@"; do
1804+ if [ "${x#* }" != "${x}" ]; then
1805+ if [ "${x#*$quote}" = "${x}" ]; then
1806+ x="\"$x\""
1807+ else
1808+ x="'$x'"
1809+ fi
1810+ fi
1811+ vline="${vline} $x"
1812+ done
1813+ echo "$vline"
1814+}
1815+
1816+get_bios_opts() {
1817+ # get_bios_opts(bios, uefi, nvram)
1818+ # bios is a explicit bios to boot.
1819+ # uefi is boolean indicating uefi
1820+ # nvram is optional and indicates that ovmf vars should be copied
1821+ # to that file if it does not exist. if it exists, use it.
1822+ local bios="$1" uefi="${2:-false}" nvram="$3"
1823+ local ovmf_dir="/usr/share/OVMF"
1824+ local bios_opts="" pflash_common="if=pflash,format=raw"
1825+ unset _RET
1826+ _RET=( )
1827+ if [ -n "$bios" ]; then
1828+ _RET=( -drive "${pflash_common},file=$bios" )
1829+ return 0
1830+ elif ! $uefi; then
1831+ return 0
1832+ fi
1833+
1834+ # ovmf in older releases (14.04) shipped only a single file
1835+ # /usr/share/ovmf/OVMF.fd
1836+ # newer ovmf ships split files
1837+ # /usr/share/OVMF/OVMF_CODE.fd
1838+ # /usr/share/OVMF/OVMF_VARS.fd
1839+ # with single file, pass only one file and read-write
1840+ # with split, pass code as readonly and vars as read-write
1841+ local joined="/usr/share/ovmf/OVMF.fd"
1842+ local code="/usr/share/OVMF/OVMF_CODE.fd"
1843+ local vars="/usr/share/OVMF/OVMF_VARS.fd"
1844+ local split="" nvram_src=""
1845+ if [ -e "$code" -o -e "$vars" ]; then
1846+ split=true
1847+ nvram_src="$vars"
1848+ elif [ -e "$joined" ]; then
1849+ split=false
1850+ nvram_src="$joined"
1851+ elif [ -n "$nvram" -a -e "$nvram" ]; then
1852+ error "WARN: nvram given, but did not find expected ovmf files."
1853+ error " assuming this is code and vars (OVMF.fd)"
1854+ split=false
1855+ else
1856+ error "uefi support requires ovmf bios: apt-get install -qy ovmf"
1857+ return 1
1858+ fi
1859+
1860+ if [ -n "$nvram" ]; then
1861+ if [ ! -f "$nvram" ]; then
1862+ cp "$nvram_src" "$nvram" ||
1863+ { error "failed copy $nvram_src to $nvram"; return 1; }
1864+ debug 1 "copied $nvram_src to $nvram"
1865+ fi
1866+ else
1867+ debug 1 "uefi without --uefi-nvram storage." \
1868+ "nvram settings likely will not persist."
1869+ nvram="${nvram_src}"
1870+ fi
1871+
1872+ if [ ! -w "$nvram" ]; then
1873+ debug 1 "nvram file ${nvram} is readonly"
1874+ nvram_ro="readonly"
1875+ fi
1876+
1877+ if $split; then
1878+ # to ensure bootability firmware must be first, then variables
1879+ _RET=( -drive "${pflash_common},file=$code,readonly" )
1880+ fi
1881+ _RET=( "${_RET[@]}"
1882+ -drive "${pflash_common},file=$nvram${nvram_ro:+,${nvram_ro}}" )
1883+}
1884+
1885+should_try_kvm() {
1886+ local name="should_try_kvm" msg="set _USE_KVM=1 to force."
1887+ case "${_USE_KVM-notset}" in
1888+ 0) debug 1 "$name = no. _USE_KVM=${_USE_KVM}"; return 1;;
1889+ 1) debug 1 "$name = yes. _USE_KVM=${_USE_KVM}"; return 0;;
1890+ notset) :;;
1891+ *) debug 1 "$name = no. unknown _USE_KVM=${_USE_KVM}"; return 1;;
1892+ esac
1893+ local virt="no-detect-virt"
1894+ if command -v systemd-detect-virt >/dev/null; then
1895+ virt="$(systemd-detect-virt --vm)"
1896+ fi
1897+ case "$virt" in
1898+ none) :;;
1899+ kvm) debug 1 "$name = no. virt=$virt (nested kvm is finicky). $msg";
1900+ return 1;;
1901+ *) debug 1 "$name = no. virt=$virt. $msg"; return 1;;
1902+ esac
1903+ debug 1 "$name = yes."
1904+ return 0
1905+}
1906+
1907 main() {
1908 local short_opts="hd:n:v"
1909- local long_opts="help,dowait,disk:,dry-run,kvm:,no-dowait,netdev:,verbose"
1910+ local long_opts="bios:,help,dowait,disk:,dry-run,kvm:,no-dowait,netdev:,uefi,uefi-nvram:,verbose"
1911 local getopt_out=""
1912 getopt_out=$(getopt --name "${0##*/}" \
1913 --options "${short_opts}" --long "${long_opts}" -- "$@") &&
1914 eval set -- "${getopt_out}" || { bad_Usage; return 1; }
1915
1916- local bridge="$DEF_BRIDGE"
1917+ local bridge="$DEF_BRIDGE" oifs="$IFS"
1918 local netdevs="" need_tap="" ret="" p="" i="" pt="" cur="" conn=""
1919 local kvm="" kvmcmd="" archopts=""
1920- local def_diskif=${DEF_DISKIF:-"virtio"}
1921+ local def_disk_driver=${DEF_DISK_DRIVER:-"virtio-blk"}
1922 local def_netmodel=${DEF_NETMODEL:-"virtio-net-pci"}
1923+ local bios="" uefi=false uefi_nvram=""
1924
1925 archopts=( )
1926 kvmcmd=( )
1927@@ -259,13 +374,16 @@ main() {
1928 -h|--help) Usage; exit 0;;
1929 -d|--disk)
1930 diskdevs[${#diskdevs[@]}]="$next"; shift;;
1931- --dry-run) DRY_RUN=true;;
1932- --kvm) kvm="$next"; shift;;
1933+ --dry-run) DRY_RUN=true;;
1934+ --kvm) kvm="$next"; shift;;
1935 -n|--netdev)
1936 netdevs[${#netdevs[@]}]=$next; shift;;
1937 -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
1938- --dowait) dowait=true;;
1939- --no-dowait) dowait=false;;
1940+ --dowait) dowait=true;;
1941+ --no-dowait) dowait=false;;
1942+ --bios) bios="$next"; shift;;
1943+ --uefi) uefi=true;;
1944+ --uefi-nvram) uefi=true; uefi_nvram="$next"; shift;;
1945 --) shift; break;;
1946 esac
1947 shift;
1948@@ -274,7 +392,7 @@ main() {
1949 [ ${#netdevs[@]} -eq 0 ] && netdevs=( "${DEF_BRIDGE}" )
1950 pt=( "$@" )
1951
1952- local kvm_pkg=""
1953+ local kvm_pkg="" virtio_scsi_bus="virtio-scsi-pci"
1954 [ -n "$kvm" ] && kvm_pkg="none"
1955 case $(uname -m) in
1956 i?86)
1957@@ -285,28 +403,119 @@ main() {
1958 [ -n "$kvm" ] ||
1959 { kvm="qemu-system-x86_64"; kvm_pkg="qemu-system-x86"; }
1960 ;;
1961+ s390x)
1962+ [ -n "$kvm" ] ||
1963+ { kvm="qemu-system-s390x"; kvm_pkg="qemu-system-misc"; }
1964+ def_netmodel=${DEF_NETMODEL:-"virtio-net-ccw"}
1965+ virtio_scsi_bus="virtio-scsi-ccw"
1966+ ;;
1967 ppc64*)
1968 [ -n "$kvm" ] ||
1969 { kvm="qemu-system-ppc64"; kvm_pkg="qemu-system-ppc"; }
1970- def_netmodel="spapr-vlan"
1971+ def_netmodel="virtio-net-pci"
1972 # virtio seems functional on in 14.10, but might want scsi here
1973 #def_diskif="scsi"
1974 archopts=( "${archopts[@]}" -machine pseries,usb=off )
1975 archopts=( "${archopts[@]}" -device spapr-vscsi )
1976 ;;
1977- *) kvm=qemu-system-$(uname -r);;
1978+ *) kvm=qemu-system-$(uname -m);;
1979 esac
1980- kvmcmd=( $kvm -enable-kvm )
1981+ KVM="$kvm"
1982+ kvmcmd=( $kvm )
1983+ if should_try_kvm; then
1984+ if [ ! -e /dev/kvm ]; then
1985+ # if /dev/kvm didn't exist, then try to make it so.
1986+ local mod=""
1987+ for mod in kvm-intel kvm-amd kvm-hv kvm-pv; do
1988+ modprobe --quiet $mod && [ -w /dev/kvm ] &&
1989+ debug 1 "loaded module $mod and got kvm" && break
1990+ done
1991+ fi
1992+ if [ -w /dev/kvm ]; then
1993+ kvmcmd[${#kvmcmd[@]}]="-enable-kvm"
1994+ else
1995+ error "WARNING: no kvm support it seems."
1996+ fi
1997+ fi
1998
1999- local out="" fmt=""
2000- for i in "${diskdevs[@]}"; do
2001- if [ -f "$i" ]; then
2002- out=$(LANG=C qemu-img info "$i") &&
2003+ local bios_opts=""
2004+ if [ -n "$bios" ] && $uefi; then
2005+ error "--uefi (or --uefi-nvram) is incompatible with --bios"
2006+ return 1
2007+ fi
2008+ get_bios_opts "$bios" "$uefi" "$uefi_nvram" ||
2009+ { error "failed to get bios opts"; return 1; }
2010+ bios_opts=( "${_RET[@]}" )
2011+
2012+ local out="" fmt="" bus="" unit="" index="" serial="" driver="" devopts=""
2013+ local busorindex="" driveopts="" cur="" val="" file=""
2014+ for((i=0;i<${#diskdevs[@]};i++)); do
2015+ cur=${diskdevs[$i]}
2016+ IFS=","; set -- $cur; IFS="$oifs"
2017+ driver=""
2018+ id=$(printf "disk%02d" "$i")
2019+ file=""
2020+ fmt=""
2021+ bus=""
2022+ unit=""
2023+ index=""
2024+ serial=""
2025+ for tok in "$@"; do
2026+ [ "${tok#*=}" = "${tok}" -a -f "${tok}" -a -z "$file" ] && file="$tok"
2027+ val=${tok#*=}
2028+ case "$tok" in
2029+ driver=*) driver=$val;;
2030+ if=virtio) driver=virtio-blk;;
2031+ if=scsi) driver=scsi-hd;;
2032+ if=pflash) driver=;;
2033+ if=sd|if=mtd|floppy) fail "do not know what to do with $tok on $cur";;
2034+ id=*) id=$val;;
2035+ file=*) file=$val;;
2036+ fmt=*|format=*) fmt=$val;;
2037+ serial=*) serial=$val;;
2038+ bus=*) bus=$val;;
2039+ unit=*) unit=$val;;
2040+ index=*) index=$val;;
2041+ esac
2042+ done
2043+ [ -z "$file" ] && fail "did not read a file from $cur"
2044+ if [ -f "$file" -a -z "$fmt" ]; then
2045+ out=$(LANG=C qemu-img info "$file") &&
2046 fmt=$(echo "$out" | awk '$0 ~ /^file format:/ { print $3 }') ||
2047- { error "failed to determine format of $i"; return 1; }
2048+ { error "failed to determine format of $file"; return 1; }
2049+ else
2050+ fmt=raw
2051 fi
2052- diskargs[${#diskargs[@]}]="-drive";
2053- diskargs[${#diskargs[@]}]="if=${def_diskif},file=$i,format=$fmt";
2054+ if [ -z "$driver" ]; then
2055+ driver="$def_disk_driver"
2056+ fi
2057+ if [ -z "$serial" ]; then
2058+ serial="${file##*/}"
2059+ fi
2060+
2061+ # make sure we add either bus= or index=
2062+ if [ -n "$bus" -o "$unit" ] && [ -n "$index" ]; then
2063+ fail "bus and index cant be specified together: $cur"
2064+ elif [ -z "$bus" -a -z "$unit" -a -z "$index" ]; then
2065+ index=$i
2066+ elif [ -n "$bus" -a -z "$unit" ]; then
2067+ unit=$i
2068+ fi
2069+
2070+ busorindex="${bus:+bus=$bus,unit=$unit}${index:+index=${index}}"
2071+ diskopts="file=${file},id=$id,if=none,format=$fmt,$busorindex"
2072+ devopts="$driver,drive=$id${serial:+,serial=${serial}}"
2073+ for tok in "$@"; do
2074+ case "$tok" in
2075+ id=*|if=*|driver=*|$file|file=*) continue;;
2076+ fmt=*|format=*) continue;;
2077+ serial=*|bus=*|unit=*|index=*) continue;;
2078+ esac
2079+ isdevopt "$driver" "$tok" && devopts="${devopts},$tok" ||
2080+ diskopts="${diskopts},${tok}"
2081+ done
2082+
2083+ diskargs=( "${diskargs[@]}" -drive "$diskopts" -device "$devopts" )
2084 done
2085
2086 local mnics_vflag=""
2087@@ -316,7 +525,7 @@ main() {
2088 # now go through and split out options
2089 # -device virtio-net-pci,netdev=virtnet0,mac=52:54:31:15:63:02
2090 # -netdev type=tap,id=virtnet0,vhost=on,script=/etc/kvm/kvm-ifup.br0,downscript=no
2091- local oifs="$IFS" netopts="" devopts="" id="" need_taps=0 model=""
2092+ local netopts="" devopts="" id="" need_taps=0 model=""
2093 local device_args netdev_args
2094 device_args=( )
2095 netdev_args=( )
2096@@ -403,6 +612,15 @@ main() {
2097
2098 if [ $need_taps -ne 0 ]; then
2099 local missing="" missing_pkgs="" reqs="" req="" pkgs="" pkg=""
2100+ for i in "${connections[@]}"; do
2101+ [ "$i" = "user" -o -e "/sys/class/net/$i" ] ||
2102+ missing="${missing} $i"
2103+ done
2104+ [ -z "$missing" ] || {
2105+ error "cannot create connection on ${missing# }."
2106+ error "bridges do not exist.";
2107+ return 1;
2108+ }
2109 error "creating tap devices: ${connections[*]}"
2110 if $DRY_RUN; then
2111 error "sudo $0 tap-control make-nics" \
2112@@ -444,15 +662,21 @@ main() {
2113 -netdev "${netdev_args[$i]}")
2114 done
2115
2116- cmd=( "${kvmcmd[@]}" "${archopts[@]}" "${netargs[@]}"
2117- "${diskargs[@]}" "${pt[@]}" )
2118- error "${cmd[@]}"
2119+ local bus_devices
2120+ bus_devices=( -device "$virtio_scsi_bus,id=virtio-scsi-xkvm" )
2121+ cmd=( "${kvmcmd[@]}" "${archopts[@]}"
2122+ "${bios_opts[@]}"
2123+ "${bus_devices[@]}"
2124+ "${netargs[@]}"
2125+ "${diskargs[@]}" "${pt[@]}" )
2126+ local pcmd=$(quote_cmd "${cmd[@]}")
2127+ error "$pcmd"
2128 ${DRY_RUN} && return 0
2129
2130 if $dowait; then
2131 "${cmd[@]}" &
2132 KVM_PID=$!
2133- debug 1 "kvm pid=$KVM_PID. my pid=$$"
2134+ debug 1 "kvm pid=$KVM_PID. my pid=$$. ppid=$PPID."
2135 wait
2136 ret=$?
2137 KVM_PID=""

Subscribers

People subscribed via source and target branches