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