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 310main() 311{ 312 local script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" 313 local kernel_checkout=$(realpath "${script_dir}"/../../../../) 314 # By default the script searches for the kernel in the checkout directory but 315 # it also obeys environment variables O= and KBUILD_OUTPUT= 316 local kernel_bzimage="${kernel_checkout}/${BZIMAGE}" 317 local command="${DEFAULT_COMMAND}" 318 local update_image="no" 319 local exit_command="poweroff -f" 320 local debug_shell="no" 321 322 while getopts 'hskid:j:' opt; do 323 case ${opt} in 324 i) 325 update_image="yes" 326 ;; 327 d) 328 OUTPUT_DIR="$OPTARG" 329 ;; 330 j) 331 NUM_COMPILE_JOBS="$OPTARG" 332 ;; 333 s) 334 command="" 335 debug_shell="yes" 336 exit_command="bash" 337 ;; 338 h) 339 usage 340 exit 0 341 ;; 342 \? ) 343 echo "Invalid Option: -$OPTARG" 344 usage 345 exit 1 346 ;; 347 : ) 348 echo "Invalid Option: -$OPTARG requires an argument" 349 usage 350 exit 1 351 ;; 352 esac 353 done 354 shift $((OPTIND -1)) 355 356 if [[ $# -eq 0 && "${debug_shell}" == "no" ]]; then 357 echo "No command specified, will run ${DEFAULT_COMMAND} in the vm" 358 else 359 command="$@" 360 fi 361 362 local kconfig_file="${OUTPUT_DIR}/latest.config" 363 local make_command="make -j ${NUM_COMPILE_JOBS} KCONFIG_CONFIG=${kconfig_file}" 364 365 # Figure out where the kernel is being built. 366 # O takes precedence over KBUILD_OUTPUT. 367 if [[ "${O:=""}" != "" ]]; then 368 if is_rel_path "${O}"; then 369 O="$(realpath "${PWD}/${O}")" 370 fi 371 kernel_bzimage="${O}/${BZIMAGE}" 372 make_command="${make_command} O=${O}" 373 elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then 374 if is_rel_path "${KBUILD_OUTPUT}"; then 375 KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")" 376 fi 377 kernel_bzimage="${KBUILD_OUTPUT}/${BZIMAGE}" 378 make_command="${make_command} KBUILD_OUTPUT=${KBUILD_OUTPUT}" 379 fi 380 381 populate_url_map 382 383 local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}" 384 local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}" 385 386 echo "Output directory: ${OUTPUT_DIR}" 387 388 mkdir -p "${OUTPUT_DIR}" 389 mkdir -p "${mount_dir}" 390 update_kconfig "${kernel_checkout}" "${kconfig_file}" 391 392 recompile_kernel "${kernel_checkout}" "${make_command}" 393 394 if [[ "${update_image}" == "no" && ! -f "${rootfs_img}" ]]; then 395 echo "rootfs image not found in ${rootfs_img}" 396 update_image="yes" 397 fi 398 399 if [[ "${update_image}" == "yes" ]]; then 400 create_vm_image 401 fi 402 403 update_selftests "${kernel_checkout}" "${make_command}" 404 update_init_script "${command}" "${exit_command}" 405 run_vm "${kernel_bzimage}" 406 if [[ "${command}" != "" ]]; then 407 copy_logs 408 echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}" 409 fi 410} 411 412catch() 413{ 414 local exit_code=$1 415 local exit_status_file="${OUTPUT_DIR}/${EXIT_STATUS_FILE}" 416 # This is just a cleanup and the directory may 417 # have already been unmounted. So, don't let this 418 # clobber the error code we intend to return. 419 unmount_image || true 420 if [[ -f "${exit_status_file}" ]]; then 421 exit_code="$(cat ${exit_status_file})" 422 fi 423 exit ${exit_code} 424} 425 426trap 'catch "$?"' EXIT 427 428main "$@" 429