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