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