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