summaryrefslogtreecommitdiffstats
path: root/scripts/run_bisect_qemu.sh
blob: d0f8dcc2538b6d5ec21b019d32a8fdb26ad4dab2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#!/bin/sh
#
# Copyright 2020 Johannes Schauer Marin Rodrigues <josch@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# this script is part of debbisect and usually called by debbisect itself
#
# it accepts eight or ten arguments:
#    1. dependencies
#    2. script name or shell snippet
#    3. mirror URL
#    4. architecture
#    5. suite
#    6. components
#    7. memsize
#    8. disksize
#    9. (optional) second mirror URL
#   10. (optional) package to upgrade
#
# It will create an ephemeral qemu virtual machine using mmdebstrap and
# guestfish using (3.) as mirror, (4.) as architecture, (5.) as suite and
# (6.) as components, install the dependencies given in (1.) and execute the
# script given in (2.).
# Its output is the exit code of the script as well as a file ./pkglist
# containing the output of "dpkg-query -W" inside the chroot.
#
# If not only six but eight arguments are given, then the second mirror URL
# (9.) will be added to the apt sources and the single package (10.) will be
# upgraded to its version from (9.).
#
# shellcheck disable=SC2016

set -exu

if [ $# -ne 8 ] && [ $# -ne 10 ]; then
	echo "usage: $0 depends script mirror1 architecture suite components memsize disksize [mirror2 toupgrade]"
	exit 1
fi

depends=$1
script=$2
mirror1=$3
architecture=$4
suite=$5
components=$6
memsize=$7
disksize=$8

if [ $# -eq 10 ]; then
	mirror2=$9
	toupgrade=${10}
fi

case $architecture in
	alpha)    qemuarch=alpha;;
	amd64)    qemuarch=x86_64;;
	arm)      qemuarch=arm;;
	arm64)    qemuarch=aarch64;;
	armel)    qemuarch=arm;;
	armhf)    qemuarch=arm;;
	hppa)     qemuarch=hppa;;
	i386)     qemuarch=i386;;
	m68k)     qemuarch=m68k;;
	mips)     qemuarch=mips;;
	mips64)   qemuarch=mips64;;
	mips64el) qemuarch=mips64el;;
	mipsel)   qemuarch=mipsel;;
	powerpc)  qemuarch=ppc;;
	ppc64)    qemuarch=ppc64;;
	ppc64el)  qemuarch=ppc64le;;
	riscv64)  qemuarch=riscv64;;
	s390x)    qemuarch=s390x;;
	sh4)      qemuarch=sh4;;
	sparc)    qemuarch=sparc;;
	sparc64)  qemuarch=sparc64;;
	*) echo "no qemu support for $architecture"; exit 1;;
esac
case $architecture in
	i386)     linuxarch=686-pae;;
	amd64)    linuxarch=amd64;;
	arm64)    linuxarch=arm64;;
	armhf)    linuxarch=armmp;;
	ia64)     linuxarch=itanium;;
	m68k)     linuxarch=m68k;;
	armel)    linuxarch=marvell;;
	hppa)     linuxarch=parisc;;
	powerpc)  linuxarch=powerpc;;
	ppc64)    linuxarch=powerpc64;;
	ppc64el)  linuxarch=powerpc64le;;
	riscv64)  linuxarch=riscv64;;
	s390x)    linuxarch=s390x;;
	sparc64)  linuxarch=sparc64;;
	*) echo "no kernel image for $architecture"; exit 1;;
esac

TMPDIR=$(mktemp --tmpdir --directory debbisect_qemu.XXXXXXXXXX)
cleantmp() {
	for f in customize.sh id_rsa id_rsa.pub qemu.log config; do
		rm -f "$TMPDIR/$f"
	done
	rmdir "$TMPDIR"
}

trap cleantmp EXIT
# the temporary directory must be world readable (for example in unshare mode)
chmod a+xr "$TMPDIR"

ssh-keygen -q -t rsa -f "$TMPDIR/id_rsa" -N ""

cat << SCRIPT > "$TMPDIR/customize.sh"
#!/bin/sh
set -exu

rootfs="\$1"

# setup various files in /etc
echo host > "\$rootfs/etc/hostname"
echo "127.0.0.1 localhost host" > "\$rootfs/etc/hosts"
echo "/dev/vda1 / auto errors=remount-ro 0 1" > "\$rootfs/etc/fstab"
cat /etc/resolv.conf > "\$rootfs/etc/resolv.conf"

# setup users
chroot "\$rootfs" passwd --delete root
chroot "\$rootfs" useradd --home-dir /home/user --create-home user
chroot "\$rootfs" passwd --delete user

# extlinux config to boot from /dev/vda1 with predictable network interface
# naming and a serial console for logging
cat << END > "\$rootfs/extlinux.conf"
default linux
timeout 0

