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