vmtest.sh 11 KB

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