WIP: add QEMU tests

Change-Id: Ibf75514b866fffb11e90529e4705f126b23d7415
diff --git a/.gitignore b/.gitignore
index bf5732d..56eefa6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@
 
 tools/gtp-link
 tools/gtp-tunnel
+
+tests/qemu/_*
diff --git a/Makefile.am b/Makefile.am
index c34b6db..c6b9682 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4,7 +4,12 @@
 
 ACLOCAL_AMFLAGS = -I m4
 
-SUBDIRS = src include tools
+SUBDIRS = \
+	  include \
+	  src \
+	  tests \
+	  tools \
+	  $(NULL)
 
 pkgconfigdir = $(libdir)/pkgconfig
 pkgconfig_DATA = libgtpnl.pc
diff --git a/configure.ac b/configure.ac
index 2665b70..b846c8c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -72,6 +72,19 @@
 	CPPFLAGS="$CPPFLAGS $WERROR_FLAGS"
 fi
 
+AC_ARG_ENABLE(qemu_tests,
+	[AS_HELP_STRING(
+		[--enable-qemu-tests],
+		[Run automated tests in QEMU]
+	)],
+	[qemu_tests=$enableval], [qemu_tests="no"])
+AC_MSG_CHECKING([whether to enable QEMU tests])
+AC_MSG_RESULT([$qemu_tests])
+AM_CONDITIONAL(ENABLE_QEMU_TESTS, test x"$qemu_tests" = x"yes")
+if test x"$qemu_tests" = x"yes" && ! $srcdir/tests/qemu/check-depends.sh; then
+	AC_MSG_ERROR([missing programs for --enable-qemu-tests])
+fi
+
 AC_SUBST([CPPFLAGS])
 AC_SUBST([CFLAGS])
 AC_CONFIG_FILES([
@@ -82,6 +95,7 @@
 	include/linux/Makefile
 	libgtpnl.pc
 	src/Makefile
+	tests/Makefile
 	tools/Makefile
 ])
 AC_OUTPUT()
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..54af52d
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,15 @@
+check-local:
+	$(MAKE) qemu-tests
+
+if ENABLE_QEMU_TESTS
+qemu-download-kernel:
+	rm -f qemu/_linux
+	wget -O qemu/_linux \
+		https://jenkins.osmocom.org/jenkins/job/ttcn3-ggsn-test-kernel-latest-net-next/ws/_cache/kernel-test/linux
+qemu-tests:
+	qemu/initrd-build.sh
+	qemu/run-qemu.sh
+else
+qemu-tests:
+	@echo "Not running QEMU tests (determined at configure-time)"
+endif
diff --git a/tests/qemu/00_test_functions.sh b/tests/qemu/00_test_functions.sh
new file mode 100644
index 0000000..6215ead
--- /dev/null
+++ b/tests/qemu/00_test_functions.sh
@@ -0,0 +1,63 @@
+#!/bin/sh -ex
+
+# Use ip from iproute2 instead of busybox ip, because iproute2's version has
+# "ip netns" implemented. Calling /bin/ip explicitly is needed here, otherwise
+# busybox sh will use busybox ip, regardless of PATH.
+alias ip="/bin/ip"
+alias ggsn_side="ip netns exec ggsn_side"
+
+# MS - SGSN -gtp- GGSN - WEBSERVER
+tunnel_start() {
+	ip netns add ggsn_side
+
+	# SGSN side: prepare veth_sgsn (SGSN), lo (MS)
+	ip link add veth_sgsn type veth peer name veth_ggsn
+	ip addr add "$SGSN"/"$SGSN_PREFLEN" dev veth_sgsn
+	ip link set veth_sgsn up
+	ip addr add "$MS"/"$MS_PREFLEN" dev lo
+	ip link set lo up
+
+	# SGSN side: prepare gtp-tunnel
+	gtp-link add gtp_sgsn --sgsn &
+	sleep 1
+	gtp-tunnel add gtp_sgsn v1 200 100 "$MS" "$GGSN"
+	ip route add "$WEBSERVER"/"$MS_PREFLEN" dev gtp_sgsn
+
+	# GGSN side: prepare veth_ggsn (GGSN), lo (WEBSERVER)
+	ip link set veth_ggsn netns ggsn_side
+	ggsn_side ip addr add "$GGSN"/"$SGSN_PREFLEN" dev veth_ggsn
+	ggsn_side ip link set veth_ggsn up
+	ggsn_side ip addr add "$WEBSERVER"/"$MS_PREFLEN" dev lo
+	ggsn_side ip link set lo up
+
+	# GGSN side: prepare gtp-tunnel
+	ggsn_side gtp-link add gtp_ggsn &
+	sleep 1
+	ggsn_side gtp-tunnel add gtp_ggsn v1 100 200 "$MS" "$SGSN"
+	ggsn_side ip route add "$MS"/"$MS_PREFLEN" dev gtp_ggsn
+
+	# List tunnel from both sides
+	gtp-tunnel list
+	ggsn_side gtp-tunnel list
+}
+
+tunnel_ping() {
+	ping -c 1 "$WEBSERVER"
+	ggsn_side ping -c 1 "$MS"
+}
+
+tunnel_stop() {
+	killall gtp-link
+
+	ip addr del "$MS"/"$MS_PREFLEN" dev lo
+	ip link set veth_sgsn down
+	ip addr del "$SGSN"/"$SGSN_PREFLEN" dev veth_sgsn
+	ip link del veth_sgsn
+	ip route del "$WEBSERVER"/"$MS_PREFLEN" dev gtp_sgsn
+	gtp-tunnel delete gtp_sgsn v1 200
+	gtp-link del gtp_sgsn
+
+	ggsn_side gtp-tunnel delete gtp_ggsn v1 100
+	ggsn_side gtp-link del gtp_ggsn
+	ip netns del ggsn_side
+}
diff --git a/tests/qemu/01_ms_ip4_sgsn_ip4.sh b/tests/qemu/01_ms_ip4_sgsn_ip4.sh
new file mode 100644
index 0000000..7aaf17b
--- /dev/null
+++ b/tests/qemu/01_ms_ip4_sgsn_ip4.sh
@@ -0,0 +1,13 @@
+#!/bin/sh -ex
+. /tests/00_test_functions.sh
+
+MS="172.99.0.1"
+MS_PREFLEN="32"
+SGSN="172.0.0.1"
+SGSN_PREFLEN="24"
+GGSN="172.0.0.2"
+WEBSERVER="172.99.0.2"
+
+tunnel_start
+tunnel_ping
+tunnel_stop
diff --git a/tests/qemu/02_ms_ip4_sgsn_ip6.sh b/tests/qemu/02_ms_ip4_sgsn_ip6.sh
new file mode 100644
index 0000000..67b605a
--- /dev/null
+++ b/tests/qemu/02_ms_ip4_sgsn_ip6.sh
@@ -0,0 +1,13 @@
+#!/bin/sh -ex
+. /tests/00_test_functions.sh
+
+MS="172.99.0.1"
+MS_PREFLEN="32"
+SGSN="fd00::1"
+SGSN_PREFLEN="7"
+GGSN="fd00::2"
+WEBSERVER="172.99.0.2"
+
+tunnel_start
+tunnel_ping
+tunnel_stop
diff --git a/tests/qemu/03_ms_ip6_sgsn_ip4.sh b/tests/qemu/03_ms_ip6_sgsn_ip4.sh
new file mode 100644
index 0000000..23e5980
--- /dev/null
+++ b/tests/qemu/03_ms_ip6_sgsn_ip4.sh
@@ -0,0 +1,13 @@
+#!/bin/sh -ex
+. /tests/00_test_functions.sh
+
+MS="fd00::1"
+MS_PREFLEN="7"
+SGSN="172.0.0.1"
+SGSN_PREFLEN="24"
+GGSN="172.0.0.2"
+WEBSERVER="fd00::2"
+
+tunnel_start
+tunnel_ping
+tunnel_stop
diff --git a/tests/qemu/04_ms_ip6_sgsn_ip6.sh b/tests/qemu/04_ms_ip6_sgsn_ip6.sh
new file mode 100644
index 0000000..1fdd05f
--- /dev/null
+++ b/tests/qemu/04_ms_ip6_sgsn_ip6.sh
@@ -0,0 +1,13 @@
+#!/bin/sh -ex
+. /tests/00_test_functions.sh
+
+MS="fc00::1"
+MS_PREFLEN="7"
+SGSN="fd00::1"
+SGSN_PREFLEN="7"
+GGSN="fd00::2"
+WEBSERVER="fc00::2"
+
+tunnel_start
+tunnel_ping
+tunnel_stop
diff --git a/tests/qemu/check-depends.sh b/tests/qemu/check-depends.sh
new file mode 100755
index 0000000..15a7600
--- /dev/null
+++ b/tests/qemu/check-depends.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+RET=0
+
+require_program() {
+	if [ -z "$(command -v "$1")" ]; then
+		RET=1
+		echo "ERROR: missing program: $1"
+	fi
+}
+
+require_program busybox
+require_program cpio
+require_program find
+require_program gzip
+require_program ip
+require_program qemu-system-x86_64
+
+exit "$RET"
diff --git a/tests/qemu/initrd-build.sh b/tests/qemu/initrd-build.sh
new file mode 100755
index 0000000..34d3bc5
--- /dev/null
+++ b/tests/qemu/initrd-build.sh
@@ -0,0 +1,110 @@
+#!/bin/sh -e
+DIR="$(cd "$(dirname "$0")" && pwd)"
+DIR_INITRD="$DIR/_initrd"
+SRC_LIBS="$(realpath "$DIR/../../src/.libs/")"
+TOOLS_LIBS="$(realpath "$DIR/../../tools/.libs/")"
+
+# Add one or more files to the initramfs, with parent directories.
+# usr-merge: resolve symlinks for /lib -> /usr/lib etc. so "cp --parents" does
+# not fail with "cp: cannot make directory '/tmp/initrd/lib': File exists"
+# $@: path to files
+initrd_add_file() {
+	local i
+
+	for i in "$@"; do
+		case "$i" in
+		/bin/*|/sbin/*|/lib/*|/lib64/*)
+			cp -a --parents "$i" "$DIR_INITRD"/usr
+			;;
+		*)
+			cp -a --parents "$i" "$DIR_INITRD"
+			;;
+		esac
+	done
+}
+
+# Add binaries with depending libraries
+# $@: paths to binaries
+initrd_add_bin() {
+	local bin
+	local bin_path
+	local file
+
+	for bin in "$@"; do
+		local bin_path="$(which "$bin")"
+		if [ -z "$bin_path" ]; then
+			echo "ERROR: file not found: $bin"
+			exit 1
+		fi
+
+		lddtree_out="$(lddtree -l "$bin_path")"
+		if [ -z "$lddtree_out" ]; then
+			echo "ERROR: lddtree failed on '$bin_path'"
+			exit 1
+		fi
+
+		for file in $lddtree_out; do
+			initrd_add_file "$file"
+
+			# Copy resolved symlink
+			if [ -L "$file" ]; then
+				initrd_add_file "$(realpath "$file")"
+			fi
+		done
+	done
+}
+
+# Add command to run inside the initramfs
+# $@: commands
+initrd_add_cmd() {
+	local i
+
+	if ! [ -e "$DIR_INITRD"/cmd.sh ]; then
+		echo "#!/bin/sh -ex" > "$DIR_INITRD"/cmd.sh
+		chmod +x "$DIR_INITRD"/cmd.sh
+	fi
+
+	for i in "$@"; do
+		echo "$i" >> "$DIR_INITRD"/cmd.sh
+	done
+}
+
+rm -rf "$DIR_INITRD"
+mkdir -p "$DIR_INITRD"
+cd "$DIR_INITRD"
+
+for dir in bin sbin lib lib64; do
+	ln -s usr/"$dir" "$dir"
+done
+
+mkdir -p \
+	dev/net \
+	proc \
+	run \
+	sys \
+	tmp \
+	usr/bin \
+	usr/sbin
+
+initrd_add_bin \
+	busybox \
+	ip
+
+initrd_add_cmd \
+	"export LD_LIBRARY_PATH=$SRC_LIBS:$LD_LIBRARY_PATH"
+
+export LD_LIBRARY_PATH="$SRC_LIBS:$LD_LIBRARY_PATH"
+
+for i in gtp-link gtp-tunnel; do
+	initrd_add_bin "$TOOLS_LIBS"/"$i"
+	ln -s "$TOOLS_LIBS"/"$i" usr/bin/"$i"
+done
+
+mkdir tests
+cp "$DIR"/*.sh tests
+
+cp "$DIR"/initrd-init.sh init
+
+find . -print0 \
+	| cpio --quiet -o -0 -H newc \
+	| gzip -1 > "$DIR"/_initrd.gz
diff --git a/tests/qemu/initrd-init.sh b/tests/qemu/initrd-init.sh
new file mode 100755
index 0000000..2b5f824
--- /dev/null
+++ b/tests/qemu/initrd-init.sh
@@ -0,0 +1,37 @@
+#!/bin/busybox sh
+echo "Running initrd-init.sh"
+set -x
+
+run_test() {
+	echo
+	echo "QEMU test: $1"
+	echo
+	if ! sh -ex "/tests/$1"; then
+		poweroff -f
+	fi
+}
+
+export HOME=/root
+export LD_LIBRARY_PATH=/usr/local/lib
+export PATH=/usr/local/bin:/usr/bin:/bin:/sbin:/usr/local/sbin:/usr/sbin
+export TERM=screen
+
+/bin/busybox --install -s
+hostname qemu
+mount -t proc proc /proc
+mount -t sysfs sys /sys
+mknod /dev/null c 1 3
+. /cmd.sh
+set +x
+
+# Run all tests
+
+run_test 01_ms_ip4_sgsn_ip4.sh  # OK
+# run_test 02_ms_ip4_sgsn_ip6.sh  # NOK: kernel panic
+# run_test 03_ms_ip6_sgsn_ip4.sh  # NOK: ping doesn't work
+# run_test 04_ms_ip6_sgsn_ip6.sh  # NOK: ping doesn't work
+# run_test 05_ms_ip46_sgsn_ip4.sh  # WIP: not implemented yet
+
+# Success (run-qemu.sh checks for this line)
+echo "QEMU_TEST_SUCCESSFUL"
+poweroff -f
diff --git a/tests/qemu/run-qemu.sh b/tests/qemu/run-qemu.sh
new file mode 100755
index 0000000..63f5087
--- /dev/null
+++ b/tests/qemu/run-qemu.sh
@@ -0,0 +1,52 @@
+#!/bin/sh -e
+DIR="$(cd "$(dirname "$0")" && pwd)"
+
+if [ -e /dev/kvm ]; then
+	MACHINE_ARG="-machine pc,accel=kvm"
+else
+	echo "WARNING: /dev/kvm not found, emulation will be slower"
+	MACHINE_ARG="-machine pc"
+fi
+
+if ! [ -e "$DIR"/_linux ]; then
+	echo "ERROR: linux kernel not found: $DIR/_linux"
+	echo "Put a kernel there, either download it from the Osmocom jenkins:"
+	echo "$ make -C tests qemu-download-kernel"
+	echo "(FIXME: isn't built with required config options yet)"
+	echo
+	echo "Or build your own kernel. Make sure to set:"
+	echo "  CONFIG_GTP=y"
+	echo "  CONFIG_NETNS=y"
+	echo "  CONFIG_VETH=y"
+	exit 1
+fi
+
+KERNEL_CMDLINE="root=/dev/ram0 console=ttyS0 panic=-1 init=/init"
+
+set -x
+qemu-system-x86_64 \
+	$MACHINE_ARG \
+	-smp 1 \
+	-m 512M \
+	-no-user-config -nodefaults -display none \
+	-gdb unix:"$DIR"/_gdb.pipe,server=on,wait=off \
+	-no-reboot \
+	-kernel "$DIR"/_linux \
+	-initrd "$DIR"/_initrd.gz \
+	-append "${KERNEL_CMDLINE}" \
+	-serial stdio \
+	-chardev socket,id=charserial1,path="$DIR"/_gdb-serial.pipe,server=on,wait=off \
+	-device isa-serial,chardev=charserial1,id=serial1 \
+	2>&1 | tee "$DIR/_output"
+
+set +x
+if grep -q "QEMU_TEST_SUCCESSFUL" "$DIR/_output"; then
+	echo
+	echo "QEMU tests: successful"
+	echo
+else
+	echo
+	echo "QEMU tests: failed"
+	echo
+	exit 1
+fi