Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ examples/aws/credentials.json
# Project-specific config (generated by setup configure)
run-config.json
demo-config.json
pgbench-config.json

# Test kernel RPMs (large binary files)
setup/test-kernel-rpms/
Expand Down
7 changes: 5 additions & 2 deletions src/kernel_ci_cloud_labs/providers/aws_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,10 @@ def wait_for_task_completion(self):
finishes the kernelci-api node incomplete/Infrastructure with the
matched line surfaced in error_msg.
* No new VM console output for PULLAB_TASK_HANG_THRESHOLD_SEC seconds
(default 600) -- silent stall, same treatment as a crash.
(default 1200) -- silent stall, same treatment as a crash. The
default accommodates CPU-heavy benchmarks (e.g. UnixBench) whose
console goes quiet for many minutes during a run; lower it via the
env var for faster hang detection on lighter workloads.
* Overall PULLAB_TASK_WAIT_TIMEOUT_SEC seconds elapsed (default 3600)
-- final safety net for whatever isn't covered above.

Expand All @@ -331,7 +334,7 @@ def wait_for_task_completion(self):

poll_interval = float(os.getenv("PULLAB_TASK_POLL_INTERVAL_SEC") or 30)
log_interval = float(os.getenv("PULLAB_TASK_PROGRESS_LOG_SEC") or 120)
hang_threshold = float(os.getenv("PULLAB_TASK_HANG_THRESHOLD_SEC") or 600)
hang_threshold = float(os.getenv("PULLAB_TASK_HANG_THRESHOLD_SEC") or 1200)
overall_timeout = float(os.getenv("PULLAB_TASK_WAIT_TIMEOUT_SEC") or 3600)

start = time.time()
Expand Down
21 changes: 15 additions & 6 deletions tests/test-in-venv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,27 @@
set -e

# Configuration
# PYTHON selects the interpreter used to create the virtual environment.
# Override it to build the venv with a specific version, e.g.
# PYTHON=python3.12 tests/test-in-venv.sh
# It may be a name on PATH or an absolute path. All pip/pytest calls go through
# "<python> -m ..." (never the bare pip/python3 shims).
PYTHON="${PYTHON:-python3}"
VENV_DIR=".venv-testing"
MODULE_DIR="$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
STATUS_CACHE="$MODULE_DIR/$VENV_DIR/.git_status_cache"
# Interpreter inside the venv (created from $PYTHON). Used for pip/pytest so the
# correct environment is targeted regardless of which binary bootstrapped it.
VENV_PYTHON="$MODULE_DIR/$VENV_DIR/bin/python"

# Function to create and setup virtual environment
setup_virtual_environment()
{
echo "Setting up virtual environment..."
python3 -m venv "${VENV_DIR}"
echo "Setting up virtual environment with '${PYTHON}'..."
"${PYTHON}" -m venv "${VENV_DIR}"
source "${VENV_DIR}/bin/activate"
pip install --upgrade pip
pip install -e ".[dev]"
"${VENV_PYTHON}" -m pip install --upgrade pip
"${VENV_PYTHON}" -m pip install -e ".[dev]"
}

