Loading...
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 "$@"