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