blob: 027198768fad313a535a71fdeca213e419185a86 [file] [log] [blame]
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
set -u
set -e
# This script currently only works for x86_64, as
# it is based on the VM image used by the BPF CI which is
# x86_64.
QEMU_BINARY="${QEMU_BINARY:="qemu-system-x86_64"}"
X86_BZIMAGE="arch/x86/boot/bzImage"
DEFAULT_COMMAND="./test_progs"
MOUNT_DIR="mnt"
ROOTFS_IMAGE="root.img"
OUTPUT_DIR="$HOME/.bpf_selftests"
KCONFIG_URL="https://raw.githubusercontent.com/libbpf/libbpf/master/travis-ci/vmtest/configs/latest.config"
KCONFIG_API_URL="https://api.github.com/repos/libbpf/libbpf/contents/travis-ci/vmtest/configs/latest.config"
INDEX_URL="https://raw.githubusercontent.com/libbpf/libbpf/master/travis-ci/vmtest/configs/INDEX"
NUM_COMPILE_JOBS="$(nproc)"
LOG_FILE_BASE="$(date +"bpf_selftests.%Y-%m-%d_%H-%M-%S")"
LOG_FILE="${LOG_FILE_BASE}.log"
EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status"
usage()
{
cat <<EOF
Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>]
<command> is the command you would normally run when you are in
tools/testing/selftests/bpf. e.g:
$0 -- ./test_progs -t test_lsm
If no command is specified and a debug shell (-s) is not requested,
"${DEFAULT_COMMAND}" will be run by default.
If you build your kernel using KBUILD_OUTPUT= or O= options, these
can be passed as environment variables to the script:
O=<kernel_build_path> $0 -- ./test_progs -t test_lsm
or
KBUILD_OUTPUT=<kernel_build_path> $0 -- ./test_progs -t test_lsm
Options:
-i) Update the rootfs image with a newer version.
-d) Update the output directory (default: ${OUTPUT_DIR})
-j) Number of jobs for compilation, similar to -j in make
(default: ${NUM_COMPILE_JOBS})
-s) Instead of powering off the VM, start an interactive
shell. If <command> is specified, the shell runs after
the command finishes executing
EOF
}
unset URLS
populate_url_map()
{
if ! declare -p URLS &> /dev/null; then
# URLS contain the mapping from file names to URLs where
# those files can be downloaded from.
declare -gA URLS
while IFS=$'\t' read -r name url; do
URLS["$name"]="$url"
done < <(curl -Lsf ${INDEX_URL})
fi
}
download()
{
local file="$1"
if [[ ! -v URLS[$file] ]]; then
echo "$file not found" >&2
return 1
fi
echo "Downloading $file..." >&2
curl -Lsf "${URLS[$file]}" "${@:2}"
}
newest_rootfs_version()
{
{
for file in "${!URLS[@]}"; do
if [[ $file =~ ^libbpf-vmtest-rootfs-(.*)\.tar\.zst$ ]]; then
echo "${BASH_REMATCH[1]}"
fi
done
} | sort -rV | head -1
}
download_rootfs()
{
local rootfsversion="$1"
local dir="$2"
if ! which zstd &> /dev/null; then
echo 'Could not find "zstd" on the system, please install zstd'
exit 1
fi
download "libbpf-vmtest-rootfs-$rootfsversion.tar.zst" |
zstd -d | sudo tar -C "$dir" -x
}
recompile_kernel()
{
local kernel_checkout="$1"
local make_command="$2"
cd "${kernel_checkout}"
${make_command} olddefconfig
${make_command}
}
mount_image()
{
local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
sudo mount -o loop "${rootfs_img}" "${mount_dir}"
}
unmount_image()
{
local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
sudo umount "${mount_dir}" &> /dev/null
}
update_selftests()
{
local kernel_checkout="$1"
local selftests_dir="${kernel_checkout}/tools/testing/selftests/bpf"
cd "${selftests_dir}"
${make_command}
# Mount the image and copy the selftests to the image.
mount_image
sudo rm -rf "${mount_dir}/root/bpf"
sudo cp -r "${selftests_dir}" "${mount_dir}/root"
unmount_image
}
update_init_script()
{
local init_script_dir="${OUTPUT_DIR}/${MOUNT_DIR}/etc/rcS.d"
local init_script="${init_script_dir}/S50-startup"
local command="$1"
local exit_command="$2"
mount_image
if [[ ! -d "${init_script_dir}" ]]; then
cat <<EOF
Could not find ${init_script_dir} in the mounted image.
This likely indicates a bad rootfs image, Please download
a new image by passing "-i" to the script
EOF
exit 1
fi
sudo bash -c "echo '#!/bin/bash' > ${init_script}"
if [[ "${command}" != "" ]]; then
sudo bash -c "cat >>${init_script}" <<EOF
# Have a default value in the exit status file
# incase the VM is forcefully stopped.
echo "130" > "/root/${EXIT_STATUS_FILE}"
{
cd /root/bpf
echo ${command}
stdbuf -oL -eL ${command}
echo "\$?" > "/root/${EXIT_STATUS_FILE}"
} 2>&1 | tee "/root/${LOG_FILE}"
# Ensure that the logs are written to disk
sync
EOF
fi
sudo bash -c "echo ${exit_command} >> ${init_script}"
sudo chmod a+x "${init_script}"
unmount_image
}
create_vm_image()
{
local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
rm -rf "${rootfs_img}"
touch "${rootfs_img}"
chattr +C "${rootfs_img}" >/dev/null 2>&1 || true
truncate -s 2G "${rootfs_img}"
mkfs.ext4 -q "${rootfs_img}"
mount_image
download_rootfs "$(newest_rootfs_version)" "${mount_dir}"
unmount_image
}
run_vm()
{
local kernel_bzimage="$1"
local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
if ! which "${QEMU_BINARY}" &> /dev/null; then
cat <<EOF
Could not find ${QEMU_BINARY}
Please install qemu or set the QEMU_BINARY environment variable.
EOF
exit 1
fi
${QEMU_BINARY} \
-nodefaults \
-display none \
-serial mon:stdio \
-cpu host \
-enable-kvm \
-smp 8 \
-m 4G \
-drive file="${rootfs_img}",format=raw,index=1,media=disk,if=virtio,cache=none \
-kernel "${kernel_bzimage}" \
-append "root=/dev/vda rw console=ttyS0,115200"
}
copy_logs()
{
local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
local log_file="${mount_dir}/root/${LOG_FILE}"
local exit_status_file="${mount_dir}/root/${EXIT_STATUS_FILE}"
mount_image
sudo cp ${log_file} "${OUTPUT_DIR}"
sudo cp ${exit_status_file} "${OUTPUT_DIR}"
sudo rm -f ${log_file}
unmount_image
}
is_rel_path()
{
local path="$1"
[[ ${path:0:1} != "/" ]]
}
update_kconfig()
{
local kconfig_file="$1"
local update_command="curl -sLf ${KCONFIG_URL} -o ${kconfig_file}"
# Github does not return the "last-modified" header when retrieving the
# raw contents of the file. Use the API call to get the last-modified
# time of the kernel config and only update the config if it has been
# updated after the previously cached config was created. This avoids
# unnecessarily compiling the kernel and selftests.
if [[ -f "${kconfig_file}" ]]; then
local last_modified_date="$(curl -sL -D - "${KCONFIG_API_URL}" -o /dev/null | \
grep "last-modified" | awk -F ': ' '{print $2}')"
local remote_modified_timestamp="$(date -d "${last_modified_date}" +"%s")"
local local_creation_timestamp="$(stat -c %Y "${kconfig_file}")"
if [[ "${remote_modified_timestamp}" -gt "${local_creation_timestamp}" ]]; then
${update_command}
fi
else
${update_command}
fi
}
main()
{
local script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
local kernel_checkout=$(realpath "${script_dir}"/../../../../)
# By default the script searches for the kernel in the checkout directory but
# it also obeys environment variables O= and KBUILD_OUTPUT=
local kernel_bzimage="${kernel_checkout}/${X86_BZIMAGE}"
local command="${DEFAULT_COMMAND}"
local update_image="no"
local exit_command="poweroff -f"
local debug_shell="no"
while getopts 'hskid:j:' opt; do
case ${opt} in
i)
update_image="yes"
;;
d)
OUTPUT_DIR="$OPTARG"
;;
j)
NUM_COMPILE_JOBS="$OPTARG"
;;
s)
command=""
debug_shell="yes"
exit_command="bash"
;;
h)
usage
exit 0
;;
\? )
echo "Invalid Option: -$OPTARG"
usage
exit 1
;;
: )
echo "Invalid Option: -$OPTARG requires an argument"
usage
exit 1
;;
esac
done
shift $((OPTIND -1))
if [[ $# -eq 0 && "${debug_shell}" == "no" ]]; then
echo "No command specified, will run ${DEFAULT_COMMAND} in the vm"
else
command="$@"
fi
local kconfig_file="${OUTPUT_DIR}/latest.config"
local make_command="make -j ${NUM_COMPILE_JOBS} KCONFIG_CONFIG=${kconfig_file}"
# Figure out where the kernel is being built.
# O takes precedence over KBUILD_OUTPUT.
if [[ "${O:=""}" != "" ]]; then
if is_rel_path "${O}"; then
O="$(realpath "${PWD}/${O}")"
fi
kernel_bzimage="${O}/${X86_BZIMAGE}"
make_command="${make_command} O=${O}"
elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then
if is_rel_path "${KBUILD_OUTPUT}"; then
KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")"
fi
kernel_bzimage="${KBUILD_OUTPUT}/${X86_BZIMAGE}"
make_command="${make_command} KBUILD_OUTPUT=${KBUILD_OUTPUT}"
fi
populate_url_map
local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
echo "Output directory: ${OUTPUT_DIR}"
mkdir -p "${OUTPUT_DIR}"
mkdir -p "${mount_dir}"
update_kconfig "${kconfig_file}"
recompile_kernel "${kernel_checkout}" "${make_command}"
if [[ "${update_image}" == "no" && ! -f "${rootfs_img}" ]]; then
echo "rootfs image not found in ${rootfs_img}"
update_image="yes"
fi
if [[ "${update_image}" == "yes" ]]; then
create_vm_image
fi
update_selftests "${kernel_checkout}" "${make_command}"
update_init_script "${command}" "${exit_command}"
run_vm "${kernel_bzimage}"
if [[ "${command}" != "" ]]; then
copy_logs
echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
fi
}
catch()
{
local exit_code=$1
local exit_status_file="${OUTPUT_DIR}/${EXIT_STATUS_FILE}"
# This is just a cleanup and the directory may
# have already been unmounted. So, don't let this
# clobber the error code we intend to return.
unmount_image || true
if [[ -f "${exit_status_file}" ]]; then
exit_code="$(cat ${exit_status_file})"
fi
exit ${exit_code}
}
trap 'catch "$?"' EXIT
main "$@"