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