1#!/bin/bash
2# SPDX-License-Identifier: GPL-2.0
3
4set -u
5set -e
6
7# This script currently only works for x86_64, as
8# it is based on the VM image used by the BPF CI which is
9# x86_64.
10QEMU_BINARY="${QEMU_BINARY:="qemu-system-x86_64"}"
11X86_BZIMAGE="arch/x86/boot/bzImage"
12DEFAULT_COMMAND="./test_progs"
13MOUNT_DIR="mnt"
14ROOTFS_IMAGE="root.img"
15OUTPUT_DIR="$HOME/.bpf_selftests"
16KCONFIG_URL="https://raw.githubusercontent.com/libbpf/libbpf/master/travis-ci/vmtest/configs/latest.config"
17KCONFIG_API_URL="https://api.github.com/repos/libbpf/libbpf/contents/travis-ci/vmtest/configs/latest.config"
18INDEX_URL="https://raw.githubusercontent.com/libbpf/libbpf/master/travis-ci/vmtest/configs/INDEX"
19NUM_COMPILE_JOBS="$(nproc)"
20LOG_FILE_BASE="$(date +"bpf_selftests.%Y-%m-%d_%H-%M-%S")"
21LOG_FILE="${LOG_FILE_BASE}.log"
22EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status"
23
24usage()
25{
26	cat <<EOF
27Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>]
28
29<command> is the command you would normally run when you are in
30tools/testing/selftests/bpf. e.g:
31
32	$0 -- ./test_progs -t test_lsm
33
34If no command is specified and a debug shell (-s) is not requested,
35"${DEFAULT_COMMAND}" will be run by default.
36
37If you build your kernel using KBUILD_OUTPUT= or O= options, these
38can be passed as environment variables to the script:
39
40  O=<kernel_build_path> $0 -- ./test_progs -t test_lsm
41
42or
43
44  KBUILD_OUTPUT=<kernel_build_path> $0 -- ./test_progs -t test_lsm
45
46Options:
47
48	-i)		Update the rootfs image with a newer version.
49	-d)		Update the output directory (default: ${OUTPUT_DIR})
50	-j)		Number of jobs for compilation, similar to -j in make
51			(default: ${NUM_COMPILE_JOBS})
52	-s)		Instead of powering off the VM, start an interactive
53			shell. If <command> is specified, the shell runs after
54			the command finishes executing
55EOF
56}
57
58unset URLS
59populate_url_map()
60{
61	if ! declare -p URLS &> /dev/null; then
62		# URLS contain the mapping from file names to URLs where
63		# those files can be downloaded from.
64		declare -gA URLS
65		while IFS=$'\t' read -r name url; do
66			URLS["$name"]="$url"
67		done < <(curl -Lsf ${INDEX_URL})
68	fi
69}
70
71download()
72{
73	local file="$1"
74
75	if [[ ! -v URLS[$file] ]]; then
76		echo "$file not found" >&2
77		return 1
78	fi
79
80	echo "Downloading $file..." >&2
81	curl -Lsf "${URLS[$file]}" "${@:2}"
82}
83
84newest_rootfs_version()
85{
86	{
87	for file in "${!URLS[@]}"; do
88		if [[ $file =~ ^libbpf-vmtest-rootfs-(.*)\.tar\.zst$ ]]; then
89			echo "${BASH_REMATCH[1]}"
90		fi
91	done
92	} | sort -rV | head -1
93}
94
95download_rootfs()
96{
97	local rootfsversion="$1"
98	local dir="$2"
99
100	if ! which zstd &> /dev/null; then
101		echo 'Could not find "zstd" on the system, please install zstd'
102		exit 1
103	fi
104
105	download "libbpf-vmtest-rootfs-$rootfsversion.tar.zst" |
106		zstd -d | sudo tar -C "$dir" -x
107}
108
109recompile_kernel()
110{
111	local kernel_checkout="$1"
112	local make_command="$2"
113
114	cd "${kernel_checkout}"
115
116	${make_command} olddefconfig
117	${make_command}
118}
119
120mount_image()
121{
122	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
123	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
124
125	sudo mount -o loop "${rootfs_img}" "${mount_dir}"
126}
127
128unmount_image()
129{
130	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
131
132	sudo umount "${mount_dir}" &> /dev/null
133}
134
135update_selftests()
136{
137	local kernel_checkout="$1"
138	local selftests_dir="${kernel_checkout}/tools/testing/selftests/bpf"
139
140	cd "${selftests_dir}"
141	${make_command}
142
143	# Mount the image and copy the selftests to the image.
144	mount_image
145	sudo rm -rf "${mount_dir}/root/bpf"
146	sudo cp -r "${selftests_dir}" "${mount_dir}/root"
147	unmount_image
148}
149
150update_init_script()
151{
152	local init_script_dir="${OUTPUT_DIR}/${MOUNT_DIR}/etc/rcS.d"
153	local init_script="${init_script_dir}/S50-startup"
154	local command="$1"
155	local exit_command="$2"
156
157	mount_image
158
159	if [[ ! -d "${init_script_dir}" ]]; then
160		cat <<EOF
161Could not find ${init_script_dir} in the mounted image.
162This likely indicates a bad rootfs image, Please download
163a new image by passing "-i" to the script
164EOF
165		exit 1
166
167	fi
168
169	sudo bash -c "echo '#!/bin/bash' > ${init_script}"
170
171	if [[ "${command}" != "" ]]; then
172		sudo bash -c "cat >>${init_script}" <<EOF
173# Have a default value in the exit status file
174# incase the VM is forcefully stopped.
175echo "130" > "/root/${EXIT_STATUS_FILE}"
176
177{
178	cd /root/bpf
179	echo ${command}
180	stdbuf -oL -eL ${command}
181	echo "\$?" > "/root/${EXIT_STATUS_FILE}"
182} 2>&1 | tee "/root/${LOG_FILE}"
183# Ensure that the logs are written to disk
184sync
185EOF
186	fi
187
188	sudo bash -c "echo ${exit_command} >> ${init_script}"
189	sudo chmod a+x "${init_script}"
190	unmount_image
191}
192
193create_vm_image()
194{
195	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
196	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
197
198	rm -rf "${rootfs_img}"
199	touch "${rootfs_img}"
200	chattr +C "${rootfs_img}" >/dev/null 2>&1 || true
201
202	truncate -s 2G "${rootfs_img}"
203	mkfs.ext4 -q "${rootfs_img}"
204
205	mount_image
206	download_rootfs "$(newest_rootfs_version)" "${mount_dir}"
207	unmount_image
208}
209
210run_vm()
211{
212	local kernel_bzimage="$1"
213	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
214
215	if ! which "${QEMU_BINARY}" &> /dev/null; then
216		cat <<EOF
217Could not find ${QEMU_BINARY}
218Please install qemu or set the QEMU_BINARY environment variable.
219EOF
220		exit 1
221	fi
222
223	${QEMU_BINARY} \
224		-nodefaults \
225		-display none \
226		-serial mon:stdio \
227		-cpu kvm64 \
228		-enable-kvm \
229		-smp 4 \
230		-m 2G \
231		-drive file="${rootfs_img}",format=raw,index=1,media=disk,if=virtio,cache=none \
232		-kernel "${kernel_bzimage}" \
233		-append "root=/dev/vda rw console=ttyS0,115200"
234}
235
236copy_logs()
237{
238	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
239	local log_file="${mount_dir}/root/${LOG_FILE}"
240	local exit_status_file="${mount_dir}/root/${EXIT_STATUS_FILE}"
241
242	mount_image
243	sudo cp ${log_file} "${OUTPUT_DIR}"
244	sudo cp ${exit_status_file} "${OUTPUT_DIR}"
245	sudo rm -f ${log_file}
246	unmount_image
247}
248
249is_rel_path()
250{
251	local path="$1"
252
253	[[ ${path:0:1} != "/" ]]
254}
255
256update_kconfig()
257{
258	local kconfig_file="$1"
259	local update_command="curl -sLf ${KCONFIG_URL} -o ${kconfig_file}"
260	# Github does not return the "last-modified" header when retrieving the
261	# raw contents of the file. Use the API call to get the last-modified
262	# time of the kernel config and only update the config if it has been
263	# updated after the previously cached config was created. This avoids
264	# unnecessarily compiling the kernel and selftests.
265	if [[ -f "${kconfig_file}" ]]; then
266		local last_modified_date="$(curl -sL -D - "${KCONFIG_API_URL}" -o /dev/null | \
267			grep "last-modified" | awk -F ': ' '{print $2}')"
268		local remote_modified_timestamp="$(date -d "${last_modified_date}" +"%s")"
269		local local_creation_timestamp="$(stat -c %Y "${kconfig_file}")"
270
271		if [[ "${remote_modified_timestamp}" -gt "${local_creation_timestamp}" ]]; then
272			${update_command}
273		fi
274	else
275		${update_command}
276	fi
277}
278
279main()
280{
281	local script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
282	local kernel_checkout=$(realpath "${script_dir}"/../../../../)
283	# By default the script searches for the kernel in the checkout directory but
284	# it also obeys environment variables O= and KBUILD_OUTPUT=
285	local kernel_bzimage="${kernel_checkout}/${X86_BZIMAGE}"
286	local command="${DEFAULT_COMMAND}"
287	local update_image="no"
288	local exit_command="poweroff -f"
289	local debug_shell="no"
290
291	while getopts 'hskid:j:' opt; do
292		case ${opt} in
293		i)
294			update_image="yes"
295			;;
296		d)
297			OUTPUT_DIR="$OPTARG"
298			;;
299		j)
300			NUM_COMPILE_JOBS="$OPTARG"
301			;;
302		s)
303			command=""
304			debug_shell="yes"
305			exit_command="bash"
306			;;
307		h)
308			usage
309			exit 0
310			;;
311		\? )
312			echo "Invalid Option: -$OPTARG"
313			usage
314			exit 1
315			;;
316		: )
317			echo "Invalid Option: -$OPTARG requires an argument"
318			usage
319			exit 1
320			;;
321		esac
322	done
323	shift $((OPTIND -1))
324
325	if [[ $# -eq 0  && "${debug_shell}" == "no" ]]; then
326		echo "No command specified, will run ${DEFAULT_COMMAND} in the vm"
327	else
328		command="$@"
329	fi
330
331	local kconfig_file="${OUTPUT_DIR}/latest.config"
332	local make_command="make -j ${NUM_COMPILE_JOBS} KCONFIG_CONFIG=${kconfig_file}"
333
334	# Figure out where the kernel is being built.
335	# O takes precedence over KBUILD_OUTPUT.
336	if [[ "${O:=""}" != "" ]]; then
337		if is_rel_path "${O}"; then
338			O="$(realpath "${PWD}/${O}")"
339		fi
340		kernel_bzimage="${O}/${X86_BZIMAGE}"
341		make_command="${make_command} O=${O}"
342	elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then
343		if is_rel_path "${KBUILD_OUTPUT}"; then
344			KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")"
345		fi
346		kernel_bzimage="${KBUILD_OUTPUT}/${X86_BZIMAGE}"
347		make_command="${make_command} KBUILD_OUTPUT=${KBUILD_OUTPUT}"
348	fi
349
350	populate_url_map
351
352	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
353	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
354
355	echo "Output directory: ${OUTPUT_DIR}"
356
357	mkdir -p "${OUTPUT_DIR}"
358	mkdir -p "${mount_dir}"
359	update_kconfig "${kconfig_file}"
360
361	recompile_kernel "${kernel_checkout}" "${make_command}"
362
363	if [[ "${update_image}" == "no" && ! -f "${rootfs_img}" ]]; then
364		echo "rootfs image not found in ${rootfs_img}"
365		update_image="yes"
366	fi
367
368	if [[ "${update_image}" == "yes" ]]; then
369		create_vm_image
370	fi
371
372	update_selftests "${kernel_checkout}" "${make_command}"
373	update_init_script "${command}" "${exit_command}"
374	run_vm "${kernel_bzimage}"
375	if [[ "${command}" != "" ]]; then
376		copy_logs
377		echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
378	fi
379}
380
381catch()
382{
383	local exit_code=$1
384	local exit_status_file="${OUTPUT_DIR}/${EXIT_STATUS_FILE}"
385	# This is just a cleanup and the directory may
386	# have already been unmounted. So, don't let this
387	# clobber the error code we intend to return.
388	unmount_image || true
389	if [[ -f "${exit_status_file}" ]]; then
390		exit_code="$(cat ${exit_status_file})"
391	fi
392	exit ${exit_code}
393}
394
395trap 'catch "$?"' EXIT
396
397main "$@"
398