label linux
kernel /vmlinuz
append initrd=/initrd.img root=/dev/vda1 net.ifnames=0 console=ttyS0
END

# network interface config
# we can use eth0 because we boot with net.ifnames=0 for predictable interface
# names
cat << END > "\$rootfs/etc/network/interfaces"
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
END

# copy in the public key
mkdir "\$rootfs/root/.ssh"
cp "$TMPDIR/id_rsa.pub" "\$rootfs/root/.ssh/authorized_keys"
chroot "\$rootfs" chown 0:0 /root/.ssh/authorized_keys
SCRIPT
chmod +x "$TMPDIR/customize.sh"

# The following hacks are needed to go back as far as 2006-08-10:
#
#  - Acquire::Check-Valid-Until "false" allows Release files with an expired
#    Valid-Until dates
#  - Apt::Key::gpgvcommand allows expired GPG keys
#  - Apt::Hashes::SHA1::Weak "yes" allows GPG keys with weak SHA1 signature
#  - /usr/share/keyrings lets apt use debian-archive-removed-keys.gpg
#  - /usr/share/mmdebstrap/hooks/jessie-or-older performs some setup that is
#    only required for Debian Jessie or older
#
mmdebstrap --architecture="$architecture" --verbose --variant=apt --components="$components" \
	--aptopt='Acquire::Check-Valid-Until "false"' \
	--aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' \
	--aptopt='Apt::Hashes::SHA1::Weak "yes"' \
	--keyring=/usr/share/keyrings \
	--hook-dir=/usr/share/mmdebstrap/hooks/maybe-jessie-or-older \
	--hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr \
	--skip=check/signed-by \
	--include='openssh-server,systemd-sysv,ifupdown,netbase,isc-dhcp-client,udev,policykit-1,linux-image-'"$linuxarch" \
	--customize-hook="$TMPDIR/customize.sh" \
	"$suite" debian-rootfs.tar "$mirror1"

# use guestfish to prepare the host system
#
#  - create a single 4G partition and unpack the rootfs tarball into it
#  - unpack the tarball of the container into /
#  - put a syslinux MBR into the first 440 bytes of the drive
#  - install extlinux and make partition bootable
#
# useful stuff to debug any errors:
#   LIBGUESTFS_BACKEND_SETTINGS=force_tcg
#   libguestfs-test-tool || true
#   export LIBGUESTFS_DEBUG=1 LIBGUESTFS_TRACE=1
guestfish -N "debian-rootfs.img=disk:$disksize" -- \
	part-disk /dev/sda mbr : \
	mkfs ext4 /dev/sda1 : \
	mount /dev/sda1 / : \
	tar-in "debian-rootfs.tar" / : \
	upload /usr/lib/SYSLINUX/mbr.bin /mbr.bin : \
	copy-file-to-device /mbr.bin /dev/sda size:440 : \
	rm /mbr.bin : \
	extlinux / : \
	sync : \
	umount / : \
	part-set-bootable /dev/sda 1 true : \
	shutdown


# start the host system
# prefer using kvm but fall back to tcg if not available
# avoid entropy starvation by feeding the crypt system with random bits from /dev/urandom
# the default memory size of 128 MiB is not enough for Debian, so we go with 1G
# use a virtio network card instead of emulating a real network device
# we don't need any graphics
# this also multiplexes the console and the monitor to stdio
# creates a multiplexed stdio backend connected to the serial port and the qemu
# monitor
# redirect tcp connections on port 10022 localhost to the host system port 22
# redirect all output to a file
# run in the background
timeout --kill-after=60s 60m \
	qemu-system-"$qemuarch" \
	-M accel=kvm:tcg \
	-no-user-config \
	-object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0 \
	-m "$memsize" \
	-net nic,model=virtio \
	-nographic \
	-serial mon:stdio \
	-net user,hostfwd=tcp:127.0.0.1:10022-:22 \
	-drive file="debian-rootfs.img",format=raw,if=virtio \
	> "$TMPDIR/qemu.log" </dev/null 2>&1 &

# store the pid
QEMUPID=$!

# use a function here, so that we can properly quote the path to qemu.log
showqemulog() {
	cat --show-nonprinting "$TMPDIR/qemu.log"
}

# show the log and kill qemu in case the script exits first
trap 'showqemulog; cleantmp; kill $QEMUPID' EXIT

# the default ssh command does not store known hosts and even ignores host keys
# it identifies itself with the rsa key generated above
# pseudo terminal allocation is disabled or otherwise, programs executed via
# ssh might wait for input on stdin of the ssh process

