#!/usr/bin/env bash # # Copyright (c) 2018 Intel Corporation # # SPDX-License-Identifier: Apache-2.0 set -o errexit set -o nounset set -o pipefail [ -n "${DEBUG:-}" ] && set -o xtrace readonly script_dir="$(dirname $(readlink -f $0))" readonly script_name=${0##*/} readonly project_dir="$(dirname ${script_dir})" readonly tmp_dir=$(mktemp -t -d osbuilder-test.XXXXXXX) readonly tmp_rootfs="${tmp_dir}/rootfs-osbuilder" readonly images_dir="${tmp_dir}/images" readonly osbuilder_file="/var/lib/osbuilder/osbuilder.yaml" readonly docker_image="busybox" readonly systemd_docker_config_file="/etc/systemd/system/docker.service.d/kata-containers.conf" readonly sysconfig_docker_config_file="/etc/sysconfig/docker" readonly tests_repo="github.com/kata-containers/tests" readonly tests_repo_dir="${project_dir}/../tests" readonly mgr="${tests_repo_dir}/cmd/kata-manager/kata-manager.sh" readonly test_config=${script_dir}/test_config.sh readonly rootfs_builder=${project_dir}/rootfs-builder/rootfs.sh readonly DOCKER_RUNTIME=${DOCKER_RUNTIME:-runc} readonly RUNTIME=${RUNTIME:-kata-runtime} readonly MACHINE_TYPE=`uname -m` readonly KATA_HYPERVISOR="${KATA_HYPERVISOR:-}" readonly KATA_DEV_MODE="${KATA_DEV_MODE:-}" readonly ci_results_dir="/var/osbuilder/tests" readonly dracut_dir=${project_dir}/dracut readonly KVM_MISSING="$([ -e /dev/kvm ] || echo true)" build_images=1 build_initrds=1 typeset -a distrosSystemd distrosAgent distrosSystemd=() distrosAgent=() # Hashes used to keep track of image sizes. # - Key: name of distro. # - Value: colon-separated roots and image sizes ("${rootfs_size}:${image_size}"). typeset -A built_images typeset -A built_initrds # If set, show the reason why a container using the built images/initrds could # not be started. Needed only after all images/initrd built successfully typeset -A showKataRunFailure= source ${test_config} source "${project_dir}/scripts/lib.sh" usage() { cat <] Options: -h | --help # Show usage. --list # List all distros that can be tested. --test-images-only # Only run images tests for the list of distros under test. --test-initrds-only # Only run initrds tests for the list of distros under test. Commands: help : Show usage. When is specified, tests are run only for the specified . Otherwise, tests are run on all distros. $(basename ${test_config}) includes a list of distros to exclude from testing, depending on the detected test environment. However, when a is specified, distro exclusion based on $(basename ${test_config}) is not enforced. EOF } # Add an entry to the specified stats file add_to_stats_file() { local statsfile="$1" local name="$2" local entry="$3" local entry_type="$4" local rootfs_size_bytes local rootfs_size_mb local image_size_bytes local image_size_mb rootfs_size_bytes=$(echo "$entry"|cut -d: -f1) image_size_bytes=$(echo "$entry"|cut -d: -f2) rootfs_size_mb=$(bc <<< "scale=2; ${rootfs_size_bytes} / 2^20") image_size_mb=$(bc <<< "scale=2; ${image_size_bytes} / 2^20") printf '%12.12s\t%10.10s\t%12.12s\t%10.10s\t%-8.8s\t%-20.20s\n' \ "${image_size_bytes}" \ "${image_size_mb}" \ "${rootfs_size_bytes}" \ "${rootfs_size_mb}" \ "${entry_type}" \ "${name}" >> "$statsfile" } # Show the sizes of all the generated initrds and images show_stats() { local name local sizes local tmpfile=$(mktemp) trap 'rm -f $tmpfile' EXIT # images for name in "${!built_images[@]}" do sizes=${built_images[$name]} add_to_stats_file "$tmpfile" "$name" "$sizes" 'image' done # initrds if [ "$KATA_HYPERVISOR" != "firecracker" ]; then for name in "${!built_initrds[@]}" do sizes=${built_initrds[$name]} add_to_stats_file "$tmpfile" "$name" "$sizes" 'initrd' done fi info "Image and rootfs sizes (in bytes and MB), smallest image first:" echo printf '%12.12s\t%10.10s\t%12.12s\t%10.10s\t%-8.8s\t%-20.20s\n' \ "image-bytes" \ "image-MB" \ "rootfs-bytes" \ "rootfs-MB" \ "Type" \ "Name" sort -k1,1n -k3,3n "$tmpfile" } # Run a kata-manager.sh command run_mgr() { [ -n "${KATA_DEV_MODE:-}" ] && return silent_run $mgr $* } exit_handler() { if [ "$?" -eq 0 ] then info "tests passed successfully - cleaning up" # Rootfs and images are owned by root sudo -E rm -rf "${tmp_rootfs}" sudo -E rm -rf "${images_dir}" rm -rf "${tmp_dir}" # Restore the default image in config file [ -n "${KVM_MISSING:-}" ] || run_mgr configure-image return fi info "ERROR: test failed" # The test failed so dump what we can if [ -d "${tmp_rootfs}" ]; then info "rootfs:" sudo -E ls -l "${tmp_rootfs}" >&2 sudo -E rm -rf "${tmp_rootfs}" else info "no rootfs created" # If no rootfs are created, no need to dump other info return fi if [ -d "${images_dir}" ]; then info "images:" sudo -E ls -l "${images_dir}" >&2 sudo -E rm -rf "${images_dir}" else info "no images created" # If no images are created, no need to dump other info return fi if [ -z "${showKataRunFailure}" ]; then # Restore the default image in config file run_mgr configure-image return fi info "local runtime config:" cat /etc/kata-containers/configuration.toml >&2 info "main runtime config:" cat /usr/share/defaults/kata-containers/configuration.toml >&2 info "collect script output:" sudo -E kata-collect-data.sh >&2 info "processes:" sudo -E ps -efwww | grep -E "docker|kata" >&2 # Restore the default image in config file run_mgr configure-image } die() { msg="$*" echo "ERROR: $msg" >&2 exit 1 } info() { s="$*" echo -en "INFO: $s\n" >&2 } debug() { [ -z "${TEST_DEBUG:-}" ] && return s="$*" echo -e "DBG: $s" >&2 } # Run a command in silent mode using chronic. # The command output is printed only if the command fails silent_run() { typeset -a commandLine=("$@") info "running: ${commandLine[@]}" if [ -z "${DEBUG:-}" ]; then chronic "${commandLine[@]}" else "${commandLine[@]}" fi } set_runtime() { local name="$1" [ -z "$name" ] && die "need name" [ -n "${KATA_DEV_MODE}" ] && return [ -n "${KVM_MISSING:-}" ] && return if [ "$KATA_HYPERVISOR" != "firecracker" ]; then if [ -f "$sysconfig_docker_config_file" ]; then docker_config_file="$sysconfig_docker_config_file" sed_script="s|^( *DOCKER_OPTS=.+--default-runtime[= ] *)[^ \"]+(.*\"$)|\1${name}\2|g" else docker_config_file="$systemd_docker_config_file" sed_script="s/--default-runtime[= ][^ ]*/--default-runtime=${name}/g" fi sudo -E sed -i -E "$sed_script" "$docker_config_file" sudo -E systemctl daemon-reload sudo -E systemctl restart docker fi } setup() { mkdir -p "${images_dir}" [ -n "${KVM_MISSING:-}" ] && return [ ! -d "${tests_repo_dir}" ] && git clone "https://${tests_repo}" "${tests_repo_dir}" if [ -z "${KATA_DEV_MODE}" ]; then mkdir -p /etc/kata-containers/ sudo cp -a /usr/share/defaults/kata-containers/configuration.toml /etc/kata-containers/configuration.toml else info "Running with KATA_DEV_MODE set, skipping installation of docker and kata packages" fi run_mgr enable-debug # "docker build" does not work with a VM-based runtime, and # also does not accept a --runtime option, so our only # option is to overwrite the system docker default runtime set_runtime "${DOCKER_RUNTIME}" } # Fetches the distros test configuration from the distro-specific config.sh file. # $1 : only fetch configuration for the distro with name $1. When not specified, # fetch configuration for all distros. get_distros_config() { local distro="$1" local distrosList local -A distroCfg=(\ [INIT_PROCESS]=\ [ARCH_EXCLUDE_LIST]=\ ) if [ -n "$distro" ]; then distrosList=("$distro") # When specifying a single distro name, skip does not apply skipWhenTestingAll=() else distrosList=($(make list-distros)) fi for d in ${distrosList[@]:-}; do debug "Getting config for distro $d" distroPattern="\<${d}\>" if [[ "${skipWhenTestingAll[@]:-}" =~ $distroPattern ]]; then info "Skipping distro $d as specified by $(basename ${test_config})" continue fi tmpfile=$(mktemp /tmp/osbuilder-$d-config.XXX) trap 'rm -f $tmpfile' EXIT ${rootfs_builder} -t $d > $tmpfile # Get value of all keys in distroCfg for k in ${!distroCfg[@]}; do distroCfg[$k]="$(awk -v cfgKey=$k 'BEGIN{FS=":\t+"}{if ($1 == cfgKey) print $2}' $tmpfile)" debug "distroCfg[$k]=${distroCfg[$k]}" done machinePattern="\<${MACHINE_TYPE}\>" if [[ "${distroCfg[ARCH_EXCLUDE_LIST]}" =~ $machinePattern ]]; then info "Skipping distro $d on architecture $MACHINE_TYPE" continue fi case "${distroCfg[INIT_PROCESS]}" in systemd) distrosSystemd+=($d) ;; kata-agent) distrosAgent+=($d) ;; *) die "Invalid init process specified for distro $d: \"${distroCfg[INIT_PROCESS]}\"" ;; esac done } create_container() { # If KATA_DEV_MODE is set, we don't have any way to point kata-runtime # at the image/initrd to boot, so there's nothing to do [ -n "${KATA_DEV_MODE}" ] && return out=$(mktemp) local file="/proc/version" # Create a container using the runtime under test which displays a # file that is expected to exist. docker run --rm -i --runtime "${RUNTIME}" "$docker_image" cat "${file}" > "$out" info "contents of docker image ${docker_image} container file '${file}':" cat "${out}" >&2 [ -s "$out" ] rm -f "$out" } install_image_create_container() { local file="$1" [ -z "$file" ] && die "need file" [ ! -e "$file" ] && die "file does not exist: $file" [ -n "${KVM_MISSING:-}" ] && return showKataRunFailure=1 run_mgr reset-config run_mgr enable-vsock run_mgr configure-image "$file" create_container showKataRunFailure= } install_initrd_create_container() { local file="$1" [ -z "$file" ] && die "need file" [ ! -e "$file" ] && die "file does not exist: $file" [ -n "${KVM_MISSING:-}" ] && return showKataRunFailure=1 run_mgr reset-config run_mgr enable-vsock run_mgr configure-initrd "$file" create_container showKataRunFailure= } # Displays a list of distros which can be tested list_distros() { tr " " "\n" <<< "${distrosSystemd[@]:-} ${distrosAgent[@]:-}" | sort } # # Calls the `GNU make` utility with the set of passed arguments. # Arguments can either be make targets or make variables assignments (in the form of VARIABLE=) # call_make() { targetType=$1 shift makeVars=() makeTargets=() # Split args between make variable and targets for t in $@; do # RE to match a make variable assignment pattern="^\w+\=" if [[ "$t" =~ $pattern ]]; then makeVars+=("$t") else makeTargets+=($targetType-$t) fi done # Set a default make target [ "${#makeTargets[@]}" = "0" ] && makeTargets+=($targetType) makeJobs= ((makeJobs=$(nproc) / 2)) # When calling make, do not use the silent_run wrapper, pass the # OSBUILDER_USE_CHRONIC instead. # In this way running make in parallel mode will, in case of failure, just # show the print out of the single target failing. makeVars+=(OSBUILDER_USE_CHRONIC=1) info "Starting make with \n\ # of // jobs: ${makeJobs:-[unlimited]} \n\ targets: ${makeTargets[@]} \n\ variables: ${makeVars[@]}" sudo -E make -j $makeJobs ${makeTargets[@]} ${makeVars[@]} } make_rootfs() { call_make rootfs $@ } make_image() { call_make image $@ } make_initrd() { call_make initrd $@ } get_rootfs_size() { [ $# -ne 1 ] && die "get_rootfs_size: wrong number of arguments" local rootfs_dir=$1 ! [ -d "$rootfs_dir" ] && die "$rootfs_dir is not a valid rootfs path" sudo -E du -sb "${rootfs_dir}" | awk '{print $1}' } show_rootfs_metadata() { [ $# -ne 1 ] && die "show_rootfs_metadata: wrong number of arguments" local rootfs_path=$1 local osbuilder_file_fullpath="${rootfs_path}/${osbuilder_file}" yamllint "${osbuilder_file_fullpath}" info "osbuilder metadata file for $d:" cat "${osbuilder_file_fullpath}" >&2 } # Create an image and/or initrd for the available distributions, # then test each by configuring the runtime and creating a container. # # When passing the name of a distribution, tests are run against that # distribution only. # # Parameters: # # 1: distro name. # test_distros() { local distro="$1" get_distros_config "$distro" local commonMakeVars=( \ USE_DOCKER=true \ DOCKER_RUNTIME="${DOCKER_RUNTIME}" \ ROOTFS_BUILD_DEST="$tmp_rootfs" \ IMAGES_BUILD_DEST="$images_dir" \ DEBUG=1 ) # If a distro was specified, filter out the distro list to only include that distro if [ -n "$distro" ]; then pattern="\<$distro\>" if [[ "${distrosAgent[@]:-}" =~ $pattern ]]; then distrosAgent=($distro) distrosSystemd=() elif [[ "${distrosSystemd[@]:-}" =~ $pattern ]]; then distrosSystemd=($distro) distrosAgent=() build_initrds= else die "Not a valid distro: $distro" fi info "Running tests for distro: $distro" else info "Running tests for all distros" # Graceful exit allowed for selected distros, but only when testing all distros commonMakeVars+=(GRACEFUL_EXIT=1) fi # distro with systemd as init -> normal rootfs image # distro with kata-agent as init -> normal rootfs image AND initrd image # If user does not need rootfs images, then do not build systemd rootfses [ -z "$build_images" ] && distrosSystemd=() # Build systemd and agent rootfs with 2 separate jobs bgJobs=() if [ ${#distrosSystemd[@]} -gt 0 ]; then info "building rootfses with systemd as init: ${distrosSystemd[@]}" make_rootfs ${commonMakeVars[@]} "${distrosSystemd[@]}" & bgJobs+=($!) fi if [ "$KATA_HYPERVISOR" != "firecracker" ]; then if [ ${#distrosAgent[@]} -gt 0 ]; then info "building all rootfses with kata-agent as init" make_rootfs ${commonMakeVars[@]} AGENT_INIT=yes "${distrosAgent[@]}" & bgJobs+=($!) fi fi # Check for build failures (`wait` remembers up to CHILD_MAX bg processes exit status) for j in ${bgJobs[@]}; do if ! wait $j; then info "Background rootfs build job failed:" #find completed an uncompleted jobs checking for the rootfs marker local marker=$(make print-ROOTFS_MARKER_SUFFIX) [ -z "$marker" ] && die "Invalid rootfs marker" typeset -a completed=($(find ${tmp_rootfs} -name ".*${marker}" -exec basename {} \; | sed -E "s/\.(.+)${marker}/\1/")) for d in "${distrosSystemd[@]:-}" "${distrosAgent[@]:-}"; do if [[ "${completed[@]}" =~ $d ]]; then info "- $d : completed" else info "- $d : failed" fi done die "rootfs build failed" fi done # TODO: once support for rootfs images with kata-agent as init is in place, # uncomment the following line # for d in ${distrosSystemd[@]} ${distrosAgent[@]}; do for d in ${distrosSystemd[@]:-}; do local rootfs_path="${tmp_rootfs}/${d}_rootfs" local image_path="${images_dir}/kata-containers-image-$d.img" local rootfs_size=$(get_rootfs_size "$rootfs_path") # Skip failed distros if [ -e "${tmp_rootfs}/${d}_fail" ]; then info "Building rootfs for ${d} failed, not creating an image" continue fi show_rootfs_metadata "$rootfs_path" info "Making rootfs image for ${d}" make_image ${commonMakeVars[@]} $d local image_size=$(stat -c "%s" "${image_path}") built_images["${d}"]="${rootfs_size}:${image_size}" info "Creating container for ${d}" install_image_create_container $image_path done for d in ${distrosAgent[@]:-}; do local rootfs_path="${tmp_rootfs}/${d}_rootfs" local initrd_path="${images_dir}/kata-containers-initrd-$d.img" local rootfs_size=$(get_rootfs_size "$rootfs_path") # Skip failed distros if [ -e "${tmp_rootfs}/${d}_fail" ]; then info "Building rootfs for ${d} failed, not creating an initrd" continue fi if [ "$KATA_HYPERVISOR" != "firecracker" ]; then info "Making initrd image for ${d}" make_initrd ${commonMakeVars[@]} AGENT_INIT=yes $d local initrd_size=$(stat -c "%s" "${initrd_path}") built_initrds["${d}"]="${rootfs_size}:${initrd_size}" info "Creating container for ${d}" install_initrd_create_container $initrd_path fi done show_stats } test_dracut() { local initrd_path="${images_dir}/kata-containers-initrd-dracut.img" local image_path="${images_dir}/kata-containers-image-dracut.img" local rootfs_path="${tmp_rootfs}/dracut_rootfs" local overlay_path="${tmp_rootfs}/dracut_overlay" detect_go_version || die "Could not detect the required Go version for AGENT_VERSION='${AGENT_VERSION:-master}'." detect_rust_version || die "Could not detect the required rust version for AGENT_VERSION='${AGENT_VERSION:-master}'." generate_dockerfile ${dracut_dir} info "Creating container for dracut" silent_run docker build -t dracut-test-osbuilder ${dracut_dir} typeset -a dockerRunArgs=(\ --rm \ --runtime="${DOCKER_RUNTIME}" \ -v "${project_dir}":"${project_dir}" \ -v "${tmp_dir}":"${tmp_dir}" \ -v /etc/localtime:/etc/localtime:ro \ dracut-test-osbuilder \ ) typeset -a makeVars=(\ BUILD_METHOD=dracut \ TARGET_INITRD="${initrd_path}" \ TARGET_IMAGE=${image_path} \ TARGET_ROOTFS=${rootfs_path} \ DRACUT_OVERLAY_DIR="${overlay_path}" \ USE_DOCKER=1 \ DOCKER_RUNTIME="${DOCKER_RUNTIME}" \ ) info "Making image for dracut inside a container" silent_run docker run ${dockerRunArgs[@]} make -C ${project_dir} ${makeVars[@]} rootfs make_image ${makeVars[@]} local image_size=$(stat -c "%s" "${image_path}") local rootfs_size=$(get_rootfs_size "$rootfs_path") built_images["dracut"]="${rootfs_size}:${image_size}" info "Creating container for dracut" install_image_create_container $image_path if [ "$KATA_HYPERVISOR" != "firecracker" ]; then info "Making initrd for dracut inside a container" silent_run docker run ${dockerRunArgs[@]} make -C ${project_dir} ${makeVars[@]} AGENT_INIT=yes clean initrd local initrd_size=$(stat -c "%s" "${initrd_path}") built_initrds["dracut"]="${rootfs_size}:${initrd_size}" install_initrd_create_container $initrd_path fi } main() { local args=$(getopt \ -n "$script_name" \ -a \ --options="h" \ --longoptions="help distro: list test-images-only test-initrds-only" \ -- "$@") eval set -- "$args" [ $? -ne 0 ] && { usage >&2; exit 1; } local distro= while [ $# -gt 1 ] do case "$1" in -h|--help) usage; exit 0 ;; --list) list_distros; exit 0;; --test-images-only) build_initrds= ;; --test-initrds-only) build_images= ;; --) shift; break ;; esac shift done # Consume getopt cruft [ "$1" = "--" ] && shift case "${1:-}" in help) usage; exit 0;; *) distro="${1:-}";; esac trap exit_handler EXIT ERR setup # Run only if distro is not dracut [ "${distro:-}" != "dracut" ] && test_distros "$distro" # Run if distro is empty or it is dracut [ -z "$distro" ] || [ "$distro" = "dracut" ] && test_dracut # We shouldn't really need a message like this but the CI can fail in # mysterious ways so make it clear! info "all tests finished successfully" } main "$@"