# Function to activate virtual environment
Expand All @@ -40,7 +49,7 @@ install_module()
{
echo "Installing module..." 1>&2
status=0
output=$(pip install -e "${MODULE_DIR}" 2>&1) || status=$?
output=$("${VENV_PYTHON}" -m pip install -e "${MODULE_DIR}" 2>&1) || status=$?
if [ $status -ne 0 ]; then
echo "Installation failed, with output:" 1>&2
echo "$output" 1>&2
Expand All @@ -53,7 +62,7 @@ run_tests()
{
echo "Running unit tests..." 1>&2
status=0
output=$(python3 -m pytest tests/ -v -m "not integration" 2>&1) || status=$?
output=$("${VENV_PYTHON}" -m pytest tests/ -v -m "not integration" 2>&1) || status=$?
if [ $status -eq 0 ]; then
echo "Unit tests passed" 1>&2
else
Expand Down
1 change: 1 addition & 0 deletions vm-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ See the main [README](../README.md) for writing new tests, configuration, and th
| `example-kernel-reboot-test` | 3 | yes | Installs two kernels with reboot between each |
| `simple-unixbench` | 1 | no | Runs UnixBench on the default kernel |
| `unixbench-kernel-regression` | 3 | yes | Installs two kernels, runs UnixBench on each, produces benchmark CSVs |
| `pgbench-kernel-regression` | 3 | yes | Installs two kernels, runs PostgreSQL pgbench (read-only + read-write) on each, produces benchmark CSVs |
| `simple-source-reboot` | 2 | yes | Installs kernel from source RPM, reboots, verifies |

## How Multi-Stage Tests Work
Expand Down
50 changes: 50 additions & 0 deletions vm-tests/TODO-shared-lib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Shared kernel-management helpers across vm-tests

## Approach (implemented)

The kernel install/upgrade helpers live once in:

vm-tests/lib/kernel_helpers.sh

Each kernel test includes it with a **symlink** in its own directory:

vm-tests/<test>/kernel_helpers.sh -> ../lib/kernel_helpers.sh

and its `common_lib.sh` sources it after setting `SOURCE_DIR`:

source "${SOURCE_DIR}/kernel_helpers.sh"

Why a symlink works with zero pipeline changes: `upload_test_payload()` builds
the payload with `Path(test_dir).rglob("*")` + `zf.write(...)`, which follows
the symlink and stores the **target's content** as a real file named
`kernel_helpers.sh`. On the VM the payload is extracted flat, so the test dir
gets a normal `kernel_helpers.sh` next to the `run*.sh` scripts.

Fix once, benefit everywhere: the underscore/dash RPM-version handling, the
FIPS-disable-before-reboot logic, and the `--allowerasing` cross-series install
live only in the shared lib.

## Status — migration complete

All kernel tests now source the shared lib and keep only their test-specific
functions in `common_lib.sh`:

- [x] `pgbench-kernel-regression` — pgbench/PostgreSQL functions local.
- [x] `example-kernel-reboot-test` — no test-specific functions; just sources
the shared lib.
- [x] `simple-source-reboot` — source-RPM build helpers
(`install_source_kernel_rpm`, `build_kernel_rpm_src`,
`get_first_source_kernel_rpm_from_dir`, `install_and_build_kernel`) local.
- [x] `unixbench-kernel-regression` — UnixBench helpers (`prepare_unixbench`,
`run_unixbench`, `summarize_unixbench_log`) local.

`simple-unixbench` and other non-kernel tests do not install kernels and do not
use the shared lib.

## Adding a new kernel test

1. `cd vm-tests/<test> && ln -s ../lib/kernel_helpers.sh kernel_helpers.sh`
2. In `common_lib.sh`, `source "${SOURCE_DIR}/kernel_helpers.sh"` and add only
test-specific functions.
3. Verify: `bash -n common_lib.sh` and a source-order smoke test with
`SOURCE_DIR` set.
258 changes: 8 additions & 250 deletions vm-tests/example-kernel-reboot-test/common_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,253 +2,11 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# Common functions for kernel reboot test

# Get results bucket and test paths from environment
RESULTS_BUCKET="${S3_BUCKET:-}"
ARCH=$(uname -m)
KERNEL_RPM_DIR="/tmp/kernel-rpms"

# Validate required environment variables
if [ -z "$RESULTS_BUCKET" ] || [ -z "$RUN_PREFIX" ] || [ -z "$TEST_NAME" ]; then
echo "ERROR: Missing required environment variables (S3_BUCKET, RUN_PREFIX, TEST_NAME)" >&2
exit 1
fi

# Error trap handler to show line where error occurred
error_trap()
{
local exit_code=$?
local line_number=$1
echo "$(date): ERROR: Script failed at line $line_number with exit code $exit_code"
echo "$(date): ERROR: Command that failed: $(sed -n "${line_number}p" "$0")"
exit $exit_code
}
trap 'error_trap $LINENO' ERR

#Return current runnning kernel
get_running_kernel()
{
uname -r
}

# Install a single given package
install_package()
{
local pkg="$1"
local output
echo "Installing package $pkg ..."
if output=$(sudo yum install -y "$pkg" 2>&1) || output=$(sudo dnf install -y "$pkg" 2>&1); then
return 0
else
echo "Failed to install package $pkg:"
echo "$output"
return 1
fi
}

# Install all dependencies for this test
install_test_dependencies()
{
local deps_file="${SOURCE_DIR}/dependencies.txt"

if [ -f "$deps_file" ]; then
while IFS= read -r pkg || [ -n "$pkg" ]; do
# Skip empty lines and comments
[[ -z "$pkg" || "$pkg" =~ ^[[:space:]]*# ]] && continue

# Remove leading/trailing whitespace
pkg=$(echo "$pkg" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')

# Install package if not empty
if [ -n "$pkg" ]; then
install_package "$pkg" || return 1
fi
done <"$deps_file"
else
# Fallback to hardcoded dependencies
install_package gcc make tar || return 1
fi
}

# List available kernels from S3
list_kernels_from_s3()
{
S3_PATH="s3://${RESULTS_BUCKET}/${RUN_PREFIX}/shared/kernel-rpms/binary/${ARCH}/"
aws s3 ls "${S3_PATH}" | grep "\.rpm$" | awk '{print $4}'
}

# Download specific kernel RPM from S3
download_kernel_rpm()
{
if [ -z "${1:-}" ]; then
echo "ERROR: download_kernel_rpm requires kernel_name parameter" >&2
return 1
fi
local kernel_name="$1"

S3_PATH="s3://${RESULTS_BUCKET}/${RUN_PREFIX}/shared/kernel-rpms/binary/${ARCH}/"

mkdir -p "$KERNEL_RPM_DIR"
local local_path="${KERNEL_RPM_DIR}/${kernel_name}"

# Download if not already present
if [ -f "$local_path" ]; then
echo "$local_path"
return 0
fi

if aws s3 cp "${S3_PATH}${kernel_name}" "$local_path" --no-progress >&2; then
echo "$local_path"
return 0
else
echo "ERROR: Failed to download kernel" >&2
return 1
fi
}

# Dump boot configuration for debugging kernel install issues
dump_boot_info()
{
echo "=== Boot Debug Info ==="
echo "--- OS ---"
head -2 /etc/os-release 2>/dev/null || true
echo "--- Running kernel ---"
uname -r
echo "--- Installed kernel packages ---"
rpm -qa 'kernel*' | sort
echo "--- vmlinuz files in /boot ---"
ls -la /boot/vmlinuz-* 2>/dev/null || echo "(none)"
echo "--- BLS entries ---"
ls -la /boot/loader/entries/ 2>/dev/null || echo "(no BLS directory)"
echo "--- grubby default ---"
sudo grubby --default-kernel 2>/dev/null || echo "(grubby --default-kernel failed)"
echo "--- grubby --info=ALL ---"
sudo grubby --info=ALL 2>/dev/null || echo "(grubby --info=ALL failed)"
echo "=== End Boot Debug Info ==="
}

# Install kernel RPM, make sure it's used as boot target
install_kernel_rpm()
{
if [ -z "${1:-}" ]; then
echo "ERROR: install_kernel_rpm requires kernel_rpm parameter" >&2
return 1
fi
local kernel_rpm="$1"

# Check architecture compatibility
local host_arch=$(uname -m)
local rpm_arch=$(rpm -qp --queryformat '%{ARCH}' "$kernel_rpm" 2>/dev/null)

if [ "$rpm_arch" != "$host_arch" ]; then
echo "ERROR: Architecture mismatch - Host: $host_arch, RPM: $rpm_arch" >&2
return 1
fi

echo "kernel before installation: $(uname -r)"
echo "Installing kernel from $kernel_rpm (arch: $rpm_arch)"

if sudo yum localinstall -y "$kernel_rpm" 2>/dev/null || sudo dnf install -y "$kernel_rpm" 2>/dev/null; then
dump_boot_info

# Set the newly installed kernel as default boot target.
# Without this, GRUB boots the newest kernel which may not be the one we just installed.
local installed_version
installed_version=$(rpm -qp --queryformat '%{VERSION}' "$kernel_rpm" 2>/dev/null)

# Find the grubby entry matching the installed kernel version.
# Use grep || true to avoid ERR trap when no match is found.
local grub_kernel
grub_kernel=$(sudo grubby --info=ALL 2>/dev/null \
| grep "^kernel=" \
| grep "$installed_version" \
| head -1 \
| sed 's/^kernel=//' \
| tr -d '"' \
|| true)

if [ -z "$grub_kernel" ]; then
# Upstream make binrpm-pkg kernels don't register with grubby.
# Find the vmlinuz file and add a boot entry manually.
local vmlinuz
vmlinuz=$(ls /boot/vmlinuz-*"$installed_version"* 2>/dev/null | head -1)
if [ -n "$vmlinuz" ]; then
echo "Adding grubby entry for $vmlinuz"
local initrd="/boot/initramfs-${installed_version}.img"
if [ ! -f "$initrd" ]; then
echo "Generating initramfs at $initrd"
sudo dracut --force "$initrd" "$installed_version" 2>/dev/null \
|| sudo mkinitrd "$initrd" "$installed_version" 2>/dev/null \
|| true
fi
if [ -f "$initrd" ]; then
sudo grubby --add-kernel="$vmlinuz" \
--initrd="$initrd" \
--title="Linux $installed_version" \
--copy-default \
--make-default
echo "✓ Added and set default: $vmlinuz"
else
echo "WARNING: No initramfs for $installed_version, trying set-default anyway"
sudo grubby --set-default="$vmlinuz" || true
fi
grub_kernel="$vmlinuz"
else
echo "WARNING: No vmlinuz found for version $installed_version"
fi
else
echo "Setting default boot kernel to $grub_kernel"
sudo grubby --set-default="$grub_kernel"
fi

if [ -n "$grub_kernel" ]; then
echo "Verifying default kernel:"
sudo grubby --default-kernel
fi
return 0
else
echo "ERROR: Failed to install new kernel" >&2
return 1
fi
}

# Return kernel RPM with lowest version (downloads from S3)
get_first_kernel_rpm_from_dir()
{
local kernels=$(list_kernels_from_s3 | sort -V)
local first_kernel=$(echo "$kernels" | head -n 1)

if [ -z "$first_kernel" ]; then
return 1
fi

download_kernel_rpm "$first_kernel"
}

# Return kernel RPM with highest version (downloads from S3)
get_last_kernel_rpm_from_dir()
{
local kernels=$(list_kernels_from_s3 | sort -V)
local last_kernel=$(echo "$kernels" | tail -n 1)

if [ -z "$last_kernel" ]; then
return 1
fi

download_kernel_rpm "$last_kernel"
}

# Install a given kernel RPM (passed as argument)
install_specified_kernel_rpm()
{
local kernel_rpm="$1"

if [ -z "$kernel_rpm" ]; then
echo "ERROR: install_specified_kernel_rpm requires a kernel RPM path"
return 1
fi

echo "Installing kernel RPM: $(basename "$kernel_rpm")"
install_kernel_rpm "$kernel_rpm"
}
# Common functions for the kernel reboot test.
#
# All kernel-management logic (environment validation, kernel RPM
# download/selection, install_kernel_rpm, reboot helpers) lives in the shared
# vm-tests/lib/kernel_helpers.sh, included here via the kernel_helpers.sh
# symlink in this directory. SOURCE_DIR is set by the run script before this
# file is sourced.
source "${SOURCE_DIR}/kernel_helpers.sh"
Loading
Loading