cat << END > "$TMPDIR/config"
Host qemu
	Hostname 127.0.0.1
	User root
	Port 10022
	UserKnownHostsFile /dev/null
	StrictHostKeyChecking no
	IdentityFile $TMPDIR/id_rsa
	RequestTTY no
END

TIMESTAMP=$(sleepenh 0 || [ $? -eq 1 ])
TIMEOUT=5
NUM_TRIES=40
i=0
while true; do
	rv=0
	ssh -F "$TMPDIR/config" -o ConnectTimeout=$TIMEOUT qemu echo success || rv=1
	[ $rv -eq 0 ] && break
	# if the command before took less than $TIMEOUT seconds, wait the remaining time
	TIMESTAMP=$(sleepenh "$TIMESTAMP" "$TIMEOUT" || [ $? -eq 1 ]);
	i=$((i+1))
	if [ $i -ge $NUM_TRIES ]; then
		break
	fi
done

if [ $i -eq $NUM_TRIES ]; then
	echo "timeout reached: unable to connect to qemu via ssh"
	exit 1
fi

# if any url in sources.list points to 127.0.0.1 then we have to replace them
# by the host IP as seen by the qemu guest
cat << SCRIPT | ssh -F "$TMPDIR/config" qemu sh
set -eu
if [ -e /etc/apt/sources.list ]; then
	sed -i 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/' /etc/apt/sources.list
fi
find /etc/apt/sources.list.d -type f -name '*.list' -print0 \
	| xargs --null --no-run-if-empty sed -i 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/'
SCRIPT

# we install dependencies now and not with mmdebstrap --include in case some
# dependencies require a full system present
if [ -n "$depends" ]; then
	ssh -F "$TMPDIR/config" qemu apt-get update
	# shellcheck disable=SC2046,SC2086
	ssh -F "$TMPDIR/config" qemu env DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get --yes install --no-install-recommends $(echo $depends | tr ',' ' ')
fi

# in its ten-argument form, a single package has to be upgraded to its
# version from the first bad timestamp
if [ $# -eq 10 ]; then
	# replace content of sources.list with first bad timestamp
	mirror2=$(echo "$mirror2" | sed 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/')
	echo "deb $mirror2 $suite $(echo "$components" | tr ',' ' ')" | ssh -F "$TMPDIR/config" qemu "cat > /etc/apt/sources.list"
	ssh -F "$TMPDIR/config" qemu apt-get update
	# upgrade a single package (and whatever else apt deems necessary)
	before=$(ssh -F "$TMPDIR/config" qemu dpkg-query -W)
	ssh -F "$TMPDIR/config" qemu env DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get --yes install --no-install-recommends "$toupgrade"
	after=$(ssh -F "$TMPDIR/config" qemu dpkg-query -W)
	# make sure that something was upgraded
	if [ "$before" = "$after" ]; then
		echo "nothing got upgraded -- this should never happen" >&2
		exit 1
	fi
	ssh -F "$TMPDIR/config" qemu dpkg-query -W > "./debbisect.$DEBIAN_BISECT_TIMESTAMP.$toupgrade.pkglist"
else
	ssh -F "$TMPDIR/config" qemu dpkg-query -W > "./debbisect.$DEBIAN_BISECT_TIMESTAMP.pkglist"
fi

ssh -F "$TMPDIR/config" qemu dpkg-query --list -no-pager

# explicitly export all necessary variables
# because we use set -u this also makes sure that this script has these
# variables set in the first place
export DEBIAN_BISECT_EPOCH="$DEBIAN_BISECT_EPOCH"
export DEBIAN_BISECT_TIMESTAMP="$DEBIAN_BISECT_TIMESTAMP"
if [ -z ${DEBIAN_BISECT_MIRROR+x} ]; then
	# DEBIAN_BISECT_MIRROR was unset (caching is disabled)
	true
else
	# replace the localhost IP by the IP of the host as seen by qemu
	DEBIAN_BISECT_MIRROR=$(echo "$DEBIAN_BISECT_MIRROR" | sed 's/http:\/\/127.0.0.1:/http:\/\/10.0.2.2:/')
	export DEBIAN_BISECT_MIRROR="$DEBIAN_BISECT_MIRROR"
fi


# either execute $script as a script from $PATH or as a shell snippet
ret=0
if [ -x "$script" ] || echo "$script" | grep --invert-match --silent --perl-regexp '[^\w@\%+=:,.\/-]'; then
	"$script" "$TMPDIR/config" || ret=$?
else
	sh -c "$script" exec "$TMPDIR/config" || ret=$?
fi

# since we installed systemd-sysv, systemctl is available
ssh -F "$TMPDIR/config" qemu systemctl poweroff

wait $QEMUPID

trap - EXIT

showqemulog
cleantmp

if [ "$ret" -eq 0 ]; then
	exit 0
else
	exit 1
fi