vmtest.sh 9.7 KB

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