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