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 host \ 228 -enable-kvm \ 229 -smp 8 \ 230 -m 4G \ 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 "$@" 398