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