#!/bin/bash
# Wrapper script for gen_livepatch.sh. Runs gen_livepatch.sh inside
# of a docker container. Makes it much easier to install dependencies,
# control build environment, etc.
#
# Args:
# -k: Specifies the kernel version. If not set, builds native livepatch
# -p: Patch file list. Need at least one patch file listed here
# -n: Output file name. Will be default if not specified.
# -o: Output directory for livepatch modules
# -R: Don't set replace flag in livepatch module. Replace flag is set by default.
# -d: Use file contents as description field for livepatch module.
# --export-debuginfo: Save debug files such as patched vmlinux, changed objs, etc.
# -h/--help: Prints help message
# --rpm: Package the kernel module as an rpm
# --rpm-version: Specify the version number of the rpm
# --rpm-release: Specify the release number of the rpm
# --rpm-desc: Specify the description file for the rpm. If not set, it will be the same as the module.
#
#
# ex)
# With all options enabled, and multiple patches:
# auto_livepatch -k 4.19.247-2.ph3 -o my_dir -n my_livepatch.ko -p my_patch1.patch my_patch2.patch -d description.txt
# ex)
# All default settings. Must supply at least one patch file though. Builds a livepatch for the current kernel version
# auto_livepatch -p my_patch.patch
set -o pipefail
# keeps track of what version of kpatch-utils this is
# very important to know when to rebuild the docker images
VERSION_TAG=4
if [[ "$EUID" -ne 0 ]]; then
echo "Please run as root user"
exit 1
fi
# check to make sure docker is installed and running
if [[ $(systemctl is-active docker) != active ]]; then
echo "Looks like docker is either not installed or not running. Please install or start docker"
echo "To install: tdnf install docker"
echo "To start: systemctl start docker"
exit 1
fi
AUTO_LIVEPATCH_DIR=/var/opt/auto_livepatch
DOCKER=/usr/bin/docker
DEFAULT_OUTPUT_DIR=$AUTO_LIVEPATCH_DIR/livepatches
DEBUGINFO_DIR=$AUTO_LIVEPATCH_DIR/debuginfo
OUTPUT_DIR=$DEFAULT_OUTPUT_DIR
ARGS=""
PH_TAG=""
DOCKER_BUILDDIR=/var/opt/gen_livepatch
DOCKER_LIVEPATCH_DIR=$DOCKER_BUILDDIR/livepatches
DOCKER_IMAGE_NAME=""
DOCKER_DEBUGINFO_DIR=/var/opt/gen_livepatch/debuginfo
DOCKER_CONTAINER_NAME=""
DOCKERFILE_NAME=""
DOCKERFILE_DIR=/etc/auto_livepatch/dockerfiles
PATCH_DIR=$AUTO_LIVEPATCH_DIR/patches
DOCKER_KPATCH_BUILDLOG=/root/.kpatch/build.log
FAILED=0
EXPORT_DEBUGINFO=0
DESC_GIVEN=0
RPM_DESC_GIVEN=0
SRC_RPM_LOCAL_PATH=""
DEBUGINFO_LOCAL_PATH=""
# args
# 1. string to match
# 2. array values
match_string_in_array() {
local string_to_match=$1
shift
while (( $# )); do
if [[ "$string_to_match" == "$1" ]]; then
return 0
fi
shift
done
return 1
}
gen_dockerfile() {
local dockerfile=$DOCKERFILE_DIR/$DOCKERFILE_NAME
[[ -d $DOCKERFILE_DIR ]] || mkdir -p $DOCKERFILE_DIR
# don't need to rebuild if already present
[[ -f "$dockerfile" ]] && return 0
local pkgs_req=( "build-essential" \
"elfutils-libelf-devel" \
"tar" \
"findutils" \
"audit-devel" \
"binutils-devel" \
"elfutils-devel" \
"gdb" \
"glib-devel" \
"kmod-devel" \
"libcap-devel" \
"libunwind-devel" \
"openssl-devel" \
"pciutils-devel" \
"procps-ng-devel" \
"python3-devel" \
"slang-devel" \
"xz-devel" \
"rpm-build" \
"bc" \
"Linux-PAM-devel" \
"kbd" \
"libdnet-devel" \
"libmspack-devel" \
"xerces-c-devel" \
"xml-security-c-devel" \
"coreutils" \
"util-linux" \
"kpatch" \
"kpatch-build" \
"kpatch-utils" )
echo "FROM photon:${PH_TAG//[^0-9]/}.0" > $dockerfile
echo "RUN tdnf install -y \\" >> $dockerfile
for pkg in ${pkgs_req[@]}; do
if [[ "$pkg" == "${pkgs_req[-1]}" ]]; then
echo "$pkg" >> $dockerfile
else
echo "$pkg \\" >> $dockerfile
fi
done
}
# just get what we need for this script from the arguments.
# do some error checking here so that it will error out before creating
# docker container, which could take some time if the image is not
# already created.
parse_args() {
# just print help message if no arguments
if [ $# -eq 0 ]; then
source gen_livepatch.sh
elif [[ $1 != -* ]]; then
echo "Flag must be set before any other parameters"
exit 1
fi
mkdir -p $PATCH_DIR
local flag=""
local patch_given=0
local flags=( "-s" "-v" "-p" "-o" "-h" "--help" "-k" "-n" "-R" "--export-debuginfo" "-d" "--rpm" "--rpm-version" "--rpm-release" "--rpm-desc" )
local no_arg_flags=( "-R" "--export-debuginfo" "-h" "--help" "--rpm" )
while (( "$#" )); do
arg=$1
if [[ $1 == -* ]]; then
flag=$1
if ! match_string_in_array $flag ${flags[@]}; then
error "Unknown option $flag"
elif [[ $1 == -h || $1 == --help ]]; then
source gen_livepatch.sh
exit 0
elif [[ $1 == --export-debuginfo ]]; then
EXPORT_DEBUGINFO=1
elif ! match_string_in_array $flag ${no_arg_flags[@]} && [[ ($2 == -* || -z $2) ]]; then
error "$1 needs at least one argument"
elif [[ $3 != -* && $flag != -p && -n $3 ]] && ! match_string_in_array $flag ${no_arg_flags[@]} ; then
error "$1 only takes one argument"
fi
else
case "$flag" in
-p)
patch_given=1
cp "$1" $PATCH_DIR &> /dev/null || error "Couldn't find patch file $1"
arg="$DOCKER_BUILDDIR/patches/$(basename "$arg")"
;;
-k)
VERSION_RELEASE_FLAVOR=$1
;;
-o)
OUTPUT_DIR=$1
;;
-d)
DESC_GIVEN=1
cp "$1" "$AUTO_LIVEPATCH_DIR/description.txt" &> /dev/null || error "Description file $1 not found"
;;
--rpm-desc)
RPM_DESC_GIVEN=1
cp "$1" "$AUTO_LIVEPATCH_DIR/rpm-description.txt" &> /dev/null || error "RPM description file $1 not found"
;;
-s)
SRC_RPM_LOCAL_PATH=$1
;;
-v)
DEBUGINFO_LOCAL_PATH=$1
;;
esac
fi
# pass all arguments except for output directory into docker container
if [[ $flag != "-o" ]] && [[ $flag != "-d" ]] && [[ $flag != "--rpm-desc" ]] && [[ $flag != "-s" ]] && [[ $flag != "-v" ]]; then
if [ -z "$ARGS" ]; then
ARGS="$arg"
else
ARGS="$ARGS $arg"
fi
fi
# shift to the next argument
shift
done
if [[ $patch_given -eq 0 ]]; then
error "Please input at least one patch file"
fi
if [[ -n "$SRC_RPM_LOCAL_PATH" ]]; then
ARGS="$ARGS -s $DOCKER_BUILDDIR/$(basename $SRC_RPM_LOCAL_PATH)"
fi
if [[ -n "$DEBUGINFO_LOCAL_PATH" ]]; then
ARGS="$ARGS -v $DOCKER_BUILDDIR/$(basename $DEBUGINFO_LOCAL_PATH)"
fi
#make sure description file is easily accessible
if [[ $DESC_GIVEN -eq 1 ]]; then
ARGS="$ARGS -d $DOCKER_BUILDDIR/description.txt"
fi
if [[ $RPM_DESC_GIVEN -eq 1 ]]; then
ARGS="$ARGS --rpm-desc $DOCKER_BUILDDIR/rpm-description.txt"
fi
# output livepatch(es) to the same folder in the docker container
ARGS="$ARGS -o livepatches"
if [ -z "$VERSION_RELEASE_FLAVOR" ]; then
VERSION_RELEASE_FLAVOR=$(uname -r)
fi
[[ $VERSION_RELEASE_FLAVOR =~ \.ph[0-9]+ ]] && PH_TAG="${BASH_REMATCH:1}"
if [ -z "$PH_TAG" ]; then
echo "Wrong kernel version detected: $VERSION_RELEASE_FLAVOR"
echo "Check for typos. Make sure it is in the same format as uname -r"
exit 1
fi
DOCKER_IMAGE_NAME=livepatch-$PH_TAG-$VERSION_TAG
DOCKER_CONTAINER_NAME=$PH_TAG-livepatch-container
DOCKERFILE_NAME=Dockerfile.$PH_TAG
gen_dockerfile
}
config_container() {
echo "Configuring docker container"
if [[ $OUTPUT_DIR == "$DEFAULT_OUTPUT_DIR" ]]; then
echo "No output directory specified, outputting to $DEFAULT_OUTPUT_DIR"
mkdir -p $DEFAULT_OUTPUT_DIR
fi
if [[ -z "$(docker images -q "$DOCKER_IMAGE_NAME" 2> /dev/null)" ]]; then
# clean up old docker images from alternate versions
$DOCKER rmi -f "$(docker images | grep livepatch-$PH_TAG | awk '{print $1}')" &> /dev/null
echo "No existing docker image found, building..."
$DOCKER build --network=host -f $DOCKERFILE_DIR/"$DOCKERFILE_NAME" -t "$DOCKER_IMAGE_NAME" . || error
fi
if [[ -n "$(docker ps -a -f "name=$DOCKER_CONTAINER_NAME" -q 2> /dev/null)" ]]; then
# clean up old docker container if it is still lurking around
$DOCKER rm -f "$DOCKER_CONTAINER_NAME" || error
fi
$DOCKER run --network=host -t -d --name "$DOCKER_CONTAINER_NAME" "$DOCKER_IMAGE_NAME" /bin/bash > /dev/null || error
# copy all necessary files into docker container, put patches into patches folder
$DOCKER exec -t "$DOCKER_CONTAINER_NAME" mkdir -p $DOCKER_LIVEPATCH_DIR || error
$DOCKER cp $PATCH_DIR "$DOCKER_CONTAINER_NAME":$DOCKER_BUILDDIR/ || error
if [[ $DESC_GIVEN -eq 1 ]]; then
$DOCKER cp "$AUTO_LIVEPATCH_DIR/description.txt" "$DOCKER_CONTAINER_NAME":$DOCKER_BUILDDIR/ || error
fi
if [[ $RPM_DESC_GIVEN -eq 1 ]]; then
$DOCKER cp "$AUTO_LIVEPATCH_DIR/rpm-description.txt" "$DOCKER_CONTAINER_NAME":$DOCKER_BUILDDIR/ || error
fi
if [[ -n "$SRC_RPM_LOCAL_PATH" ]]; then
[[ -f "$SRC_RPM_LOCAL_PATH" ]] || error "Failed to find local src rpm at $SRC_RPM_LOCAL_PATH"
$DOCKER cp "$SRC_RPM_LOCAL_PATH" "$DOCKER_CONTAINER_NAME":$DOCKER_BUILDDIR/ || error
fi
if [[ -n "$DEBUGINFO_LOCAL_PATH" ]]; then
[[ -f "$DEBUGINFO_LOCAL_PATH" ]] || error "Failed to find local debuginfo rpm at $DEBUGINFO_LOCAL_PATH"
$DOCKER cp "$DEBUGINFO_LOCAL_PATH" "$DOCKER_CONTAINER_NAME":$DOCKER_BUILDDIR/ || error
fi
}
# prints error message and then exits
error() {
if [[ -z "$1" ]]; then
echo "Error! Exiting." 1>&2
else
echo "ERROR: $1" 1>&2
fi
exit 1
}
save_buildlog() {
echo "Copying kpatch build log from docker container to $AUTO_LIVEPATCH_DIR"
$DOCKER cp "$DOCKER_CONTAINER_NAME":"$DOCKER_KPATCH_BUILDLOG" $AUTO_LIVEPATCH_DIR &> /dev/null || error "Couldn't find kpatch build log"
}
cleanup() {
# copy output (should just be livepatch module) from docker container
mkdir -p "$OUTPUT_DIR"
$DOCKER cp "$DOCKER_CONTAINER_NAME":$DOCKER_LIVEPATCH_DIR/. "$OUTPUT_DIR" || error
# debug flag, save files if set, otherwise let them get deleted
if [[ $EXPORT_DEBUGINFO == 1 ]]; then
[ $FAILED ] || save_buildlog
echo "Saving build debug files to $DEBUGINFO_DIR"
mkdir -p $DEBUGINFO_DIR
$DOCKER cp "$DOCKER_CONTAINER_NAME":$DOCKER_DEBUGINFO_DIR/. "$DEBUGINFO_DIR" || error "Couldn't find debug files"
else
rm -rf $PATCH_DIR
fi
if [[ $FAILED == 1 ]]; then
save_buildlog
fi
# delete old description files
rm -f "$AUTO_LIVEPATCH_DIR/description.txt"
echo "Cleaning up docker container:"
docker rm -f "$DOCKER_CONTAINER_NAME" || error "Failed to delete existing docker container: $DOCKER_CONTAINER_NAME"
}
# make sure things are cleaned up if interrupted, especially because this is a long process
trap cleanup SIGINT SIGTERM EXIT
parse_args "$@"
config_container
# run gen_livepatch.sh script
$DOCKER exec -w $DOCKER_BUILDDIR "$DOCKER_CONTAINER_NAME" gen_livepatch.sh $ARGS || FAILED=1