diff --git a/build-ubunturepo b/build-ubunturepo deleted file mode 100755 index 8a36a17e4..000000000 --- a/build-ubunturepo +++ /dev/null @@ -1,711 +0,0 @@ -#!/bin/bash -# Update GSA Ubuntu Repositories or create a local repository -# -# Author: Leonardo Tonetto (tonetto@linux.vnet.ibm.com) -# Revisor: Arif Ali (aali@ocf.co.uk) -# -# -# Getting Started: -# - Clone the xcat-core git repository under a directory named "xcat-core/src" -# - make sure reprepro is installed on the build machine -# - Run this script from the local git repository you just created. -# ./build-ubunturepo -c BUILDALL=1 - -# Usage: attr=value attr=value ... ./build-ubunturepo { -c | -d } -# PROMOTE=1 - if the attribute "PROMOTE" is specified, means an official dot release. This does not -# actually build xcat, just uploads the most recent snap build to http://xcat.org/files/xcat/ . -# If not specified, a snap build is assumed, which uploads to https://xcat.org/files/xcat/ -# PREGA=1 - use this option with PROMOTE=1 on a branch that already has a released dot release, but this -# build is a GA candidate build, not to be released yet. This will result in the tarball -# being uploaded to http://xcat.org/files/xcat/repos/apt -# (but the tarball file name will be like a released tarball, not a snap build). -# When you are ready to release this build, use PROMOTE=1 without PREGA -# BUILDALL=1 - build all rpms, whether they changed or not. Should be used for snap builds that are in -# prep for a release. -# GPGSIGN=0 - Do not sign the repo in the end of the build. The repo will be signed by default -# -# LOCAL_KEY=1 Use local keys to sign repo instead of WGET from GSA. By default use GSA. -# -# GPG_HOME= - Use the specified directory as GNUPGHOME for signing (no passphrase assumed). -# Bypasses GSA download and LOCAL_KEY. -# -# SETUP=1 Setup environment for build. By default do not setup environment. -# -# LOG= - provide an LOG file option to redirect some output into log file -# -# DEST= - provide a directory to contains the build result -# -# Running builds in parallel on one host: the build lock is scoped to the source checkout (see -# the "build-lock" block below), so two builds from DIFFERENT -# checkouts -- e.g. the devel and stable Ubuntu CD lanes -- run -# concurrently, while two builds of the SAME checkout still fail-fast -# (they build in-place and would corrupt each other). For parallel -# builds give each a separate checkout and a separate output tree -# (a distinct DEST). GPG_HOME may be SHARED between parallel builds -# -- it is used read-only for signing. -# -# For the dependency packages 1. All the xcat dependency deb packages should be uploaded to -# "pokgsa/projects/x/xcat/build/ubuntu/xcat-dep/debs/" on GSA -# 2. run ./build-ubunturepo -d -# -# 3. the built xcat-dep deb packages tarball can be found in "../../xcat-dep" -# related to the path of this script -############################ -printusage() -{ - printf "Usage: %s {-c | -d} \n" $(basename $0) >&2 - echo " -c : Build the xcat-core packages and create the repo" - echo " -d : Create the xcat-dep repo." -} -# For the purpose of getting the distribution name -if [[ ! -f /etc/lsb-release ]]; then - echo "ERROR: Could not find /etc/lsb-release, is this script executed on a Ubuntu machine?" - exit 1 -fi -. /etc/lsb-release - -export HOME=/root - - -# Process cmd line variable assignments, assigning each attr=val pair to a variable of same name -for i in $*; do - echo $i | grep '=' -q - if [ $? != 0 ];then - continue - fi - # upper case the variable name - varstring=`echo "$i"|cut -d '=' -f 1|tr '[a-z]' '[A-Z]'`=`echo "$i"|cut -d '=' -f 2` - export $varstring -done - -#Setup environment so the xcat-deps can be built on a FVT test machine -if [ "$SETUP" = "1" ];then - #Mount GSA - POKGSA="/gsa/pokgsa" - POKGSA2="/gsa/pokgsa-p2" - POKGSAIBM="pokgsa.ibm.com" - if [ ! -d $POKGSA ];then - mkdir -p $POKGSA - mount ${POKGSAIBM}:${POKGSA} ${POKGSA} - fi - if [ ! -d $POKGSA2 ];then - mkdir -p $POKGSA2 - mount ${POKGSAIBM}:${POKGSA2} ${POKGSA2} - fi - - # Verify needed packages installed - REPREPO="reprepro" - DEVSCRIPTS="devscripts" - DEBHELPER="debhelper" - QUILT="quilt" - - apt-get -y install $REPREPO $DEVSCRIPTS $DEBHELPER $QUILT - - echo "Finished setup for xcat-dep build. Rerun this script with SETUP=0 LOCAL_KEY=1 flags" - exit 1 -fi - -# Check the necessary packages before starting the build -declare -a packages=( "reprepro" "devscripts" "debhelper" "libsoap-lite-perl" "libdbi-perl" "quilt" "git") - -for package in ${packages[@]}; do - RC=`dpkg -l | grep $package >> /dev/null 2>&1; echo $?` - if [[ ${RC} != 0 ]]; then - echo "ERROR: Could not find $package, install using 'apt-get install $package' to continue" - exit 1 - fi -done - -# Supported distributions. Set DISTS="jammy noble resolute" to limit local validation builds. -dists="${DISTS:-saucy trusty utopic xenial bionic focal jammy noble resolute}" - -# GPG key used to sign the apt repo (reprepro SignWith). Defaults to the historic -# name. Override with GPG_KEY_ID= (space-free, since it is passed via -# the attr=value parser above), e.g. GPG_KEY_ID=xcat-build@xcat.org -GPG_KEY_ID="${GPG_KEY_ID:-xCAT Automatic Signing Key}" - -c_flag= # xcat-core (trunk-delvel) path -d_flag= # xcat-dep (trunk) path -r_flag= #genesis base rpm package path - -while getopts 'cdr:' OPTION -do - case $OPTION in - c) c_flag=1 - ;; - d) d_flag=1 - ;; - r) r_flag=1 - genesis_rpm_path="$OPTARG" - ;; - ?) printusage - exit 2 - ;; - esac -done -shift $(($OPTIND - 1)) - -if [ -z "$c_flag" -a -z "$d_flag" ];then - printusage - exit 2 -fi - -if [ "$c_flag" -a "$d_flag" ];then - printusage - exit 2 -fi - -if [ -z "$BUILDALL" ]; then - BUILDALL=1 -fi - -# Find where this script is located to set some build variables -old_pwd=`pwd` -cd `dirname $0` -curdir=`pwd` - -# Scope the build lock to THIS checkout. build-ubunturepo builds the packages in-place -# in its own source tree (it rewrites debian/changelog and debian/control, drops -# *.orig.tar.gz at the checkout root and runs dpkg-buildpackage inside the package -# dirs), so the resource two builds actually contend for is the checkout -- not the -# host. The historic single /var/lock/xcatbld.lock was host-global and fail-fast, so -# two builds from *different* checkouts (e.g. the devel and stable Ubuntu CD lanes on -# one build host) collided and the loser failed the pipeline even though they share -# nothing. Key the lock on the checkout path instead: builds of the SAME checkout -# still fail-fast (they would corrupt each other in-place), while builds of DISTINCT -# checkouts get distinct locks and run in parallel. The lock file stays on the local -# /var/lock (reliable flock; the checkout may live on NFS/virtiofs where flock is not) -# and the source tree is left byte-pristine. -# -# NOTE: the two marked regions below are extracted verbatim and exercised by the unit -# test xCAT-test/unit/build_ubunturepo_lock.t (which runs them with a chosen $curdir) -# -- keep the markers, and keep each region self-contained. -# BEGIN build-lock-id -lock_id_for() { printf '%s' "$1" | md5sum | cut -c1-12; } -LOCKFILE="/var/lock/xcatbld-$(lock_id_for "$curdir").lock" -# END build-lock-id -# BEGIN build-lock-acquire -exec 8>"$LOCKFILE" -if ! flock -n 8; then - echo "ERROR: Can't get lock $LOCKFILE for checkout $curdir. Another build is already using this checkout. Exiting...." - exit 1 -fi -# END build-lock-acquire - -# for the git case, query the current branch and set REL (changing master to devel if necessary) -function setbranch { - # Get the current branch name. safe.directory='*' so this still works when the - # build runs as root against a repo owned by another user (otherwise git errors - # with "dubious ownership", returns empty, and REL collapses to an unstable value). - branch=`git -c safe.directory='*' rev-parse --abbrev-ref HEAD 2>/dev/null` - if [ "$branch" = "master" ]; then - REL="devel" - elif [ "$branch" = "HEAD" ] || [ -z "$branch" ]; then - # Special handling when in a 'detached HEAD' state - branch=`git -c safe.directory='*' describe --abbrev=0 HEAD 2>/dev/null` - [[ -n "$branch" ]] && REL=`echo $branch|cut -d. -f 1,2` - else - REL=$branch - fi -} - -WGET_CMD="wget" -if [ ! -z ${LOG} ]; then - WGET_CMD="wget -o ${LOG}" -fi - -if [ "$GPGSIGN" = "0" ];then - echo "GPGSIGN=$GPGSIGN specified, skip gnupg key downloading" -elif [ -n "$GPG_HOME" ];then - echo "GPG_HOME=$GPG_HOME specified, using provided GNUPGHOME" - export GNUPGHOME="$GPG_HOME" -else - #sync the gpg key to the build machine local - gsa_url=http://pokgsa.ibm.com/projects/x/xcat/build/linux - mkdir -p $HOME/.gnupg - for key_name in pubring.gpg secring.gpg trustdb.gpg; do - if [ "$LOCAL_KEY" = "1" ];then - # Keys are already in the local $HOME/.gnupg directory - chmod 600 $HOME/.gnupg/$key_name - else - # Need to download keys from GSA - if [ ! -f $HOME/.gnupg/$key_name ] || [ `wc -c $HOME/.gnupg/$key_name|cut -f 1 -d' '` == 0 ]; then - rm -f $HOME/.gnupg/$key_name - ${WGET_CMD} -P $HOME/.gnupg $gsa_url/keys/$key_name - chmod 600 $HOME/.gnupg/$key_name - fi - fi - done -fi - -REL=xcat-core -if [ "$c_flag" ] -then - setbranch - # Sanitize REL into a stable, filesystem-safe token: replace any character that - # isn't [A-Za-z0-9._-] (e.g. the '/' in a branch like feat/ubuntu-e2e, which would - # otherwise create nested dirs) with '-', and never let it be empty. - REL=${REL//[^A-Za-z0-9._-]/-} - [ -z "$REL" ] && REL="local" - package_dir_name=debs$REL - - #define the dep source code path, core build target path and dep build target path - if [ -z "$DEST" ]; then - local_core_repo_path="$curdir/../../xcat-core" - PKGDIR="../../$package_dir_name" - else - local_core_repo_path="$DEST/$package_dir_name/xcat-core" - PKGDIR="$DEST/$package_dir_name/$package_dir_name" - fi - if [ ! -d "$PKGDIR" ];then - mkdir -p "$PKGDIR" - fi - - echo "#############################################################" - echo "Building xcat-core on branch ($REL) to $local_core_repo_path" - echo "#############################################################" - if [ "$PROMOTE" != 1 ]; then - code_change=0 - update_log='' - - if [ -z "$GITUP" ];then - update_log=../coregitup - echo "git pull > $update_log" - git pull > $update_log - else - update_log=$GITUP - fi - - if ! grep -q 'Already up-to-date' $update_log; then - code_change=1 - fi - ver=`cat Version` - short_ver=`cat Version|cut -d. -f 1,2` - short_short_ver=`cat Version|cut -d. -f 1` - commit_id_long=`git rev-parse HEAD` - commit_id="${commit_id_long:0:7}" - if [ -f Gitepoch ]; then - source_date_epoch=$(cat Gitepoch) - else - source_date_epoch=$(git log -1 --format=%ct HEAD 2>/dev/null || date +%s) - fi - export SOURCE_DATE_EPOCH="$source_date_epoch" - export DEBEMAIL="xcat-build@xcat.org" - export DEBFULLNAME="xCAT Build" - build_time=$(date -d "@$source_date_epoch" --utc '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u) - build_machine=`hostname` - - if [ $code_change == 0 -a "$UP" != 1 -a "$BUILDALL" != 1 ]; then - echo "Nothing new detected. Exiting...." - exit 0 - fi - - echo "###############################" - echo "# Building xcat-core packages #" - echo "###############################" - - #the package type: local | snap | alpha - #the build introduce string - build_string="Snap_Build" - if [ -f Release ]; then - xcat_release=$(cat Release) - else - xcat_release="snap$(date -d "@$source_date_epoch" --utc '+%Y%m%d%H%M')" - fi - pkg_version="${ver}-${xcat_release}" - - packages="xCAT-client xCAT-genesis-scripts perl-xCAT xCAT-server xCAT xCATsn xCAT-test xCAT-buildkit xCAT-vlan xCAT-confluent xCAT-probe" - if [ -n "$PACKAGE" ]; then - match="" - for p in $packages; do - p_low=$(echo "$p" | tr '[A-Z]' '[a-z]') - pkg_low=$(echo "$PACKAGE" | tr '[A-Z]' '[a-z]') - if [ "$p_low" = "$pkg_low" ]; then - match="$p" - break - fi - done - if [ -z "$match" ]; then - echo "ERROR: Package '$PACKAGE' not found. Valid packages: $packages" - exit 1 - fi - packages="$match" - fi - target_archs=(amd64 ppc64el) - for file in $packages - do - file_low=`echo $file | tr '[A-Z]' '[a-z]'` - if [ "$file" = "xCAT" -o "$file" = "xCAT-genesis-scripts" -o "$file" = "xCATsn" ]; then - target_archs="amd64 ppc64el" - else - target_archs="all" - fi - for target_arch in $target_archs - do - tar_orig="${file_low}_${ver}.orig.tar.gz" - if grep -q "3.0 (quilt)" "${file}/debian/source/format" && [ ! -f "$tar_orig" ]; then - tar czf "$tar_orig" --exclude debian -C "$file" . - fi - - if grep -q $file $update_log || [ "$BUILDALL" == 1 -o "$file" = "perl-xCAT" ]; then - rm -f $PKGDIR/${file_low}_*.$target_arch.deb - cd $file - CURDIR=$(pwd) - - find . -name '*.dch' -delete - deterministic_date=$(date -R -d "@$SOURCE_DATE_EPOCH" --utc 2>/dev/null || date -R --utc) - sed -i "1s/(.*)/(${pkg_version})/" debian/changelog - sed -i "s/^ -- .*/ -- $DEBFULLNAME <$DEBEMAIL> $deterministic_date/" debian/changelog - if [ "$target_arch" = "all" ]; then - #xcat probe use some functions shipped by xCAT, for below reasons we need to copy files to xCAT-probe directory - #1 make xcat probe code to be self-contained - #2 don't maintain two files for each script - #3 symbolic link can't work during package - if [ $file_low = "xcat-probe" ]; then - mkdir -p ${CURDIR}/lib/perl/xCAT/ - cp -f ${CURDIR}/../perl-xCAT/xCAT/CommandUtils.pm ${CURDIR}/lib/perl/xCAT/ - cp -f ${CURDIR}/../perl-xCAT/xCAT/NetworkUtils.pm ${CURDIR}/lib/perl/xCAT/ - cp -f ${CURDIR}/../perl-xCAT/xCAT/GlobalDef.pm ${CURDIR}/lib/perl/xCAT/ - cp -f ${CURDIR}/../perl-xCAT/xCAT/ServiceNodeUtils.pm ${CURDIR}/lib/perl/xCAT/ - fi - CURDIR=$(pwd) - cp ${CURDIR}/debian/control ${CURDIR}/debian/control.save.998 - # Magic string used here - sed -i -e "s#>= 2.13-snap000000000000#= ${pkg_version}#g" ${CURDIR}/debian/control - dpkg-buildpackage -rfakeroot -uc -us - mv ${CURDIR}/debian/control.save.998 ${CURDIR}/debian/control - else - if [ "$file" = "xCAT-genesis-scripts" ]; then - echo "Rename control file to build pkg: mv ${CURDIR}/debian/control-${target_arch} ${CURDIR}/debian/control" - cp ${CURDIR}/debian/control-${target_arch} ${CURDIR}/debian/control - elif [ "$file" = "xCAT" ]; then - # shipping bmcsetup and getipmi scripts as part of postscripts - files=("bmcsetup" "getipmi") - for f in "${files[@]}"; do - cp ${CURDIR}/../xCAT-genesis-scripts/usr/bin/$f ${CURDIR}/postscripts/$f - sed -i "s/xcat.genesis.$f/$f/g" ${CURDIR}/postscripts/$f - done - fi - CURDIR=$(pwd) - cp ${CURDIR}/debian/control ${CURDIR}/debian/control.save.998 - # Magic string used here - sed -i -e "s#>= 2.13-snap000000000000#= ${pkg_version}#g" ${CURDIR}/debian/control - dpkg-buildpackage -rfakeroot -uc -us -a$target_arch - mv ${CURDIR}/debian/control.save.998 ${CURDIR}/debian/control - if [ "$file" = "xCAT-genesis-scripts" ]; then - echo "Move control file back: mv ${CURDIR}/debian/control ${CURDIR}/debian/control-${target_arch}" - rm ${CURDIR}/debian/control - elif [ "$file" = "xCAT" ]; then - files=("bmcsetup" "getipmi") - for f in "${files[@]}"; do - rm -f ${CURDIR}/postscripts/$f - done - fi - fi - rc=$? - if [ $rc -gt 0 ]; then - echo "Error: $file build package failed exit code $rc" - exit $rc - fi - cd - - find $file -maxdepth 3 -type d -name "${file_low}*" | grep debian | xargs rm -rf - find $file -maxdepth 3 -type f -name "files" | grep debian | xargs rm -rf - mv ${file_low}* $PKGDIR/ - fi - done - done - - find $PKGDIR/* ! -name '*.deb' | xargs rm -f - fi - - if [ "$PROMOTE" = 1 ]; then - upload_dir="xcat-core" - tar_name="xcat-core-$ver.tar.bz2" - else - upload_dir="core-snap" - tar_name="core-debs-snap.tar.bz2" - fi - - echo "#################################" - echo "# Creating xcat-core repository #" - echo "#################################" - - #clean the repo directory - if [ -e $local_core_repo_path ]; then - rm -rf $local_core_repo_path - fi - mkdir -p $local_core_repo_path - cd $local_core_repo_path - mkdir conf - - for dist in $dists; do - # for all releases moving forward, support amd64 and ppc64el - tmp_out_arch="amd64 ppc64el" - if [ "$dist" = "saucy" ]; then - # for older releases of Ubuntu that does not support ppc64el - tmp_out_arch="amd64" - fi - cat << __EOF__ >> conf/distributions -Origin: xCAT internal repository -Label: xcat-core bazaar repository -Codename: $dist -Architectures: $tmp_out_arch -Components: main -Description: Repository automatically genereted conf -__EOF__ - - if [ "$GPGSIGN" = "0" ];then - #echo "GPGSIGN=$GPGSIGN specified, the repo will not be signed" - echo "" >> conf/distributions - else - keyid=$(gpg --list-keys --keyid-format long "$GPG_KEY_ID" | grep '^pub' | sed -e 's/.*\///' -e 's/ .*//') - echo "SignWith: $keyid" >> conf/distributions - echo "" >> conf/distributions - fi - done - - if [ -n "$GPG_HOME" ]; then - cat << __EOF__ > conf/options -verbose -basedir . -__EOF__ - else - cat << __EOF__ > conf/options -verbose -ask-passphrase -basedir . -__EOF__ - fi - - #import the deb packages into the repo - amd_files=`ls ../$package_dir_name/*.deb | grep -v "ppc64el"` - all_files=`ls ../$package_dir_name/*.deb` - for dist in $dists; do - deb_files=$all_files - if [ "$dist" = "saucy" ]; then - # for older releases of Ubuntu that does not support ppc64el - deb_files=$amd_files - fi - for file in $deb_files; do - reprepro -b ./ includedeb $dist $file; - done - done - #create the mklocalrepo script - cat << '__EOF__' > mklocalrepo.sh -. /etc/lsb-release -cd `dirname $0` -host_arch=`uname -m` -if [ "$host_arch" != "ppc64le" ];then - host_arch="amd64" -else - host_arch="ppc64el" -fi -echo deb [arch=$host_arch] file://"`pwd`" $DISTRIB_CODENAME main > /etc/apt/sources.list.d/xcat-core.list -__EOF__ - - chmod 775 mklocalrepo.sh - - # - # Add a buildinfo file into the tar.bz2 file to track information about the build - # - BUILDINFO=$local_core_repo_path/buildinfo - echo "VERSION=$ver" > $BUILDINFO - echo "RELEASE=$xcat_release" >> $BUILDINFO - echo "BUILD_TIME=$build_time" >> $BUILDINFO - echo "BUILD_MACHINE=$build_machine" >> $BUILDINFO - echo "COMMIT_ID=$commit_id" >> $BUILDINFO - echo "COMMIT_ID_LONG=$commit_id_long" >> $BUILDINFO - - #create the xcat-core.list file - - cd ../ - if ! grep xcat /etc/group ; then - groupadd xcat - fi - - chgrp -R root xcat-core - chmod -R g+w xcat-core - - #build the tar ball - echo "Creating `pwd`/$tar_name ..." - tar -hjcf $tar_name xcat-core - chgrp root $tar_name - chmod g+w $tar_name - - if [ -n "$DEST" ]; then - ln -sf $(basename `pwd`)/$tar_name ../$tar_name - if [ $? != 0 ]; then - echo "ERROR: Failed to make symbol link $DEST/$tar_name" - fi - fi - - if [ ! -e core-snap ]; then - ln -s xcat-core core-snap - fi - - cd $old_pwd - exit 0 -fi - -if [ "$d_flag" ] -then - echo "################################" - echo "# Creating xcat-dep repository #" - echo "################################" - - #the path of ubuntu xcat-dep deb packages on GSA - GSA="/gsa/pokgsa/projects/x/xcat/build/ubuntu/xcat-dep" - if [ ! -d $GSA ]; then - echo "build-ubunturepo: It appears that you do not have GSA to access the xcat-dep pkgs." - exit 1; - fi - - #define the dep source code path, core build target path and dep build target path - if [ -z "$DEST" ]; then - local_dep_repo_path="$curdir/../../xcat-dep/xcat-dep" - else - local_dep_repo_path="$DEST/xcat-dep/xcat-dep" - fi - - # Sync from the GSA master copy of the dep rpms - echo "Creating directory $local_dep_repo_path" - mkdir -p $local_dep_repo_path/ - - echo "Syncing RPMs from $GSA/ to $local_dep_repo_path/../ ..." - rsync -ilrtpu --delete $GSA/ $local_dep_repo_path/../ - if [ $? -ne 0 ]; then - echo "Error from rsync, cannot continue!" - exit 1 - fi - - #clean all old files - if [ -e $local_dep_repo_path ];then - rm -rf $local_dep_repo_path - fi - mkdir -p $local_dep_repo_path - cd $local_dep_repo_path - mkdir conf - - - #create the conf/distributions file - for dist in $dists; do - tmp_out_arch="amd64 ppc64el" - if [ "$dist" = "saucy" ]; then - # for older releases of Ubuntu that does not support ppc64el - tmp_out_arch="amd64" - fi - cat << __EOF__ >> conf/distributions -Origin: xCAT internal repository -Label: xcat-dep bazaar repository -Codename: $dist -Architectures: $tmp_out_arch -Components: main -Description: Repository automatically genereted conf -__EOF__ - - if [ "$GPGSIGN" = "0" ];then - echo "GPGSIGN=$GPGSIGN specified, the repo will not be signed" - echo "" >> conf/distributions - else - keyid=$(gpg --list-keys --keyid-format long "$GPG_KEY_ID" | grep '^pub' | sed -e 's/.*\///' -e 's/ .*//') - echo "SignWith: $keyid" >> conf/distributions - echo "" >> conf/distributions - fi - - done - - - - if [ -n "$GPG_HOME" ]; then - cat << __EOF__ > conf/options -verbose -basedir . -__EOF__ - else - cat << __EOF__ > conf/options -verbose -ask-passphrase -basedir . -__EOF__ - fi - - #import the deb packages into the repo - amd_files=`ls ../debs/*.deb | grep -v "ppc64el"` - all_files=`ls ../debs/*.deb` - for dist in $dists; do - deb_files=$all_files - if [ "$dist" = "saucy" ]; then - # for older releases of Ubuntu that does not support ppc64el - deb_files=$amd_files - fi - for file in $deb_files; do - reprepro -b ./ includedeb $dist $file; - done - done - - cat << '__EOF__' > mklocalrepo.sh -. /etc/lsb-release -cd `dirname $0` -host_arch=`uname -m` -if [ "$host_arch" != "ppc64le" ];then - host_arch="amd64" -else - host_arch="ppc64el" -fi -echo deb [arch=$host_arch] file://"`pwd`" $DISTRIB_CODENAME main > /etc/apt/sources.list.d/xcat-dep.list -__EOF__ - - chmod 775 mklocalrepo.sh - - cd .. - if ! grep xcat /etc/group ; then - groupadd xcat - fi - - chgrp -R root xcat-dep - chmod -R g+w xcat-dep - - #create the tar ball - dep_tar_name=xcat-dep-ubuntu-`date +%Y%m%d%H%M`.tar.bz2 - tar -hjcf $dep_tar_name xcat-dep - chgrp root $dep_tar_name - chmod g+w $dep_tar_name - - - USER="xcat" - SERVER="xcat.org" - FILES_PATH="files" - FRS="/var/www/${SERVER}/${FILES_PATH}" - APT_DIR="${FRS}/xcat" - APT_REPO_DIR="${APT_DIR}/repos/apt/devel" - - # Decide whether to upload the xcat-dep package or NOT (default is to NOT upload xcat-dep - if [ "$UP" != "1" ]; then - echo "Upload not specified, Done! (rerun with UP=1, to upload)" - cd $old_pwd - exit 0 - fi - - #upload the dep packages - i=0 - echo "Uploading debs from xcat-dep to ${APT_REPO_DIR}/xcat-dep/ ..." - while [ $((i+=1)) -le 5 ] && ! rsync -urLv --delete xcat-dep $USER@${SERVER}:${APT_REPO_DIR}/ - do : ; done - - #upload the tarball - i=0 - echo "Uploading $dep_tar_name to ${APT_DIR}/xcat-dep/2.x_Ubuntu/ ..." - while [ $((i+=1)) -le 5 ] && ! rsync -v --force $dep_tar_name $USER@${SERVER}:${APT_DIR}/xcat-dep/2.x_Ubuntu/ - do : ; done - - #upload the README file - cd debs - i=0 - echo "Uploading README to ${APT_DIR}/xcat-dep/2.x_Ubuntu/ ..." - while [ $((i+=1)) -le 5 ] && ! rsync -v --force README $USER@${SERVER}:${APT_DIR}/xcat-dep/2.x_Ubuntu/ - do : ; done - -fi - -cd $old_pwd -exit 0 diff --git a/builddebs.pl b/builddebs.pl index abf016468..def0fd89b 100755 --- a/builddebs.pl +++ b/builddebs.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # Build the xcat-core Debian packages and assemble a signed apt repository. # -# Replaces build-ubunturepo. The shape mirrors buildrpms.pl -- Getopt::Long options, +# Builds every xCAT deb and the apt repository. The shape mirrors buildrpms.pl -- Getopt::Long options, # one package list, build then index then sign -- so the two builders read the same way # and share XCAT::BuildUtils. # @@ -388,7 +388,7 @@ carry an architecture, and there the difference is packaging metadata rather tha compiled output. Consequently this builder needs no C and no per-codename chroot. (xcat-dep is different: its packages are compiled, so it builds per codename.) -Replaces C. The GSA upload paths, the C/C release +Replaced C, removed in 2.19. The GSA upload paths, the C/C release flows and the C<-d> xcat-dep repository mode were not carried over: publishing is done by the CD pipeline's own deploy step, and xcat-dep is built from its own repository. diff --git a/buildrpms.pl b/buildrpms.pl index 1c8b86990..e14bdd436 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -346,6 +346,9 @@ sub buildsources_genesis_base($) { "Error copying dracut_105 sources"); cp "xCAT-genesis-builder/80-net-name-slot.rules", "$staging_root/80-net-name-slot.rules"; + # %install runs this against the extracted payload before it becomes an rpm. + cp "xCAT-genesis-builder/verify-genesis-payload", + "$staging_root/verify-genesis-payload"; unlink $support_tarball if -f $support_tarball; sh_or_die(qq(tar --sort=name --owner=0 --group=0 --mtime="\@$SOURCE_DATE_EPOCH" -cjf "$support_tarball" -C "$staging_parent" xCAT-genesis-base-build-support), diff --git a/docs/source/developers/guides/code/builds.rst b/docs/source/developers/guides/code/builds.rst index 1146e4225..021428aae 100644 --- a/docs/source/developers/guides/code/builds.rst +++ b/docs/source/developers/guides/code/builds.rst @@ -36,10 +36,8 @@ emitted, because a source-only run has no binary packages to advertise. ``buildrpms.pl`` replaces all three, and its ``--source-only`` replaces the old ``SRCONLY=1``. - ``build-ubunturepo`` is superseded by ``builddebs.pl`` but is **still in the - tree for now**, as a differential oracle: it is the reference the new builder - is checked against, and it is removed once the CD pipelines have been moved - over. Do not add features to it. + ``build-ubunturepo`` was removed in 2.19. ``builddebs.pl`` replaces it, and the + CD pipelines build every Ubuntu target with it. Debian and Ubuntu packages -------------------------- diff --git a/docs/source/guides/install-guides/maintenance/uninstall_xcat.rst b/docs/source/guides/install-guides/maintenance/uninstall_xcat.rst index b31b1a79d..c5aa4f497 100644 --- a/docs/source/guides/install-guides/maintenance/uninstall_xcat.rst +++ b/docs/source/guides/install-guides/maintenance/uninstall_xcat.rst @@ -64,7 +64,7 @@ Remove xCAT Files [Ubuntu] :: - apt-get remove conserver-xcat elilo-xcat goconserver grub2-xcat ipmitool-xcat perl-xcat syslinux-xcat xcat xcat-buildkit xcat-client xcat-confluent xcat-genesis-base-amd64 xcat-genesis-base-ppc64 xcat-genesis-scripts-amd64 xcat-genesis-scripts-ppc64 xcat-probe xcat-server xcat-test xcat-vlan xcatsn xnba-undi + apt-get remove conserver-xcat elilo-xcat goconserver grub2-xcat ipmitool-xcat perl-xcat syslinux-xcat xcat xcat-buildkit xcat-client xcat-confluent xcat-genesis-base-amd64 xcat-genesis-base-ppc64el xcat-genesis-scripts-amd64 xcat-genesis-scripts-ppc64el xcat-probe xcat-server xcat-test xcat-vlan xcatsn xnba-undi To do an even more thorough cleanup, use links below to get a list of RPMs installed by xCAT. Some RPMs may not to be installed in a specific environment. diff --git a/github_action_xcat_test.pl b/github_action_xcat_test.pl index 4a47d4c7d..34fc6b675 100644 --- a/github_action_xcat_test.pl +++ b/github_action_xcat_test.pl @@ -32,13 +32,9 @@ my $GITHUB_API = "https://api.github.com"; # through FindBin, so they can only be run from a source tree. Take a copy # before building and run the unit tests out of the copy. # -# This used to be mandatory rather than tidy: build-ubunturepo set -# local_core_repo_path="$curdir/../../xcat-core" -# which, under the work// layout GitHub checks out into, resolved to -# the checkout's own parent, and it rm -rf'd that path to make room for the apt -# repository -- destroying the tree the tests need. builddebs.pl writes under -# dist/debs INSIDE the checkout and restores every file it edits, so the copy is -# now only isolating the tests from build residue. +# The copy is tidiness, not a requirement: builddebs.pl writes under dist/debs +# inside the checkout and restores every file it edits, so the copy only keeps +# build residue away from the tests. my $srcdir = getcwd(); my $unitsrc = ($ENV{'RUNNER_TEMP'} ? $ENV{'RUNNER_TEMP'} : "/tmp") . "/xcat-core-unitsrc"; diff --git a/xCAT-genesis-builder/builddeb-genesis-base b/xCAT-genesis-builder/builddeb-genesis-base index 0195f672d..5b9bc9141 100755 --- a/xCAT-genesis-builder/builddeb-genesis-base +++ b/xCAT-genesis-builder/builddeb-genesis-base @@ -15,6 +15,20 @@ case "$BUILDARCH" in *) echo "ERROR: unsupported architecture: $BUILDARCH" >&2; exit 1 ;; esac +# The Genesis debs carry the architecture in the package name, so the packages an upgrade has +# to displace carry it too. 2.19 renames the ppc64 debs to ppc64el; dpkg keeps the old package, +# and its copy of the same files, unless the new one replaces it by name. +rewrite_control() { + local control=$1 arch=$2 superseded + case "$arch" in + ppc64el) superseded="xcat-genesis-ppc64, xcat-genesis-base-ppc64" ;; + *) superseded="xcat-genesis-$arch" ;; + esac + sed -i -e "s/xcat-genesis-base-amd64/xcat-genesis-base-$arch/g" \ + -e "s/xcat-genesis-scripts-amd64/xcat-genesis-scripts-$arch/g" \ + -e "s/xcat-genesis-amd64/$superseded/g" "$control" +} + VERSION=$(cat "$DIR/../Version" 2>/dev/null || echo "2.18.0") RELEASE=$(cat "$DIR/../Release" 2>/dev/null || echo "snap$(date +%Y%m%d%H%M)") CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME") @@ -163,8 +177,7 @@ rm -rf "$DIR/opt" cp -a "$GENESIS_TMPDIR/opt" "$DIR/" # Adjust control file for target arch -sed -i "s/xcat-genesis-base-amd64/xcat-genesis-base-$BUILDARCH/g" "$DIR/debian/control" -sed -i "s/xcat-genesis-scripts-amd64/xcat-genesis-scripts-$BUILDARCH/g" "$DIR/debian/control" +rewrite_control "$DIR/debian/control" "$BUILDARCH" PKG_VERSION="${VERSION}-${RELEASE}~${CODENAME}" rm -f "$DIR/debian/changelog" diff --git a/xCAT-genesis-builder/debuild-xcat-genesis-base b/xCAT-genesis-builder/debuild-xcat-genesis-base index 39ed5d4e8..ffa0dc2d9 100755 --- a/xCAT-genesis-builder/debuild-xcat-genesis-base +++ b/xCAT-genesis-builder/debuild-xcat-genesis-base @@ -30,17 +30,40 @@ alien -d -g -c -k "${RPM_PACKAGE}" || exit 1 PACKAGE_ARCH="${EXTRACT_DIR%-*}" PACKAGE_ARCH="${PACKAGE_ARCH##*-}" -if [[ ${EXTRACT_DIR} =~ -x86_64- ]] -then - rm -rf "${EXTRACT_DIR//x86_64/amd64}" - mv "${EXTRACT_DIR}" "${EXTRACT_DIR//x86_64/amd64}" - EXTRACT_DIR="${EXTRACT_DIR//x86_64/amd64}" +# The rpm carries the Genesis target architecture, the deb must carry the Debian architecture. +# alien copies the rpm name into the deb and writes "_" as "-", so x86_64 arrives as x86-64. +case "${PACKAGE_ARCH}" in +x86_64) + ALIEN_ARCH="x86-64" ; DEB_ARCH="amd64" ;; +ppc64|ppc64le) + ALIEN_ARCH="${PACKAGE_ARCH}" ; DEB_ARCH="ppc64el" ;; +*) + ALIEN_ARCH="${PACKAGE_ARCH}" ; DEB_ARCH="${PACKAGE_ARCH}" ;; +esac - sed -i -e 's/x86-64/amd64/g' "${EXTRACT_DIR}/debian/control" - sed -i -e 's/x86-64/amd64/g' "${EXTRACT_DIR}/debian/changelog" +if [[ "${DEB_ARCH}" != "${PACKAGE_ARCH}" ]] +then + rm -rf "${EXTRACT_DIR//${PACKAGE_ARCH}/${DEB_ARCH}}" + mv "${EXTRACT_DIR}" "${EXTRACT_DIR//${PACKAGE_ARCH}/${DEB_ARCH}}" + EXTRACT_DIR="${EXTRACT_DIR//${PACKAGE_ARCH}/${DEB_ARCH}}" + + sed -i -e "s/${ALIEN_ARCH}/${DEB_ARCH}/g" "${EXTRACT_DIR}/debian/control" + sed -i -e "s/${ALIEN_ARCH}/${DEB_ARCH}/g" "${EXTRACT_DIR}/debian/changelog" fi -sed -i -e "/^Description:/i Breaks: xcat-genesis-scripts-${PACKAGE_ARCH//x86_64/amd64} (<< 2.13.10)" "${EXTRACT_DIR}/debian/control" +# The Genesis debs carry the architecture in the package name, so the packages an upgrade has +# to displace carry it too. 2.19 renames the ppc64 debs to ppc64el; dpkg keeps the old package, +# and its copy of the same files, unless the new one replaces it by name. +case "${DEB_ARCH}" in +ppc64el) + SUPERSEDED="xcat-genesis-ppc64, xcat-genesis-base-ppc64" ;; +*) + SUPERSEDED="xcat-genesis-${DEB_ARCH}" ;; +esac + +sed -i -e "/^Description:/i Replaces: ${SUPERSEDED}" \ + -e "/^Description:/i Breaks: ${SUPERSEDED}, xcat-genesis-scripts-${DEB_ARCH} (<< 2.13.10)" \ + "${EXTRACT_DIR}/debian/control" cat >"${EXTRACT_DIR}/debian/preinst" </dev/null 2>&1; then + dracut_install dhclient + elif command -v dhcpcd >/dev/null 2>&1; then + dracut_install dhcpcd + # dhcpcd runs these on every lease. They write resolv.conf, the hostname and + # ntp.conf, which is the work dhclient-script does for the ISC client. + dracut_install /usr/libexec/dhcpcd-run-hooks + for _dhcpcd_hook in /usr/libexec/dhcpcd-hooks/*; do + _dracut_install_opt "$_dhcpcd_hook" + done + _dracut_install_opt /etc/dhcpcd.conf + fi + + # OpenSSH 9.8 moved the per-connection work into sshd-session, which sshd execs by + # absolute path. + for _sshd_helper in \ + /usr/libexec/openssh/sshd-session \ + /usr/libexec/openssh/sshd-auth \ + /usr/lib/openssh/sshd-session \ + /usr/lib/openssh/sshd-auth + do + _dracut_install_opt "$_sshd_helper" + done + + # tmux exits under the C locale, and the image carries no locale data of its own. + for _lc_file in /usr/lib/locale/C.utf8/LC_*; do + _dracut_install_opt "$_lc_file" + done dracut_install /lib64/libnss_dns.so.2 dracut_install poweroff hwclock date /usr/share/terminfo/x/xterm /usr/share/terminfo/s/screen /etc/nsswitch.conf /etc/services dracut_install /sbin/rsyslogd /etc/protocols umount /bin/rpm /usr/lib/rpm/rpmrc #dracut_install chmod /sbin/route /sbin/ifconfig /usr/bin/whoami /usr/bin/head /usr/bin/tail basename /etc/redhat-release ping tr lsusb /usr/share/hwdata/usb.ids #ibm fw wrapper requirements dracut_install chmod ip /usr/bin/whoami /usr/bin/head /usr/bin/tail basename /etc/redhat-release ping tr lsusb /usr/share/hwdata/usb.ids #ibm fw wrapper requirements - dracut_install efibootmgr dmidecode #uxspi prereqs, but will use dmidecode to improve decision on loading ipmi_si + # uxspi prereqs. dmidecode also improves the decision on loading ipmi_si. Neither is + # packaged for ppc64le, so install whichever the build root carries. + for _fw_tool in efibootmgr dmidecode; do + command -v "$_fw_tool" >/dev/null 2>&1 && dracut_install "$_fw_tool" + done dracut_install lldptool dracut_install /usr/share/zoneinfo/posix/Zulu dracut_install /usr/share/zoneinfo/posix/GMT-0 diff --git a/xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh b/xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh index d0e6f053f..8039ee149 100755 --- a/xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh +++ b/xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh @@ -2,13 +2,29 @@ root=1 rootok=1 netroot=xcat + +# The image ships the C.UTF-8 locale only. tmux refuses to start under the C locale. +export LC_ALL=C.UTF-8 + +# tmux exits when the image carries no UTF-8 locale. doxcat is the whole of Genesis, so it +# must run whether or not the multiplexer starts. Prints tmux or direct. +xcat_console_mode() { + if tmux -f /dev/null new-session -d -s xcatprobe true >/dev/null 2>&1; then + tmux kill-session -t xcatprobe >/dev/null 2>&1 + echo tmux + else + echo direct + fi +} clear echo PS1="'"'[xCAT Genesis running on \H \w]\$ '"'" > /.bashrc echo PS1="'"'[xCAT Genesis running on \H \w]\$ '"'" > /.bash_profile mkdir -p /etc/ssh mkdir -p /var/tmp/ mkdir -p /var/empty/sshd -sed -i '/^root:x/d' /etc/passwd +# dracut writes this entry itself, with an empty password field unless the image is +# built --hostonly. Match the user name only. +sed -i '/^root:/d' /etc/passwd cat >>/etc/passwd <<"__ENDL" root:x:0:0::/:/bin/bash sshd:x:30:30:SSH User:/var/empty/sshd:/sbin/nologin @@ -39,10 +55,13 @@ mkdir -p /var/lib/dhclient/ mkdir -p /var/log ip link set lo up echo '127.0.0.1 localhost' >> /etc/hosts -if grep -q console=ttyS /proc/cmdline; then +XCAT_CONSOLE_MODE="$(xcat_console_mode)" +if [ "$XCAT_CONSOLE_MODE" = "tmux" ]; then + if grep -q console=ttyS /proc/cmdline; then while :; do sleep 1; tmux attach-session -t doxcat /dev/tty1; clear &>/dev/tty1 ; done & + fi + while :; do tmux new-session < /dev/tty2 &> /dev/tty2 ; done & fi -while :; do tmux new-session < /dev/tty2 &> /dev/tty2 ; done & # The section below is just for System P LE hardware discovery @@ -87,4 +106,8 @@ elif [[ ${ARCH} =~ x86_64 ]]; then done fi -while :; do tmux attach-session -t doxcat || tmux new-session -s doxcat doxcat; done +if [ "$XCAT_CONSOLE_MODE" = "tmux" ]; then + while :; do tmux attach-session -t doxcat || tmux new-session -s doxcat doxcat; done +else + while :; do doxcat; sleep 5; done +fi diff --git a/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh b/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh index ba5325247..367439ce4 100755 --- a/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh +++ b/xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh @@ -53,7 +53,25 @@ install() { dracut_install mount.nfs sshd vi reboot lspci parted screen mkfs mkfs.ext4 mkfs.btrfs #dracut_install libvirtd /usr/share/libvirt/cpu_map.xml /usr/bin/qemu-img /usr/libexec/qemu-kvm dracut_install mkswap df ifenslave ssh-keygen scp clear + # getdestiny makes its request file with mktemp. + dracut_install mktemp dracut_install dhclient lldpad + + # OpenSSH 9.8 moved the per-connection work into sshd-session, which sshd execs by + # absolute path. + for _sshd_helper in \ + /usr/libexec/openssh/sshd-session \ + /usr/libexec/openssh/sshd-auth \ + /usr/lib/openssh/sshd-session \ + /usr/lib/openssh/sshd-auth + do + _dracut_install_opt "$_sshd_helper" + done + + # tmux exits under the C locale, and the image carries no locale data of its own. + for _lc_file in /usr/lib/locale/C.utf8/LC_*; do + _dracut_install_opt "$_lc_file" + done _dracut_install_opt "/lib/$TRIPLET/libnss_dns.so.2" dracut_install poweroff hwclock date /usr/share/terminfo/x/xterm /usr/share/terminfo/s/screen /etc/nsswitch.conf /etc/services dracut_install /usr/sbin/rsyslogd /etc/protocols umount /usr/bin/dpkg diff --git a/xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh b/xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh index b6e3a0ce5..c54ec1df3 100755 --- a/xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh +++ b/xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh @@ -2,13 +2,29 @@ root=1 rootok=1 netroot=xcat + +# The image ships the C.UTF-8 locale only. tmux refuses to start under the C locale. +export LC_ALL=C.UTF-8 + +# screen exits when the image carries no usable terminal. doxcat is the whole of Genesis, so +# it must run whether or not the multiplexer starts. Prints screen or direct. +xcat_console_mode() { + if screen -ln -d -m -S xcatprobe true >/dev/null 2>&1; then + screen -S xcatprobe -X quit >/dev/null 2>&1 + echo screen + else + echo direct + fi +} clear echo PS1="'"'[xCAT Genesis running on \H \w]\$ '"'" > /.bashrc echo PS1="'"'[xCAT Genesis running on \H \w]\$ '"'" > /.bash_profile mkdir -p /etc/ssh mkdir -p /var/tmp/ mkdir -p /var/empty/sshd -sed -i '/^root:x/d' /etc/passwd +# dracut writes this entry itself, with an empty password field unless the image is +# built --hostonly. Match the user name only. +sed -i '/^root:/d' /etc/passwd cat >>/etc/passwd <<"__ENDL" root:x:0:0::/:/bin/bash sshd:x:30:30:SSH User:/var/empty/sshd:/sbin/nologin @@ -39,10 +55,13 @@ mkdir -p /var/lib/dhclient/ mkdir -p /var/log ip link set lo up echo '127.0.0.1 localhost' >> /etc/hosts -if grep -q console=ttyS /proc/cmdline; then +XCAT_CONSOLE_MODE="$(xcat_console_mode)" +if [ "$XCAT_CONSOLE_MODE" = "screen" ]; then + if grep -q console=ttyS /proc/cmdline; then while :; do sleep 1; screen -S console -ln screen -x doxcat /dev/tty1; clear &>/dev/tty1 ; done & + fi + while :; do screen -ln < /dev/tty2 &> /dev/tty2 ; done & fi -while :; do screen -ln < /dev/tty2 &> /dev/tty2 ; done & # The section below is just for System P LE hardware discovery @@ -87,4 +106,8 @@ elif [[ ${ARCH} =~ x86_64 ]]; then done fi -while :; do screen -dr doxcat || screen -S doxcat -L -ln doxcat; done +if [ "$XCAT_CONSOLE_MODE" = "screen" ]; then + while :; do screen -dr doxcat || screen -S doxcat -L -ln doxcat; done +else + while :; do doxcat; sleep 5; done +fi diff --git a/xCAT-genesis-builder/verify-genesis-payload b/xCAT-genesis-builder/verify-genesis-payload new file mode 100755 index 000000000..1f44898c1 --- /dev/null +++ b/xCAT-genesis-builder/verify-genesis-payload @@ -0,0 +1,120 @@ +#!/bin/bash +# +# verify-genesis-payload [--commands-from ] [required-path ...] +# +# dracut_install() reports a missing binary and returns, so the module install function keeps +# going and the image ships without it. Check the extracted payload before it is packaged. +# +# Paths given on the command line are relative to . --commands-from reads back +# what the dracut module installs: a bare command name is looked for in the four binary +# directories, an absolute path under itself. The caller adds what only it +# knows (the DHCP client is not the same package on every release); the rules below come from +# the payload itself. + +set -u + +commands_from="" +while [ $# -gt 0 ]; do + case "$1" in + --commands-from) + commands_from=${2:-} + shift 2 || true + ;; + --commands-from=*) + commands_from=${1#*=} + shift + ;; + *) + break + ;; + esac +done + +payload=${1:-} +if [ -z "$payload" ] || [ ! -d "$payload" ]; then + echo "verify-genesis-payload: not a payload directory: ${payload:-}" >&2 + exit 2 +fi +shift + +missing="" + +# have PATH: true when the payload carries PATH as a file, following the usr-merge symlinks +# the image ships (/sbin -> usr/sbin). +have() { + [ -e "$payload/$1" ] +} + +require() { + local path=$1 why=$2 + have "$path" || missing="$missing + $path ($why)" +} + +for path in "$@"; do + require "$path" "required by the build" +done + +# The dracut module names every command and every data file Genesis needs. A name the build +# root does not supply installs nothing and says nothing, so read the names back and check +# each one. Names under a condition are release-dependent, so only the top level of install() +# counts. +if [ -n "$commands_from" ]; then + if [ ! -r "$commands_from" ]; then + echo "verify-genesis-payload: cannot read $commands_from" >&2 + exit 2 + fi + commands=$(awk ' + /^install\(\)/ { in_install = 1; next } + in_install && /^}/ { in_install = 0 } + in_install && /^ dracut_install / { + sub(/#.*/, "") + sub(/^ dracut_install /, "") + print + }' "$commands_from" | tr ' \t' '\n\n' | grep -v '^$' | grep -v '^-' | sort -u) + if [ -z "$commands" ]; then + echo "verify-genesis-payload: no command name read from $commands_from" >&2 + exit 2 + fi + for want in $commands; do + case "$want" in + # dracut_install installs an absolute path at that same path, so read it back + # under the payload root. Dropping these let an image with no /usr/bin/awk pass. + /*) have "${want#/}" || missing="$missing + $want (installed by $commands_from)" + ;; + *) have "bin/$want" || have "sbin/$want" \ + || have "usr/bin/$want" || have "usr/sbin/$want" \ + || missing="$missing + $want (installed by $commands_from)" + ;; + esac + done +fi + +require usr/sbin/sshd "Genesis is reached over ssh" +require usr/bin/mktemp "getdestiny makes its request file with it" + +# OpenSSH 9.8 split the per-connection work into sshd-session, which sshd execs by absolute +# path. EL9 carries OpenSSH 9.9, so an image with sshd alone refuses every connection. +if have usr/sbin/sshd && grep -qa 'sshd-session' "$payload/usr/sbin/sshd" 2>/dev/null; then + if ! have usr/libexec/openssh/sshd-session && ! have usr/lib/openssh/sshd-session; then + missing="$missing + usr/libexec/openssh/sshd-session (this sshd execs it for every connection)" + fi +fi + +# tmux exits under the C locale. The hook falls back to running doxcat directly, so this is +# not fatal to booting, but a Genesis shell without tmux loses the console attach. +if have usr/bin/tmux && ! have usr/lib/locale/C.utf8/LC_CTYPE; then + missing="$missing + usr/lib/locale/C.utf8/LC_CTYPE (tmux refuses to start without a UTF-8 locale)" +fi + +if [ -n "$missing" ]; then + echo "verify-genesis-payload: $payload is incomplete:$missing" >&2 + exit 1 +fi + +echo "verify-genesis-payload: $payload is complete" +exit 0 diff --git a/xCAT-genesis-builder/xCAT-genesis-base.spec b/xCAT-genesis-builder/xCAT-genesis-base.spec index 1a6f0f212..7fa11dcce 100644 --- a/xCAT-genesis-builder/xCAT-genesis-base.spec +++ b/xCAT-genesis-builder/xCAT-genesis-base.spec @@ -12,6 +12,14 @@ Release: %{?release:%{release}}%{!?release:%(cat Release)} %ifarch aarch64 %define tarch aarch64 %endif +%ifarch riscv64 +%define tarch riscv64 +%endif +# An arch missing from the ladder above leaves %{tarch} unexpanded, and rpm then builds a package +# with a macro in its NAME instead of failing. Stop the build here instead. +%if ! %{defined tarch} +%{error:no genesis tarch for %{_target_cpu} -- add an %%ifarch branch above} +%endif BuildArch: noarch %define name xCAT-genesis-base-%{tarch} %define __spec_install_post : @@ -38,13 +46,22 @@ BuildRequires: chrony BuildRequires: cpio BuildRequires: e2fsprogs BuildRequires: hostname -%if "%{_target_cpu}" == "x86_64" +%if "%{tarch}" == "x86_64" BuildRequires: dmidecode BuildRequires: efibootmgr %endif BuildRequires: dosfstools BuildRequires: dracut BuildRequires: dracut-network +# doxcat chooses its DHCP client at run time. RHEL 10 packages no ISC dhcp-client; its +# baseos packages dhcpcd, which carries its own resolv.conf, hostname and ntp hooks and so +# needs no dhclient-script. +%if 0%{?rhel} && 0%{?rhel} < 10 +BuildRequires: dhcp-client +%endif +%if 0%{?rhel} >= 10 +BuildRequires: dhcpcd +%endif BuildRequires: ethtool BuildRequires: gawk BuildRequires: ipmitool @@ -62,6 +79,9 @@ BuildRequires: nfs-utils BuildRequires: nmap-ncat BuildRequires: openssh-clients BuildRequires: openssh-server +# getcert, getdestiny, getipmi and getadapter run the openssl command. el8 and el9 hold it in +# the build root as a dependency of another package; el10 does not. +BuildRequires: openssl BuildRequires: parted BuildRequires: pciutils BuildRequires: perl @@ -117,9 +137,6 @@ rm -rf "$DRACUTMODDIR" mkdir -p "$DRACUTMODDIR" cp -a "%{_builddir}/xCAT-genesis-base-build-support/dracut_105/el/." "$DRACUTMODDIR/" chmod 0755 "$DRACUTMODDIR/module-setup.sh" "$DRACUTMODDIR/xcatroot" "$DRACUTMODDIR/dhclient-script" -if [ "%{_target_cpu}" != "x86_64" ]; then - sed -i '/efibootmgr dmidecode/d' "$DRACUTMODDIR/module-setup.sh" -fi KERNELVERSION=$(ls -1 /lib/modules | sort -V | tail -n 1) test -n "$KERNELVERSION" @@ -216,6 +233,19 @@ test -n "$KERNEL_IMAGE" test -e "$KERNEL_IMAGE" cp "$KERNEL_IMAGE" "$GENESIS_ROOT/kernel" +# dracut_install reports a missing binary and returns, so a hole in the image reaches the +# rpm silently. Three of them did. +GENESIS_REQUIRED="" +%if 0%{?rhel} && 0%{?rhel} < 10 +GENESIS_REQUIRED="usr/sbin/dhclient" +%endif +%if 0%{?rhel} >= 10 +GENESIS_REQUIRED="usr/sbin/dhcpcd" +%endif +bash "%{_builddir}/xCAT-genesis-base-build-support/verify-genesis-payload" \ + --commands-from "$DRACUTMODDIR/module-setup.sh" \ + "$GENESIS_FS" $GENESIS_REQUIRED + find "$GENESIS_TMPDIR" -type c -delete cp -a "$GENESIS_TMPDIR/%{prefix}/." "$RPM_BUILD_ROOT/%{prefix}/" diff --git a/xCAT-genesis-builder/xcat-cmdline.sh b/xCAT-genesis-builder/xcat-cmdline.sh index 525ea19c9..3d0de2382 100755 --- a/xCAT-genesis-builder/xcat-cmdline.sh +++ b/xCAT-genesis-builder/xcat-cmdline.sh @@ -8,7 +8,9 @@ echo PS1="'"'[xCAT Genesis running on \H \w]\$ '"'" > /.bash_profile mkdir -p /etc/ssh mkdir -p /var/tmp/ mkdir -p /var/empty/sshd -sed -i '/^root:x/d' /etc/passwd +# dracut writes this entry itself, with an empty password field unless the image is +# built --hostonly. Match the user name only. +sed -i '/^root:/d' /etc/passwd cat >>/etc/passwd <<"__ENDL" root:x:0:0::/:/bin/bash sshd:x:30:30:SSH User:/var/empty/sshd:/sbin/nologin diff --git a/xCAT-genesis-scripts/debian/control-ppc64el b/xCAT-genesis-scripts/debian/control-ppc64el index 53200fc05..75853f231 100644 --- a/xCAT-genesis-scripts/debian/control-ppc64el +++ b/xCAT-genesis-scripts/debian/control-ppc64el @@ -5,11 +5,11 @@ Maintainer: xCAT Build-Depends: debhelper (>= 9) Standards-Version: 3.9.4 -Package: xcat-genesis-scripts-ppc64 +Package: xcat-genesis-scripts-ppc64el Architecture: all -Depends: xcat-genesis-base-ppc64 (>= 2.13.10) -Conflicts: xcat-genesis-scripts -Replaces: xcat-genesis-scripts +Depends: xcat-genesis-base-ppc64el (>= 2.13.10) +Conflicts: xcat-genesis-scripts, xcat-genesis-scripts-ppc64 +Replaces: xcat-genesis-scripts, xcat-genesis-scripts-ppc64 Description: xCAT genesis (Genesis Enhanced Netboot Environment for System Information and Servicing) is a small, embedded-like environment for xCAT's use in discovery and diff --git a/xCAT-genesis-scripts/usr/bin/doxcat b/xCAT-genesis-scripts/usr/bin/doxcat index 53e83fea3..d0ce643b3 100755 --- a/xCAT-genesis-scripts/usr/bin/doxcat +++ b/xCAT-genesis-scripts/usr/bin/doxcat @@ -205,6 +205,47 @@ secondary_nic_needs_dhcp() { return 0 } +# RHEL 10 packages no ISC dhcp-client, so the image carries whichever client its release +# ships. Print the command line for one interface and one address family, or nothing when +# the image carries no client at all. +genesis_dhcp_command() { + local family=$1 + local nic=$2 + + if command -v dhclient >/dev/null 2>&1; then + if [ "$family" = 6 ]; then + echo "dhclient -6 -pf /var/run/dhclient6.$nic.pid $nic -lf /var/lib/dhclient/dhclient6.leases" + else + echo "dhclient -cf /etc/dhclient.conf -pf /var/run/dhclient.$nic.pid $nic" + fi + return 0 + fi + + if command -v dhcpcd >/dev/null 2>&1; then + # dhcpcd carries its own resolv.conf, hostname and ntp hooks, so it does not need + # dhclient-script. On a single interface it exits when its timeout expires, and it + # de-configures the interface as it goes; -t 0 and -p turn both off. + echo "dhcpcd -$family -b -p -t 0 $nic" + return 0 + fi + + return 0 +} + +# Run the client genesis_dhcp_command chose. The caller backgrounds this. +genesis_start_dhcp() { + local family=$1 + local nic=$2 + local command + + command=$(genesis_dhcp_command "$family" "$nic") + if [ -z "$command" ]; then + logger -s -t $log_label -p local4.err "The image carries no DHCP client, so $nic gets no IPv$family address." + return 1 + fi + $command +} + # see if they specified static ip info, otherwise use dhcp XCATPORT=3001 for parm in `cat /proc/cmdline`; do @@ -253,8 +294,8 @@ else while [ $tries -lt 100 ]; do ALLUP_NICS=`ip link show | grep -v "^ " | grep "state UP" | awk '{print $2}' | sed -e 's/:$//'|grep -v lo | sort -n -r` for tmp1 in $ALLUP_NICS; do - dhclient -cf /etc/dhclient.conf -pf /var/run/dhclient.$tmp1.pid $tmp1 & - dhclient -6 -pf /var/run/dhclient6.$tmp1.pid $tmp1 -lf /var/lib/dhclient/dhclient6.leases & + genesis_start_dhcp 4 "$tmp1" & + genesis_start_dhcp 6 "$tmp1" & #bootnic=$tmp1 #break done @@ -290,11 +331,11 @@ else /bin/bash fi else - dhclient -cf /etc/dhclient.conf -pf /var/run/dhclient.$bootnic.pid $bootnic & + genesis_start_dhcp 4 "$bootnic" & #we'll kick of IPv6 and IPv4 on all nics, but not wait for them to come up unless doing discovery, to reduce #chances that we'll perform a partial discovery #in other scenarios where downed non-bootnics cause issues, will rely on retries to fix things up - dhclient -6 -pf /var/run/dhclient6.$bootnic.pid $bootnic -lf /var/lib/dhclient/dhclient6.leases & + genesis_start_dhcp 6 "$bootnic" & NICCANDIDATES=`ip link|grep mtu|grep -v LOOPBACK|grep -v $bootnic|grep -v usb|awk -F: '{print $2}'` TSMNIC=$(cat /tmp/tsmhostnic 2>/dev/null) NICSTOBRINGUP= @@ -305,8 +346,8 @@ else done export NICSTOBRINGUP for nic in $NICSTOBRINGUP; do - (while ! ethtool $nic | grep Link\ detected|grep yes > /dev/null && [ ! -f /tmp/netinitted ]; do sleep 5; done; dhclient -cf /etc/dhclient.conf -pf /var/run/dhclient.$nic.pid $nic ) & - (while ! ethtool $nic | grep Link\ detected|grep yes > /dev/null && [ ! -f /tmp/netinitted ]; do sleep 5; done; dhclient -cf /etc/dhclient.conf -6 -pf /var/run/dhclient6.$nic.pid -lf /var/lib/dhclient/dhclient6.leases $nic ) & + (while ! ethtool $nic | grep Link\ detected|grep yes > /dev/null && [ ! -f /tmp/netinitted ]; do sleep 5; done; genesis_start_dhcp 4 "$nic" ) & + (while ! ethtool $nic | grep Link\ detected|grep yes > /dev/null && [ ! -f /tmp/netinitted ]; do sleep 5; done; genesis_start_dhcp 6 "$nic" ) & done gripeiter=101 diff --git a/xCAT-genesis-scripts/usr/bin/getcert b/xCAT-genesis-scripts/usr/bin/getcert index cf4cf1b07..7bad27285 100755 --- a/xCAT-genesis-scripts/usr/bin/getcert +++ b/xCAT-genesis-scripts/usr/bin/getcert @@ -4,8 +4,27 @@ CREDPID=$! if [ -z "$XCATDEST" ]; then XCATDEST=$1 fi +# doxcat runs getcert in the foreground and ignores its status, so a wait with no bound stops +# the boot and prints nothing. +give_up() { + logger -s -t xcat -p local4.err "getcert: $1" + kill $CREDPID + exit 1 +} + +if ! command -v openssl > /dev/null 2>&1; then + give_up "this Genesis image carries no openssl, so no certificate is requested" +fi + #retry in case certkey.pem is not right, yet +# doxcat writes /etc/xcat/certkey.pem in the background with a 4096 bit key, so the first +# requests fail. An emulated node needs minutes for that key. +CSR_TIMEOUT=${GETCERT_CSR_TIMEOUT:-600} +CSR_DEADLINE=$((SECONDS + CSR_TIMEOUT)) while ! openssl req -new -key /etc/xcat/certkey.pem -out /tmp/tls.csr -subj "/CN=$(hostname)" >& /dev/null; do + if [ "$SECONDS" -ge "$CSR_DEADLINE" ]; then + give_up "no certificate request after ${CSR_TIMEOUT}s; /etc/xcat/certkey.pem is not usable" + fi sleep 1 done echo " diff --git a/xCAT-genesis-scripts/xCAT-genesis-scripts.spec b/xCAT-genesis-scripts/xCAT-genesis-scripts.spec index f42ffd477..1760f86d7 100644 --- a/xCAT-genesis-scripts/xCAT-genesis-scripts.spec +++ b/xCAT-genesis-scripts/xCAT-genesis-scripts.spec @@ -10,6 +10,14 @@ %ifarch aarch64 %define tarch aarch64 %endif +%ifarch riscv64 +%define tarch riscv64 +%endif +# An arch missing from the ladder above leaves %{tarch} unexpanded, and rpm then builds a package +# with a macro in its NAME instead of failing. Stop the build here instead. +%if ! %{defined tarch} +%{error:no genesis tarch for %{_target_cpu} -- add an %%ifarch branch above} +%endif %define rpminstallroot /opt/xcat/share/xcat/netboot/genesis/%{tarch}/fs BuildArch: noarch %define name xCAT-genesis-scripts-%{tarch} diff --git a/xCAT-server/lib/xcat/plugins/kvm.pm b/xCAT-server/lib/xcat/plugins/kvm.pm index 1e81317ae..a58c0d825 100644 --- a/xCAT-server/lib/xcat/plugins/kvm.pm +++ b/xCAT-server/lib/xcat/plugins/kvm.pm @@ -498,6 +498,8 @@ sub build_diskstruct { my @suffixes = ('a', 'b', 'd' .. 'zzz'); my $suffidx = 0; my $storagemodel = $confdata->{vm}->{$node}->[0]->{storagemodel}; + my $profile = guest_arch_profile($confdata->{nodetype}->{$node}->[0]->{arch}, + $confdata->{ $confdata->{vm}->{$node}->[0]->{host} }->{cpumodel}); my $cachemethod = "none"; if ($confdata->{vm}->{$node}->[0]->{storagecache}) { $cachemethod = $confdata->{vm}->{$node}->[0]->{storagecache}; @@ -511,13 +513,17 @@ sub build_diskstruct { #Setting default values of a virtual disk backed by a file at hd*. my $diskhash; - $disk =~ s/=(.*)//; - my $model = $1; + # A failed substitution leaves $1 as the last successful capture, which can come + # from a match made by a caller. Read $1 only when this substitution matches. + my $model; + if ($disk =~ s/=(.*)//) { + $model = $1; + } unless ($model) { #if not defined, model will stay undefined like above $model = $storagemodel; - unless ($model) { $model = 'ide'; } #if still not defined, ide + unless ($model) { $model = $profile->{disk_model}; } } my $prefix = 'hd'; if ($model eq 'virtio') { @@ -549,13 +555,16 @@ sub build_diskstruct { $tdiskhash->{driver}->{type} = $disks{$_}->{format}; $tdiskhash->{driver}->{cache} = $cachemethod; $tdiskhash->{source}->{file} = $_; - $tdiskhash->{target}->{dev} = $disks{$_}->{device}; + my $device = $disks{$_}->{device}; + $tdiskhash->{target}->{dev} = $device; - if ($disks{$_} =~ /^vd/) { + # libvirt reads the bus out of the device name when the disk states + # none: hd* is ide, sd* is scsi, vd* is virtio. State the same bus. + if ($device =~ /^vd/) { $tdiskhash->{target}->{bus} = 'virtio'; - } elsif ($disks{$_} =~ /^hd/) { + } elsif ($device =~ /^hd/) { $tdiskhash->{target}->{bus} = 'ide'; - } elsif ($disks{$_} =~ /^sd/) { + } elsif ($device =~ /^sd/) { $tdiskhash->{target}->{bus} = 'scsi'; } push @returns, $tdiskhash; @@ -586,7 +595,8 @@ sub build_diskstruct { push @returns, $diskhash; } } - my $cdprefix = 'hd'; + # The riscv64 virt machine has no IDE controller, so the optical drive is scsi there. + my $cdprefix = $profile->{cd_prefix}; # Normally for vmstoragemodel=virtio, we would set prefix of "vd", but device name vd* # doesn't work for CDROM, so for now use the same prefix "sd" as for vmstoragemodel=scsi. @@ -704,6 +714,63 @@ sub getUnits { } } +# default_storagemodel: the storage model of a node whose vmstoragemodel is empty. +# +# The model names the volume of the node, createstorage builds that name, and libvirt reads +# the bus of the disk out of it. scsi keeps every architecture on sd*, which is the only disk +# controller the riscv64 virt machine has. +sub default_storagemodel { + return 'scsi'; +} + +# guest_arch_profile: the libvirt domain type and settings for one guest. +# +# The architecture of the guest comes from the node, not from the hypervisor. A node whose +# arch is not the arch of the hypervisor runs under emulation, which libvirt expresses as +# domain type "qemu". riscv64 has no BIOS: the virt machine boots UEFI, and pae/acpi/apic +# are x86 features that libvirt rejects there. +# +# POWER keeps reading the hypervisor cpumodel. ppc64le hypervisors report "ppc64le" (not +# "ppc64"); both are pseries guests whose libvirt arch is "ppc64". +# +# arch and machine stay undef when libvirt is to use its own default for the hypervisor. +sub guest_arch_profile { + my ($guest_arch, $hyp_cpumodel) = @_; + my %profile = ( + domtype => 'kvm', + arch => undef, + machine => undef, + firmware => undef, + x86_features => 1, + bios => 1, + sound => 1, + video => 'vga', + usb_input => 1, + disk_model => 'ide', + cd_prefix => 'hd', + ); + if (defined($guest_arch) and $guest_arch eq 'riscv64') { + $profile{domtype} = 'qemu'; + $profile{arch} = 'riscv64'; + $profile{machine} = 'virt'; + $profile{firmware} = 'efi'; + $profile{x86_features} = 0; + $profile{bios} = 0; + $profile{sound} = 0; + $profile{video} = 'virtio'; + $profile{usb_input} = 0; + $profile{disk_model} = 'scsi'; + $profile{cd_prefix} = 'sd'; + } elsif (defined($hyp_cpumodel) and ($hyp_cpumodel eq "ppc64" or $hyp_cpumodel eq "ppc64le")) { + $profile{arch} = 'ppc64'; + $profile{machine} = 'pseries'; + $profile{x86_features} = 0; + $profile{bios} = 0; + $profile{sound} = 0; + } + return \%profile; +} + sub build_xmldesc { my $node = shift; my %args = @_; @@ -716,19 +783,16 @@ sub build_xmldesc { $hypcputhreads = "1"; } - $xtree{type} = 'kvm'; + my $profile = guest_arch_profile($confdata->{nodetype}->{$node}->[0]->{arch}, $hypcpumodel); + + $xtree{type} = $profile->{domtype}; $xtree{name}->{content} = $node; $xtree{uuid}->{content} = getNodeUUID($node); $xtree{os} = build_oshash(); - # ppc64le hypervisors report cpumodel "ppc64le" (not "ppc64"); both are pseries - # guests whose libvirt arch is "ppc64". Without this the guest is emitted - # as an x86-style domain (no machine, plus the pae/acpi/apic below) which libvirt - # rejects on ppc64le hosts: "machine type 'pseries-*' does not support ACPI". - if (defined($hypcpumodel) and ($hypcpumodel eq "ppc64" or $hypcpumodel eq "ppc64le")) { - $xtree{os}->{type}->{arch} = "ppc64"; - $xtree{os}->{type}->{machine} = "pseries"; - delete $xtree{os}->{bios}; - } + $xtree{os}->{type}->{arch} = $profile->{arch} if defined $profile->{arch}; + $xtree{os}->{type}->{machine} = $profile->{machine} if defined $profile->{machine}; + $xtree{os}->{firmware} = $profile->{firmware} if defined $profile->{firmware}; + delete $xtree{os}->{bios} unless $profile->{bios}; if ($args{memory}) { $xtree{memory}->{content} = getUnits($args{memory}, "M", 1024); if ($confdata->{vm}->{$node}->[0]->{memory}) { @@ -940,9 +1004,7 @@ sub build_xmldesc { } } - # pae/acpi/apic are x86 features; pseries (ppc64/ppc64le) guests do not support - # them and libvirt rejects the domain if they are present. - unless (defined($hypcpumodel) and ($hypcpumodel eq "ppc64" or $hypcpumodel eq "ppc64le")) { + if ($profile->{x86_features}) { $xtree{features}->{pae} = {}; $xtree{features}->{acpi} = {}; $xtree{features}->{apic} = {}; @@ -965,10 +1027,13 @@ sub build_xmldesc { $vram = 65536; } #surprise, spice blows up with less vram than this after version 0.6 and up $xtree{devices}->{video} = [ { 'content' => '', 'model' => { type => $model, vram => $vram } } ]; } else { - $xtree{devices}->{video} = [ { 'content' => '', 'model' => { type => 'vga', vram => 8192 } } ]; + $xtree{devices}->{video} = [ { 'content' => '', 'model' => { type => $profile->{video}, vram => 8192 } } ]; + } + # The riscv64 virt machine has no USB controller, and libvirt refuses a USB device there. + if ($profile->{usb_input}) { + $xtree{devices}->{input}->{type} = 'tablet'; + $xtree{devices}->{input}->{bus} = 'usb'; } - $xtree{devices}->{input}->{type} = 'tablet'; - $xtree{devices}->{input}->{bus} = 'usb'; if (defined($confdata->{vm}->{$node}->[0]->{vidproto})) { $xtree{devices}->{graphics}->{type} = $confdata->{vm}->{$node}->[0]->{vidproto}; } else { @@ -983,10 +1048,9 @@ sub build_xmldesc { } if (defined($hypcpumodel) and $hypcpumodel eq 'ppc64') { $xtree{devices}->{emulator}->{content} = "/usr/bin/qemu-system-ppc64"; - } elsif (defined($hypcpumodel) and $hypcpumodel eq 'ppc64le') { - # do nothing for ppc64le, do not support sound at this time - ; - } else { + } + # libvirt resolves the emulator for every other architecture from its own capabilities. + if ($profile->{sound}) { $xtree{devices}->{sound}->{model} = 'ich6'; } @@ -1531,8 +1595,12 @@ sub createstorage { if ($mastername and $size) { return 1, "Can not specify both a master to clone and size(s)"; } - $filename =~ s/=(.*)//; - my $model = $1; + # A failed substitution leaves $1 as the last successful capture, which can come from a + # match made by a caller. Read $1 only when this substitution matches. + my $model; + if ($filename =~ s/=(.*)//) { + $model = $1; + } unless ($model) { #if not defined, model will stay undefined like above @@ -4251,8 +4319,7 @@ sub dohyp { foreach $node (sort (keys %{ $hyphash{$hyp}->{nodes} })) { unless ($confdata->{vm}->{$node}->[0]->{storagemodel}) { - # Storage model is not set, default to scsi for all architectures - $confdata->{vm}->{$node}->[0]->{storagemodel} = "scsi"; + $confdata->{vm}->{$node}->[0]->{storagemodel} = default_storagemodel(); } if ($confdata->{$hyp}->{cpu_thread}) { $confdata->{vm}->{$node}->[0]->{cpu_thread} = $confdata->{$hyp}->{cpu_thread}; diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 3c75d0634..01ddc5b95 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -356,6 +356,45 @@ sub genesis_lzma_command { return; } +#------------------------------------------------------------------------------- + +=head3 stage_genesis_payload + +Descriptions: + Copy the Genesis payload into place for mknb: for a legacy image the unpacked + root tree and then the kernel, for an exported image the nbroot tree. + + Extracted so the outcome can be driven directly. The copies are the only + place mknb learns that an installed Genesis image is unusable, and a caller + cannot tell WHICH copy failed from a single exit status. + +Arguments: + genesis_type, genesis_dir, tftpdir, arch, tempdir, and an optional run + coderef used in place of system() by the tests. +Returns: + (rc, source) -- rc is the exit status of the copy that failed, and source + names it, so the caller reports the file it could not read. + +=cut + +#------------------------------------------------------------------------------- +sub stage_genesis_payload { + my (%a) = @_; + my $run = $a{run} || sub { return system($_[0]); }; + my $rc; + if (($a{genesis_type} // '') eq 'legacy') { + # Two copies, each able to fail on its own. Return on the first, so neither the exit + # status nor the name of the unreadable file is lost to the one that follows it. + $rc = $run->("shopt -s dotglob; GLOBIGNORE=\".:..\" cp -a $a{genesis_dir}/fs/* $a{tempdir}"); + return ($rc, "$a{genesis_dir}/fs") if $rc; + $rc = $run->("cp -a $a{genesis_dir}/kernel $a{tftpdir}/xcat/genesis.kernel.$a{arch}"); + return ($rc, "$a{genesis_dir}/kernel") if $rc; + return (0, undef); + } + $rc = $run->("cp -a $a{genesis_dir}/nbroot/* $a{tempdir}"); + return ($rc, "$a{genesis_dir}/nbroot"); +} + sub process_request { my $request = shift; my $callback = shift; @@ -584,21 +623,13 @@ sub process_request { unless (-e "$tftpdir/xcat") { mkpath("$tftpdir/xcat"); } - my $rc; - if ($genesis_type eq 'legacy') { - $rc = system("shopt -s dotglob; GLOBIGNORE=\".:..\" cp -a $genesis_dir/fs/* $tempdir"); - $rc = system("cp -a $genesis_dir/kernel $tftpdir/xcat/genesis.kernel.$arch"); - $invisibletouch = 1; - } else { - $rc = system("cp -a $genesis_dir/nbroot/* $tempdir"); - } + $invisibletouch = 1 if $genesis_type eq 'legacy'; + my ($rc, $failed_src) = stage_genesis_payload( + genesis_type => $genesis_type, genesis_dir => $genesis_dir, + tftpdir => $tftpdir, arch => $arch, tempdir => $tempdir); if ($rc) { system("rm -rf $tempdir"); - if ($invisibletouch) { - $callback->({ error => ["Failed to copy $genesis_dir/fs contents"], errorcode => [1] }); - } else { - $callback->({ error => ["Failed to copy $genesis_dir/nbroot contents"], errorcode => [1] }); - } + $callback->({ error => ["Failed to copy $failed_src contents"], errorcode => [1] }); return; } my $sshdir; diff --git a/xCAT-server/share/xcat/tools/go-xcat b/xCAT-server/share/xcat/tools/go-xcat index f1c452c7c..bba19c05f 100755 --- a/xCAT-server/share/xcat/tools/go-xcat +++ b/xCAT-server/share/xcat/tools/go-xcat @@ -199,9 +199,9 @@ GO_XCAT_INSTALL_LIST=(perl-xCAT xCAT-client xCAT xCAT-buildkit # For Debian/Ubuntu, it will need a slightly different package list type dpkg >/dev/null 2>&1 && GO_XCAT_INSTALL_LIST=(perl-xcat xcat-client xcat xcat-buildkit - xcat-genesis-scripts-amd64 xcat-genesis-scripts-ppc64 xcat-server + xcat-genesis-scripts-amd64 xcat-genesis-scripts-ppc64el xcat-server elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat - xcat-genesis-base-amd64 xcat-genesis-base-ppc64 xnba-undi) + xcat-genesis-base-amd64 xcat-genesis-base-ppc64el xnba-undi) # The package list of all the packages should be uninstalled GO_XCAT_UNINSTALL_LIST=("${GO_XCAT_INSTALL_LIST[@]}" goconserver xCAT-SoftLayer xCAT-confluent xCAT-csm xCAT-genesis-builder diff --git a/xCAT-test/autotest/testcase/genesis/cases0 b/xCAT-test/autotest/testcase/genesis/cases0 index e2e7e7ca8..066eac4e8 100644 --- a/xCAT-test/autotest/testcase/genesis/cases0 +++ b/xCAT-test/autotest/testcase/genesis/cases0 @@ -2,14 +2,13 @@ start:nodeset_shell_lzma os:rhels8 label:others,genesis description: verify could log in genesis shell lzma compression -cmd:if [[ "__GETNODEATTR($$CN,os)__" =~ "rhel" ]]; then yum install -y https://rpmfind.net/linux/centos/8-stream/PowerTools/__GETNODEATTR($$CN,arch)__/os/Packages/xz-lzma-compat-5.2.4-3.el8.__GETNODEATTR($$CN,arch)__.rpm; elif rpm -q xz; then yum download https://rpmfind.net/linux/centos/8-stream/PowerTools/__GETNODEATTR($$CN,arch)__/os/Packages/xz-lzma-compat-5.2.4-3.el8.__GETNODEATTR($$CN,arch)__.rpm; rpm -ivh --nodeps xz-lzma-compat-5.2.4-3.el8.__GETNODEATTR($$CN,arch)__.rpm; fi #Generate genesis network boot with lzma compression cmd:mknb __GETNODEATTR($$CN,arch)__ check:rc==0 cmd:nodeset $$CN shell check:rc==0 cmd:ls -l /tftpboot/xcat/genesis.fs.*.lzma -check:output=~genesis +check:rc==0 cmd:find /tftpboot -type f -name $$CN | xargs grep "lzma" check:output=~genesis cmd:perl /opt/xcat/share/xcat/tools/autotest/testcase/genesis/genesistest.pl -n $$CN -g @@ -19,8 +18,7 @@ check:rc==0 cmd:perl /opt/xcat/share/xcat/tools/autotest/testcase/genesis/genesistest.pl -n $$CN -c check:rc==0 cmd:cat /tmp/genesistestlog/* -#Remove lzma compression RPM, cleanup and generate default gz genesis network boot -cmd:yum remove -y xz-lzma-compat +#Cleanup and generate the default gz genesis network boot cmd:rm -f /tftpboot/xcat/genesis.fs.*.lzma cmd:mknb __GETNODEATTR($$CN,arch)__ end diff --git a/xCAT-test/autotest/testcase/genesis/genesistest.pl b/xCAT-test/autotest/testcase/genesis/genesistest.pl index 19beef853..6b520d744 100755 --- a/xCAT-test/autotest/testcase/genesis/genesistest.pl +++ b/xCAT-test/autotest/testcase/genesis/genesistest.pl @@ -68,13 +68,7 @@ if (!defined($noderange)) { } my $os = &get_os; if ($check_genesis_file) { - send_msg(2, "[$$]:Check if genesis packages are installed on mn..............."); - &check_genesis_file(&get_arch); - if ($?) { - send_msg(0, "genesis packages are not installed"); - } else { - send_msg(2, "genesis packages are installed"); - } + exit 1 if &report_genesis_files(&get_arch); } my $master=`lsdef -t site -i master -c 2>&1 | awk -F'=' '{print \$2}'`; if (!$master) { $master=hostname(); } @@ -89,29 +83,7 @@ if (!(-e $nodestanza)) { ####nodesetshell test for genesis #################################### if ($genesis_nodesetshell_test) { - send_msg(2, "[$$]:Running nodeset NODE shell test..............."); - `nodeset $noderange shell`; - if ($?) { - send_msg(0, "[$$]:nodeset $noderange shell failed..............."); - exit 1; - } - `rpower $noderange boot`; - if ($?) { - send_msg(0, "[$$]:rpower $noderange failed..............."); - exit 1; - } - else { - send_msg(2, "Installing with \"nodeset $noderange shell\" for shell test"); - sleep 120; # wait 2 min for install to finish - wait_for_boot(); - } - #run nodeshell test - send_msg(2, "prepare for nodeshell script."); - if ( &testxdsh(3)) { - send_msg(0, "[$$]:Could not verify test results using xdsh..............."); - exit 1; - } - send_msg(2, "[$$]:Running nodesetshell test success..............."); + exit 1 if &run_nodeset_shell_test(); } #################################### ####runcmd test for genesis @@ -148,6 +120,51 @@ if ($clear_env) { send_msg(2, "[$$]:Clear genesis test enviroment success..............."); } ################################## +#run_nodeset_shell_test +################################# +sub run_nodeset_shell_test { + send_msg(2, "[$$]:Running nodeset NODE shell test..............."); + `nodeset $noderange shell`; + if ($?) { + send_msg(0, "[$$]:nodeset $noderange shell failed..............."); + return 1; + } + `rpower $noderange boot`; + if ($?) { + send_msg(0, "[$$]:rpower $noderange failed..............."); + return 1; + } + send_msg(2, "Installing with \"nodeset $noderange shell\" for shell test"); + sleep 120; # wait 2 min for install to finish + if (&wait_for_node_status("shell")) { + send_msg(0, "[$$]:$noderange did not report the shell destiny..............."); + return 1; + } + #run nodeshell test + send_msg(2, "prepare for nodeshell script."); + if (&testxdsh(3)) { + send_msg(0, "[$$]:Could not verify test results using xdsh..............."); + return 1; + } + send_msg(2, "[$$]:Running nodesetshell test success..............."); + return 0; +} +################################## +#report_genesis_files +################################# +sub report_genesis_files { + my ($arch) = @_; + send_msg(2, "[$$]:Check if genesis packages are installed on mn..............."); + # The caller used to test $?, which holds the exit status of the last child process, not + # this return value. A node with no genesis packages therefore reported success. + if (&check_genesis_file($arch)) { + send_msg(0, "genesis packages are not installed"); + return 1; + } + send_msg(2, "genesis packages are installed"); + return 0; +} +################################## #check_genesis_file ################################# sub check_genesis_file { @@ -214,7 +231,7 @@ sub rungenesiscmd { else { send_msg(2, "Installing with \"$rinstall_cmd\" for runcmd test"); sleep 120; # wait 2 min for install to finish - wait_for_boot(); + $value = -1 if &wait_for_node_status("configuring"); } return $value; } @@ -257,13 +274,24 @@ sub rungenesisimg { } else { send_msg(2, "Installing with \"$rinstall_cmd\" for runimage test\n"); sleep 120; # wait 2 min for install to finish - wait_for_boot(); + $value = -1 if &wait_for_node_status("booting"); } return $value; } ######################################## ####sleep while for xdsh $$CN could work ######################################### +########################################## +####forget the node ssh host keys +########################################## +sub forget_host_keys { + my ($noderange) = @_; + # Genesis makes new host keys on every boot, and each case boots the node several times. + # The stale known_hosts entry then makes ssh refuse the changed key, and xdsh cannot reach + # the Genesis shell. + system("makeknownhosts $noderange -r >/dev/null 2>&1"); + return 0; +} sub testxdsh { my $value = shift; my $checkstring; @@ -285,6 +313,8 @@ sub testxdsh { return 1; } + &forget_host_keys($noderange); + # Check shell prompt on the node to verify it is running Genesis `xdsh $noderange -t 2 "echo \\\$PS1" | grep "Genesis"`; if ($?) { @@ -353,8 +383,9 @@ sub clearenv { `cat $nodestanza | chdef -z`; unlink("$nodestanza"); } + # "rinstall boot" boots the node from its disk, which carries no operating system, + # so the node reports no destiny and nodelist.status stays at powering-on. Only wait. sleep 120; # wait 2 min for reboot to finish - wait_for_boot(); return 0; } #################################### @@ -365,7 +396,10 @@ sub get_os { my $output = `cat /etc/*release* 2>&1`; if ($output =~ /suse/i) { $os = "sles"; - } elsif ($output =~ /Red Hat/i) { + } elsif ($output =~ /Red Hat/i + or $output =~ /\b(?:almalinux|rocky|centos|fedora|oracle\s+linux)\b/i + or $output =~ /^ID_LIKE=.*\brhel\b/mi) { + # AlmaLinux and Rocky release files name neither Red Hat nor themselves as one. $os = "redhat"; } elsif ($output =~ /ubuntu/i) { $os = "ubuntu"; @@ -423,9 +457,10 @@ sub send_msg { } ######################################### -### Wait for node to be in "booted" state +### Wait for the node to report the status its destiny implies ########################################## -sub wait_for_boot { +sub wait_for_node_status { + my ($expected) = @_; my $iterations = 30; # Max wait 30x10 = 5 min my $sleep_interval = 10; my $boot_status; @@ -433,11 +468,11 @@ sub wait_for_boot { foreach my $i (1..$iterations) { $boot_status = `lsdef $noderange -i status -c | cut -d'=' -f2`; chop($boot_status); - if ($boot_status eq "booted") { + if ($boot_status eq $expected) { return 0; } sleep $sleep_interval; } - print "After $iterations iterations node status: $boot_status \n"; + print "After $iterations iterations node status: $boot_status, expected $expected \n"; return 1; } diff --git a/xCAT-test/autotest/testcase/genesis/test.sh b/xCAT-test/autotest/testcase/genesis/test.sh index 4b46ea617..904eea1d4 100755 --- a/xCAT-test/autotest/testcase/genesis/test.sh +++ b/xCAT-test/autotest/testcase/genesis/test.sh @@ -16,14 +16,49 @@ function runcmd(){ # We should be using private networks TESTNODE=testnode TESTNODE_IP="192.168.3.1" +# nodeset resolves the genesis kernel by the node arch, so the node takes this machine's. +TESTNODE_ARCH="$(uname -m)" +# The boot-loader configuration lives under the tftp root. Overridable so the check can run +# against a scratch tree. +TFTPDIR="${TFTPDIR:-/tftpboot}" + +# grub2.pm names the boot loader grub2., with every ppc64 flavour written as "ppc". +TESTNODE_LOADER_ARCH="$TESTNODE_ARCH" +[[ $TESTNODE_LOADER_ARCH =~ ^ppc64 ]] && TESTNODE_LOADER_ARCH="ppc" +STAGED_BOOT_LOADER="" MASTER_PRIVATE_IP="192.168.1.1" MASTER_PRIVATE_NETMASK="255.255.0.0" MASTER_PRIVATE_NETWORK="192_168_0_0-255_255_0_0" +# xCAT builds no grub2 network boot loader for x86_64 or aarch64. The administrator installs +# grub2. by hand -- docs/source/guides/install-guides/yum/grub2.rst. grub2.pm stops the +# configuration when the file is absent, and this case reads the configuration only. +function stage_boot_loader() { + local loader="$TFTPDIR/boot/grub2/grub2.$TESTNODE_LOADER_ARCH"; + if [[ -e $loader ]];then + return 0; + fi + mkdir -p "$TFTPDIR/boot/grub2" || return 1; + : > "$loader" || return 1; + STAGED_BOOT_LOADER="$loader"; + echo "Staged an empty boot loader at $loader for the check"; + return 0; +} + +function unstage_boot_loader() { + if [[ -z $STAGED_BOOT_LOADER ]];then + return 0; + fi + # grub2.pm links grub2- to the loader. Remove the link with the file it points at. + rm -f "$STAGED_BOOT_LOADER" "$TFTPDIR/boot/grub2/grub2-${TESTNODE}"; + STAGED_BOOT_LOADER=""; + return 0; +} + function check_destiny() { - cmd="chdef ${TESTNODE} arch=ppc64le cons=ipmi groups=all ip=${TESTNODE_IP} mac=4e:ee:ee:ee:ee:0e netboot=$NETBOOT tftpserver=$MASTER_PRIVATE_IP xcatmaster=$MASTER_PRIVATE_IP"; + cmd="chdef ${TESTNODE} arch=${TESTNODE_ARCH} cons=ipmi groups=all ip=${TESTNODE_IP} mac=4e:ee:ee:ee:ee:0e netboot=$NETBOOT tftpserver=$MASTER_PRIVATE_IP xcatmaster=$MASTER_PRIVATE_IP"; runcmd $cmd; lsdef ${TESTNODE} @@ -52,8 +87,15 @@ function check_destiny() { grep ${TESTNODE} /etc/hosts cmd="nodeset ${TESTNODE} shell"; runcmd $cmd; + # grub2.pm writes the boot configuration and only then stops on a missing boot loader, + # so the file the check reads below exists even when nodeset failed. + nodeset_rc=$?; cmd="ip addr del $MASTER_PRIVATE_IP/$MASTER_PRIVATE_NETMASK dev $NET2"; runcmd $cmd; + if [[ $nodeset_rc -ne 0 ]];then + echo "'nodeset ${TESTNODE} shell' FAILED"; + return 1; + fi echo "Check if 'nodeset ${TESTNODE} shell' is added to ${SHELLFOLDER}/${TESTNODE}" echo "===============================================" cat "${SHELLFOLDER}/${TESTNODE}" @@ -86,14 +128,17 @@ while [ "$#" -ge "0" ]; do "--check" ) NETBOOT=$2; if [[ $NETBOOT =~ petitboot ]];then - SHELLFOLDER="/tftpboot/petitboot"; + SHELLFOLDER="$TFTPDIR/petitboot"; elif [[ $NETBOOT =~ xnba ]];then - SHELLFOLDER="/tftpboot/xcat/xnba/nodes" + SHELLFOLDER="$TFTPDIR/xcat/xnba/nodes" else - SHELLFOLDER="/tftpboot/boot/grub2"; + SHELLFOLDER="$TFTPDIR/boot/grub2"; + stage_boot_loader || exit 1; fi check_destiny ; - if [[ $? -eq 1 ]];then + rc=$?; + unstage_boot_loader; + if [[ $rc -eq 1 ]];then exit 1 else exit 0 diff --git a/xCAT-test/autotest/testcase/lsxcatd/cases0 b/xCAT-test/autotest/testcase/lsxcatd/cases0 index eb82fb0ff..b7003d219 100644 --- a/xCAT-test/autotest/testcase/lsxcatd/cases0 +++ b/xCAT-test/autotest/testcase/lsxcatd/cases0 @@ -2,7 +2,7 @@ start:lsxcatd_null description:lsxcatd without any flag label:mn_only,ci_test,xcatd cmd:lsxcatd -check:output=~lsxcatd +check:output=~\[-v\|--version\] end start:lsxcatd_h diff --git a/xCAT-test/autotest/testcase/ngpfb/cases0 b/xCAT-test/autotest/testcase/ngpfb/cases0 index e3604488d..4d3c1b8c3 100644 --- a/xCAT-test/autotest/testcase/ngpfb/cases0 +++ b/xCAT-test/autotest/testcase/ngpfb/cases0 @@ -78,7 +78,7 @@ description:for hwconn label:others,hctrl_fsp cmd:rmhwconn $$CN check:rc==0 -check:rc!~(state=LINE UP) +check:output!~(state=LINE UP) cmd:mkhwconn $$CN -t check:rc==0 cmd:sleep 40 @@ -87,7 +87,7 @@ check:rc==0 check:output=~(LINE UP) cmd:rmhwconn blade check:rc==0 -check:rc!~(state=LINE UP) +check:output!~(state=LINE UP) cmd:mkhwconn blade -t check:rc==0 cmd:sleep 50 diff --git a/xCAT-test/autotest/testcase/nodepurge/cases0 b/xCAT-test/autotest/testcase/nodepurge/cases0 index 451fdc46b..94a744e0a 100644 --- a/xCAT-test/autotest/testcase/nodepurge/cases0 +++ b/xCAT-test/autotest/testcase/nodepurge/cases0 @@ -21,9 +21,9 @@ cmd:ls /install/autoinst/testnode1* check:output=~No such file or directory cmd:ls /install/autoinst/testnode2* check:output=~No such file or directory -cmd:ping testnode1 +cmd:ping -c 1 -w 2 testnode1 check:rc!=0 -cmd:ping testnode2 +cmd:ping -c 1 -w 2 testnode2 check:rc!=0 end diff --git a/xCAT-test/autotest/testcase/pscp/cases0 b/xCAT-test/autotest/testcase/pscp/cases0 index e7050691e..ff061794c 100644 --- a/xCAT-test/autotest/testcase/pscp/cases0 +++ b/xCAT-test/autotest/testcase/pscp/cases0 @@ -19,7 +19,7 @@ cmd:echo "test" > /tmp/pscp.tmp check:rc==0 cmd:pscp /tmp/pscp.tmp $$CN:/tmp/ check:rc==0 -check:$$CN: done +check:output=~$$CN: done cmd:xdsh $$CN "ls -l /tmp |grep pscp.tmp" check:rc==0 check:output=~pscp.tmp @@ -41,7 +41,7 @@ cmd:echo "test" > /tmp/pscp/pscp.tmp check:rc==0 cmd:pscp -r /tmp/pscp $$CN:/tmp/ check:rc==0 -check:$$CN: done +check:output=~$$CN: done cmd:xdsh $$CN "ls -l /tmp |grep pscp" check:rc==0 check:output=~pscp diff --git a/xCAT-test/autotest/testcase/rscan/cases0 b/xCAT-test/autotest/testcase/rscan/cases0 index 98f09ae30..aaf6752e2 100644 --- a/xCAT-test/autotest/testcase/rscan/cases0 +++ b/xCAT-test/autotest/testcase/rscan/cases0 @@ -77,7 +77,7 @@ cmd:rmdef $$CN cmd:rscan __GETNODEATTR(testnode,hcp)__ -z -w check:rc==0 check:output=~parent=[\w-]+ -check:lsdef -l $$CN +cmd:lsdef -l $$CN check:rc==0 check:output=~parent=[\w-]+ cmd:rmdef all diff --git a/xCAT-test/autotest/testcase/updatenode/cases0 b/xCAT-test/autotest/testcase/updatenode/cases0 index d0d24e4c5..a8b94d8ed 100644 --- a/xCAT-test/autotest/testcase/updatenode/cases0 +++ b/xCAT-test/autotest/testcase/updatenode/cases0 @@ -83,9 +83,9 @@ start:updatenode_diskful_syncfiles_dir label:others,updatenode cmd:mkdir -p /tmp/sync/ check:rc==0 -cmd:echo "test1" > /tmp/sync/test1.txt +cmd:echo "syncdata1" > /tmp/sync/test1.txt check:rc==0 -cmd:echo "test2" > /tmp/sync/test2.txt +cmd:echo "syncdata2" > /tmp/sync/test2.txt check:rc==0 cmd:echo "/tmp/sync/* -> /tmp/" > /install/custom/install/__GETNODEATTR($$CN,os)__/compute.$$OS.synclist check:rc==0 @@ -97,9 +97,9 @@ cmd:xdsh $$CN "ls -l /tmp" check:output=~test1.txt check:output=~test2.txt cmd:xdsh $$CN "cat /tmp/test1.txt" -check:output=~test1 +check:output=~syncdata1 cmd:xdsh $$CN "cat /tmp/test2.txt" -check:output=~test2 +check:output=~syncdata2 cmd:xdsh $$CN "rm -rf /tmp/test1.txt /tmp/test2.txt" check:rc==0 cmd:chdef -t osimage -o __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-install-compute synclists= diff --git a/xCAT-test/autotest/testcase/xcat_inventory/cases.osimage b/xCAT-test/autotest/testcase/xcat_inventory/cases.osimage index cd6fb34ff..8b5e9fb3a 100644 --- a/xCAT-test/autotest/testcase/xcat_inventory/cases.osimage +++ b/xCAT-test/autotest/testcase/xcat_inventory/cases.osimage @@ -1040,8 +1040,8 @@ cmd:dir="/opt/inventory/site/osimage";if [ -e "${dir}" ];then mv ${dir} ${dir}". cmd:xcat-inventory export -t osimage -o test_myimage1,test_myimage2 --format json -d /opt/inventory/site/osimage check:rc==0 check:output=~The osimage objects has been exported to directory /opt/inventory/site/osimage -cmd: ls -R /opt/inventory/site/osimage -check: output =~ site +cmd:ls -R /opt/inventory/site/osimage +check:output=~test_myimage1 cmd:otherpkglist=`lsdef -t osimage -o test_myimage1 |grep otherpkglist|awk -F= '{print $2}'`;diff -y $otherpkglist /opt/inventory/site/osimage/test_myimage1$otherpkglist check:rc==0 cmd:synclists=`lsdef -t osimage -o test_myimage1 |grep synclists|awk -F= '{print $2}'`;diff -y $synclists /opt/inventory/site/osimage/test_myimage1$synclists @@ -1073,8 +1073,8 @@ check:rc==0 cmd: rmdef -t osimage -o test_myimage1,test_myimage2 check:rc==0 cmd:rm -rf /tmp/otherpkglist /tmp/synclists /tmp/postinstall /tmp/exlist /tmp/partitionfile /tmp/pkglist /tmp/template -cmd: ls -R /opt/inventory/site -check: output =~ site +cmd:ls -R /opt/inventory/site +check:output=~test_myimage1 cmd:xcat-inventory import -t osimage -o test_myimage1,test_myimage2 -d /opt/inventory/site/osimage check:rc==0 check:output=~The object test_myimage1 has been imported diff --git a/xCAT-test/autotest/testcase/xdcp/cases0 b/xCAT-test/autotest/testcase/xdcp/cases0 index 57fa39bf9..5d8c095a0 100644 --- a/xCAT-test/autotest/testcase/xdcp/cases0 +++ b/xCAT-test/autotest/testcase/xdcp/cases0 @@ -42,11 +42,11 @@ start:xdcp_RP label:cn_os_ready,parallel_cmds cmd:xdsh $$CN "mkdir -p /tmp/xdcp/test1" check:rc==0 -cmd:xdsh $$CN "echo "test1" > /tmp/xdcp/test1/test1.txt" +cmd:xdsh $$CN "echo "xdcpdata1" > /tmp/xdcp/test1/test1.txt" check:rc==0 cmd:xdsh $$CN "mkdir -p /tmp/xdcp/test2" check:rc==0 -cmd:xdsh $$CN "echo "test2" > /tmp/xdcp/test2/test2.txt" +cmd:xdsh $$CN "echo "xdcpdata2" > /tmp/xdcp/test2/test2.txt" check:rc==0 cmd:xdcp $$CN -RP /tmp/xdcp /tmp check:rc==0 @@ -58,9 +58,9 @@ check:output=~test1.txt cmd:ls -l /tmp/xdcp._$$CN/test2 check:output=~test2.txt cmd:cat /tmp/xdcp._$$CN/test1/test1.txt -check:output=~test1 +check:output=~xdcpdata1 cmd:cat /tmp/xdcp._$$CN/test2/test2.txt -check:output=~test2 +check:output=~xdcpdata2 cmd:xdsh $$CN "rm -rf /tmp/xdcp" check:rc==0 cmd:rm -rf /tmp/xdcp._$$CN @@ -71,11 +71,11 @@ start:xdcp_R label:cn_os_ready,parallel_cmds cmd:mkdir -p /tmp/xdcp/test1 check:rc==0 -cmd:echo "test1" > /tmp/xdcp/test1/test1.txt +cmd:echo "xdcpdata1" > /tmp/xdcp/test1/test1.txt check:rc==0 cmd:mkdir -p /tmp/xdcp/test2 check:rc==0 -cmd:echo "test2" > /tmp/xdcp/test2/test2.txt +cmd:echo "xdcpdata2" > /tmp/xdcp/test2/test2.txt check:rc==0 cmd:xdcp $$CN -R /tmp/xdcp /tmp check:rc==0 @@ -89,9 +89,9 @@ check:output=~test1.txt cmd:xdsh $$CN "ls -l /tmp/xdcp/test2" check:output=~test2.txt cmd:xdsh $$CN "cat /tmp/xdcp/test1/test1.txt" -check:output=~test1 +check:output=~xdcpdata1 cmd:xdsh $$CN "cat /tmp/xdcp/test2/test2.txt" -check:output=~test2 +check:output=~xdcpdata2 cmd:xdsh $$CN "rm -rf /tmp/xdcp" check:rc==0 cmd:rm -rf /tmp/xdcp diff --git a/xCAT-test/bats/genesis_base_deb_arch.bats b/xCAT-test/bats/genesis_base_deb_arch.bats new file mode 100644 index 000000000..addf31146 --- /dev/null +++ b/xCAT-test/bats/genesis_base_deb_arch.bats @@ -0,0 +1,93 @@ +#!/usr/bin/env bats +# +# debuild-xcat-genesis-base converts the EL Genesis base rpm to a deb. The rpm name carries the +# Genesis target architecture, and the deb must carry the Debian architecture: ppc64 becomes +# ppc64el, x86_64 becomes amd64. An unmapped architecture leaves the deb named after the rpm and +# makes it break a genesis-scripts package that no repository publishes. The rename also has to +# name the deb it supersedes, or an upgraded ppc node keeps xcat-genesis-base-ppc64 as well. +# +# The script is driven here with alien shadowed by a shell function. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(repo_path 'xCAT-genesis-builder/debuild-xcat-genesis-base')" + # Fail rather than skip: a checkout without the converter has no deb rename to measure, + # and a skip there covers nothing while reading green. + [ -r "$SCRIPT" ] + export SCRIPT +} + +# alien names the deb after the rpm: lower case, and "_" written as "-". +shadow_alien() +{ + alien() + { + local rpm="${!#}" + local name="${rpm##*/}" + name="${name%.rpm}" + local dir="${name%%-snap*}" + local package="${dir%-*}" + package="${package,,}" + package="${package//_/-}" + + mkdir -p "${dir}/debian" + cat >"${dir}/debian/control" < + +Package: ${package} +Architecture: all +Description: xCAT genesis base +CONTROL + printf '%s (%s) unstable; urgency=low\n' "${package}" "1.0" \ + >"${dir}/debian/changelog" + printf '#!/usr/bin/make -f\nbinary:\n\t@true\n' >"${dir}/debian/rules" + chmod 0755 "${dir}/debian/rules" + } +} + +# Convert one rpm name. Sets SOURCE_DIR to the produced source directory and CONTROL to its +# control file. +convert() +{ + local rpm="$1" + local work="${BATS_TEST_TMPDIR}/convert" + + rm -rf "$work" + mkdir -p "$work" + ( + shadow_alien + cd "$work" || exit 1 + : >"$rpm" + source "$SCRIPT" "$rpm" >/dev/null 2>&1 + ) + + SOURCE_DIR="$(find "$work" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | head -1)" + [ -n "$SOURCE_DIR" ] || return 1 + CONTROL="$work/$SOURCE_DIR/debian/control" + [ -f "$CONTROL" ] || return 1 +} + +@test "the x86_64 rpm becomes the amd64 deb, and replaces the package it supersedes" { + convert 'xCAT-genesis-base-x86_64-2.13.10-snap202601010000.noarch.rpm' + + [[ "$SOURCE_DIR" == *-amd64-* ]] + grep -qx 'Package: xcat-genesis-base-amd64' "$CONTROL" + grep -qE '^Breaks:.*\bxcat-genesis-scripts-amd64\b' "$CONTROL" + grep -qx 'Replaces: xcat-genesis-amd64' "$CONTROL" + grep -qE '^Breaks: xcat-genesis-amd64\b' "$CONTROL" +} + +@test "the ppc64 rpm becomes the ppc64el deb, and replaces the deb the rename leaves behind" { + convert 'xCAT-genesis-base-ppc64-2.13.10-snap202601010000.noarch.rpm' + + [[ "$SOURCE_DIR" == *-ppc64el-* ]] + grep -qx 'Package: xcat-genesis-base-ppc64el' "$CONTROL" + grep -qE '^Breaks:.*\bxcat-genesis-scripts-ppc64el\b' "$CONTROL" + grep -qx 'Replaces: xcat-genesis-ppc64, xcat-genesis-base-ppc64' "$CONTROL" + grep -qE '^Breaks: xcat-genesis-ppc64, xcat-genesis-base-ppc64\b' "$CONTROL" +} diff --git a/xCAT-test/bats/genesis_base_deb_control_rewrite.bats b/xCAT-test/bats/genesis_base_deb_control_rewrite.bats new file mode 100644 index 000000000..188671c67 --- /dev/null +++ b/xCAT-test/bats/genesis_base_deb_control_rewrite.bats @@ -0,0 +1,57 @@ +#!/usr/bin/env bats +# +# builddeb-genesis-base builds the Genesis base deb natively on Ubuntu. It writes the target +# architecture into debian/control, which is held in the amd64 form in the tree. 2.19 renames +# the ppc64 debs to ppc64el, so the ppc control must also name the deb it supersedes: without +# the relation dpkg keeps xcat-genesis-base-ppc64 installed beside the new package, and that +# old package owns the same files under /opt/xcat/share/xcat/netboot/genesis. +# +# The script needs dracut and root, so rewrite_control() is lifted out of it and run alone +# against the control file the tree ships. + +load 'helpers/shell_source' + +setup() +{ + SCRIPT="$(repo_path 'xCAT-genesis-builder/builddeb-genesis-base')" + CONTROL="$(repo_path 'xCAT-genesis-builder/debian/control')" + [ -r "$SCRIPT" ] || skip "$SCRIPT is required" + [ -r "$CONTROL" ] || skip "$CONTROL is required" + export SCRIPT CONTROL +} + +# Run the lifted rewrite_control() over a copy of the control file in the tree, and print it. +rewrite() +{ + local arch="$1" function copy="${BATS_TEST_TMPDIR}/control.$1" + + function="$(extract_shell_function "$SCRIPT" rewrite_control)" || + { echo 'rewrite_control() no longer matches in builddeb-genesis-base' >&2; return 99; } + cp "$CONTROL" "$copy" + ( + set -eu + eval "$function" + rewrite_control "$copy" "$arch" + ) || return 1 + cat "$copy" +} + +@test "the amd64 control names the package and the genesis deb it took over from" { + run rewrite amd64 + [ "$status" -eq 0 ] + + [[ "$output" =~ (^|$'\n')"Package: xcat-genesis-base-amd64"($'\n'|$) ]] + [[ "$output" =~ (^|$'\n')"Replaces: xcat-genesis-amd64"($'\n'|$) ]] + [[ "$output" =~ (^|$'\n')"Breaks: xcat-genesis-amd64, " ]] + [[ "$output" =~ "xcat-genesis-scripts-amd64 (<< 2.13.10)" ]] +} + +@test "the ppc64el control also takes over from the ppc64 deb the rename leaves behind" { + run rewrite ppc64el + [ "$status" -eq 0 ] + + [[ "$output" =~ (^|$'\n')"Package: xcat-genesis-base-ppc64el"($'\n'|$) ]] + [[ "$output" =~ (^|$'\n')"Replaces: xcat-genesis-ppc64, xcat-genesis-base-ppc64"($'\n'|$) ]] + [[ "$output" =~ (^|$'\n')"Breaks: xcat-genesis-ppc64, xcat-genesis-base-ppc64, " ]] + [[ "$output" =~ "xcat-genesis-scripts-ppc64el (<< 2.13.10)" ]] +} diff --git a/xCAT-test/bats/genesis_console_mode.bats b/xCAT-test/bats/genesis_console_mode.bats new file mode 100644 index 000000000..98d36f95d --- /dev/null +++ b/xCAT-test/bats/genesis_console_mode.bats @@ -0,0 +1,68 @@ +#!/usr/bin/env bats +# +# Drive xcat_console_mode() out of the Genesis dracut cmdline hook. +# +# The hook cannot be sourced: it mounts filesystems, starts udev and ends in an endless +# loop. Extract the one function and run it with the terminal multiplexer shadowed. + +load 'helpers/shell_source' + +setup() +{ + EL_HOOK="$(repo_path 'xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh')" + UBUNTU_HOOK="$(repo_path 'xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh')" + [ -r "$EL_HOOK" ] || skip "$EL_HOOK is required" + [ -r "$UBUNTU_HOOK" ] || skip "$UBUNTU_HOOK is required" + export EL_HOOK UBUNTU_HOOK +} + +# Run the extracted function with the multiplexer shadowed by a stub that either starts a +# session or refuses, the way tmux refuses without a UTF-8 locale. +run_mode() +{ + local hook="$1" mux="$2" mux_works="$3" body + body="$(extract_shell_function "$hook" xcat_console_mode)" || + { echo "xcat_console_mode() not found in $hook" >&2; return 99; } + ( + eval "$body" + eval "$mux() { + [ \"\$mux_works\" = 1 ] && return 0 + echo '$mux: need UTF-8 locale (LC_CTYPE) but have ANSI_X3.4-1968' >&2 + return 1 + }" + xcat_console_mode + ) 2>/dev/null +} + +# The hook reads the mode once and guards the doxcat loop with it. +assert_hook_guards_doxcat() +{ + local hook="$1" mux="$2" + grep -qx 'XCAT_CONSOLE_MODE="$(xcat_console_mode)"' "$hook" + grep -qFx "if [ \"\$XCAT_CONSOLE_MODE\" = \"$mux\" ]; then" "$hook" + grep -A1 '^else$' "$hook" | grep -qx ' while :; do doxcat; sleep 5; done' +} + +@test "the el hook leaves no unguarded tmux loop and exports a UTF-8 locale" { + # tmux exits under the C locale, so an unguarded tmux loop never reaches doxcat. + refute_grep -q '^while :; do tmux attach-session' "$EL_HOOK" + grep -qx 'export LC_ALL=C.UTF-8' "$EL_HOOK" +} + +@test "el: xcat_console_mode reports the mode tmux can actually provide" { + [ "$(run_mode "$EL_HOOK" tmux 0)" = direct ] + [ "$(run_mode "$EL_HOOK" tmux 1)" = tmux ] +} + +@test "el: the hook resolves the console mode once and runs doxcat directly without tmux" { + assert_hook_guards_doxcat "$EL_HOOK" tmux +} + +@test "ubuntu: xcat_console_mode reports the mode screen can actually provide" { + [ "$(run_mode "$UBUNTU_HOOK" screen 0)" = direct ] + [ "$(run_mode "$UBUNTU_HOOK" screen 1)" = screen ] +} + +@test "ubuntu: the hook resolves the console mode once and runs doxcat directly without screen" { + assert_hook_guards_doxcat "$UBUNTU_HOOK" screen +} diff --git a/xCAT-test/bats/genesis_dhcp_client.bats b/xCAT-test/bats/genesis_dhcp_client.bats new file mode 100644 index 000000000..f45ec2cca --- /dev/null +++ b/xCAT-test/bats/genesis_dhcp_client.bats @@ -0,0 +1,122 @@ +#!/usr/bin/env bats +# +# Drive the DHCP client selection out of doxcat. +# +# doxcat cannot be sourced: it restarts rsyslogd, reads /proc/cmdline and ends in a loop that +# waits for an address. Extract the two routines and run them with the clients shadowed by +# stubs that record their own argv. + +load 'helpers/shell_source' + +ISC4='dhclient -cf /etc/dhclient.conf -pf /var/run/dhclient.eth0.pid eth0' +ISC6='dhclient -6 -pf /var/run/dhclient6.eth0.pid eth0 -lf /var/lib/dhclient/dhclient6.leases' + +setup() +{ + DOXCAT="$(repo_path 'xCAT-genesis-scripts/usr/bin/doxcat')" + SPEC="$(repo_path 'xCAT-genesis-builder/xCAT-genesis-base.spec')" + MODULE="$(repo_path 'xCAT-genesis-builder/dracut_105/el/module-setup.sh')" + [ -r "$DOXCAT" ] || skip "$DOXCAT is required" + [ -r "$SPEC" ] || skip "$SPEC is required" + [ -r "$MODULE" ] || skip "$MODULE is required" + export DOXCAT SPEC MODULE +} + +# Run the extracted routines with only the named clients on PATH. Sets OUT to the standard +# output, RAN to the recorded argv of whatever ran, and STATUS to the exit status. +probe() +{ + local call="$1" + shift + local dir="${BATS_TEST_TMPDIR}/probe" + local bin="$dir/bin" record="$dir/record" client selector runner + + selector="$(extract_shell_function "$DOXCAT" genesis_dhcp_command)" || + { echo 'doxcat carries no genesis_dhcp_command() to choose the client' >&2; return 99; } + runner="$(extract_shell_function "$DOXCAT" genesis_start_dhcp)" || + { echo 'doxcat carries no genesis_start_dhcp() to run the chosen client' >&2; return 99; } + + rm -rf "$dir" + mkdir -p "$bin" + + # PATH holds the stubs alone, so each one names itself rather than calling basename. + for client in "$@"; do + printf '#!/bin/sh\necho "%s $*" >> "%s"\nexit 0\n' "$client" "$record" >"$bin/$client" + chmod 0755 "$bin/$client" + done + + # logger writes to the console in the image and is not what these assertions measure. + printf '#!/bin/sh\nexit 0\n' >"$bin/logger" + chmod 0755 "$bin/logger" + + printf 'log_label=test\n%s\n%s\n%s\n' "$selector" "$runner" "$call" >"$dir/probe.sh" + OUT="$(PATH="$bin" /bin/bash "$dir/probe.sh" 2>/dev/null)" && STATUS=0 || STATUS=$? + RAN="$(read_file_or_empty "$record")" + return 0 +} + +selected() +{ + local family="$1" + shift + probe "genesis_dhcp_command $family eth0" "$@" + printf '%s\n' "$OUT" +} + +started() +{ + local family="$1" + shift + probe "genesis_start_dhcp $family eth0" "$@" + printf '%s\n' "$RAN" +} + +@test "doxcat names no DHCP client directly" { + # A release that packages no ISC client has no dhclient. + refute_grep -qE '^[[:space:]]*dhclient[[:space:]]' "$DOXCAT" + refute_grep -qE ';[[:space:]]*dhclient[[:space:]]' "$DOXCAT" +} + +@test "the build root and the payload check name the client the release ships" { + # EL8 and EL9 package the ISC client; AlmaLinux 10 baseos packages dhcpcd. The payload + # check has to name the client too, or the build passes with no client in the image again. + grep -A1 '^%if 0%{?rhel} >= 10$' "$SPEC" | grep -qx 'BuildRequires: dhcpcd' + grep -A1 '^%if 0%{?rhel} >= 10$' "$SPEC" | grep -qx 'GENESIS_REQUIRED="usr/sbin/dhcpcd"' +} + +@test "the dracut module installs the client the build root carries" { + # dracut_install reports a missing binary and returns, so naming dhclient alone shipped an + # image with no client at all. + refute_grep -qE '^[[:space:]]*dracut_install dhclient lldpad$' "$MODULE" + grep -qE '^[[:space:]]*dracut_install dhcpcd$' "$MODULE" + grep -qE '^[[:space:]]*dracut_install /usr/libexec/dhcpcd-run-hooks$' "$MODULE" +} + +@test "the ISC client keeps its command lines and is preferred when both are present" { + [ "$(selected 4 dhclient)" = "$ISC4" ] + [ "$(selected 6 dhclient)" = "$ISC6" ] + [ "$(selected 4 dhclient dhcpcd)" = "$ISC4" ] +} + +@test "dhcpcd stands in for dhclient, waiting for a lease and keeping the address" { + # dhcpcd on a single interface exits when its timeout expires, and the default is 30 + # seconds; doxcat waits for the lease for as long as it takes. dhcpcd also de-configures + # the interface when it exits unless it is persistent. + [ "$(selected 4 dhcpcd)" = 'dhcpcd -4 -b -p -t 0 eth0' ] + [ "$(selected 6 dhcpcd)" = 'dhcpcd -6 -b -p -t 0 eth0' ] + [[ "$(selected 4 dhcpcd)" =~ (^|[[:space:]])-t\ 0([[:space:]]|$) ]] + [[ "$(selected 4 dhcpcd)" =~ (^|[[:space:]])-p([[:space:]]|$) ]] +} + +@test "an image with no client chooses nothing, runs nothing and reports a failure" { + [ "$(selected 4)" = '' ] + [ "$(started 4)" = '' ] + + probe 'genesis_start_dhcp 4 eth0' + [ "$STATUS" -ne 0 ] +} + +@test "genesis_start_dhcp runs the client it chose" { + [ "$(started 4 dhcpcd)" = 'dhcpcd -4 -b -p -t 0 eth0' ] + [ "$(started 4 dhclient)" = "$ISC4" ] +} diff --git a/xCAT-test/bats/genesis_getcert_missing_openssl.bats b/xCAT-test/bats/genesis_getcert_missing_openssl.bats new file mode 100644 index 000000000..f88914eda --- /dev/null +++ b/xCAT-test/bats/genesis_getcert_missing_openssl.bats @@ -0,0 +1,72 @@ +#!/usr/bin/env bats +# +# Drive getcert with openssl absent, and with a certificate key that is not ready yet. +# doxcat runs getcert in the foreground and ignores its status, so a wait with no bound stops +# the boot and prints nothing. + +load 'helpers/shell_source' + +setup() +{ + GETCERT="$(repo_path 'xCAT-genesis-scripts/usr/bin/getcert')" + [ -r "$GETCERT" ] || skip "$GETCERT is required" + COUNTER="${BATS_TEST_TMPDIR}/req-count" + export GETCERT COUNTER +} + +write_stub() +{ + local dir="$1" name="$2" body="$3" + printf '#!/bin/sh\n%s\n' "$body" >"$dir/$name" + chmod 0755 "$dir/$name" +} + +# A PATH directory holding the commands getcert runs. openssl is absent unless it is asked for. +stub_dir() +{ + local with_openssl="${1:-0}" count="" + local dir="${BATS_TEST_TMPDIR}/bin" + + rm -rf "$dir" + mkdir -p "$dir" + write_stub "$dir" allowcred.awk 'exec sleep 3' + write_stub "$dir" hostname 'echo node1' + write_stub "$dir" logger 'echo "$@" >&2' + write_stub "$dir" sleep 'exec /bin/sleep "$@"' + if [ "$with_openssl" = 1 ]; then + [ -n "${COUNT_REQUESTS:-}" ] && count="echo req >> '$COUNTER'" + write_stub "$dir" openssl "[ \"\$1\" = req ] && { $count ; exit 1; } +exit 0" + fi + printf '%s\n' "$dir" +} + +# Run getcert with only the stub directory on PATH. The timeout is the harness guard: a status +# of 124 means getcert never stopped. +run_getcert() +{ + local bin="$1" limit="$2" csr_timeout="$3" + timeout -k 2 "$limit" env PATH="$bin" GETCERT_CSR_TIMEOUT="$csr_timeout" \ + /bin/bash "$GETCERT" 192.0.2.1:3001 2>&1 , with every ppc64 flavour written as "ppc". + case "$HOST_ARCH" in + ppc64*) LOADER_NAME=ppc ;; + *) LOADER_NAME="$HOST_ARCH" ;; + esac + export SCRIPT HOST_ARCH LOADER_NAME +} + +# Run `test.sh --check ` against a scratch tftp root. test.sh resets PATH, so the xCAT +# commands are shadowed with shell functions, which bash resolves first. The fake nodeset writes +# the boot file the check greps, so the assertion is on the check, not on xCAT. +# +# Sets STATUS, OUTPUT, CHDEF, LOADER_AT_NODESET and LOADER_LEFT. +run_check() +{ + local loader="$1" write_boot_file="$2" nodeset_status="${3:-0}" + local root="${BATS_TEST_TMPDIR}/$loader-$write_boot_file-$nodeset_status" + local tftp="$root/tftpboot" + local boot_loader="$tftp/boot/grub2/grub2.$LOADER_NAME" + local folder write + + rm -rf "$root" + mkdir -p "$tftp/xcat/xnba/nodes" "$tftp/boot/grub2" "$tftp/petitboot" + + case "$loader" in + xnba) folder="$tftp/xcat/xnba/nodes" ;; + petitboot) folder="$tftp/petitboot" ;; + *) folder="$tftp/boot/grub2" ;; + esac + if [ "$write_boot_file" = 1 ]; then + write="printf 'xcatd=192.168.1.1:3001 destiny=shell\n' > '$folder/testnode'" + else + write=":" + fi + + cat >"$root/driver.sh" <> '$root/chdef.log'; } +lsdef() { + if [ "\$1" = "-t" ] && [ "\$2" = "site" ]; then echo "clustersite: master=192.168.9.9"; return 0; fi + echo "Object name: testnode" +} +ifconfig() { printf 'eth0: flags\n inet 192.168.9.9\n\n'; } +netstat() { printf 'Kernel\nIface\neth0\neth1\nlo\n'; } +ip() { return 0; } +makenetworks() { return 0; } +tabdump() { return 0; } +makehosts() { return 0; } +rmdef() { return 0; } +nodeset() { + if [ -e '$boot_loader' ]; then echo yes > '$root/loader.at.nodeset'; else echo no > '$root/loader.at.nodeset'; fi + $write + return $nodeset_status +} +export TFTPDIR='$tftp' +. '$SCRIPT' --check $loader +DRIVER + + OUTPUT="$(/bin/bash "$root/driver.sh" 2>&1)" && STATUS=0 || STATUS=$? + CHDEF="$(read_file_or_empty "$root/chdef.log")" + LOADER_AT_NODESET="$(read_file_or_empty "$root/loader.at.nodeset")" + LOADER_LEFT=0 + [ -e "$boot_loader" ] && LOADER_LEFT=1 + return 0 +} + +@test "the xnba check passes and defines the node with the management node architecture" { + # The case defined its node as ppc64le whatever the management node was, so nodeset could + # not find a genesis kernel for it on x86_64 and the case could never pass there. + run_check xnba 1 + [ "$STATUS" -eq 0 ] || { echo "$OUTPUT"; false; } + [[ "$CHDEF" =~ (^|[[:space:]])arch=$HOST_ARCH([[:space:]]|$) ]] + [ "$HOST_ARCH" = ppc64le ] || + [ "$(grep -cE '(^|[[:space:]])arch=ppc64le([[:space:]]|$)' <<<"$CHDEF")" -eq 0 ] +} + +@test "the check fails when nodeset writes no boot file" { + run_check xnba 0 + [ "$STATUS" -ne 0 ] +} + +@test "the grub2 check reads the grub2 directory, and stages then removes the boot loader" { + # grub2 and petitboot read their configuration from other directories under the tftp root. + # xCAT builds no x86_64 or aarch64 grub2 network boot loader, so grub2.pm stops before it + # configures anything. The check stages one for the node arch and removes it after. + run_check grub2 1 + [ "$STATUS" -eq 0 ] || { echo "$OUTPUT"; false; } + [ "$LOADER_AT_NODESET" = yes ] + [ "$LOADER_LEFT" -eq 0 ] +} + +@test "a nodeset that fails makes the check fail, whatever the boot file holds" { + # grub2.pm writes the boot configuration and only then stops on a missing boot loader. The + # check read the file that failed nodeset had already written, so it passed on the debris. + run_check grub2 1 1 + [ "$STATUS" -ne 0 ] + + run_check xnba 1 1 + [ "$STATUS" -ne 0 ] +} diff --git a/xCAT-test/bats/genesis_root_home.bats b/xCAT-test/bats/genesis_root_home.bats new file mode 100644 index 000000000..cab5e780d --- /dev/null +++ b/xCAT-test/bats/genesis_root_home.bats @@ -0,0 +1,89 @@ +#!/usr/bin/env bats +# +# Drive the /etc/passwd rewrite out of the Genesis dracut cmdline hooks. +# +# mknb writes the management node key to /.ssh/authorized_keys for the legacy Genesis +# image, so sshd finds it only while the home directory of root is /. The hook makes it / +# by deleting the root entry the image ships and appending its own. Run that rewrite +# against every root entry shape dracut writes and read back the result. + +load 'helpers/shell_source' + +# dracut 99base writes the root entry itself. Up to dracut 057 the password field is always +# x; from dracut 060 the x arrives only with --hostonly, and the Genesis image is built -N. +DRACUT_049_057='root:x:0:0::/root:/bin/sh' +DRACUT_107='root::0:0::/root:/bin/sh' + +# A user name that starts with root but is not root. The delete must keep this line. +DECOY='rootfsadm:x:501:501::/home/rootfsadm:/sbin/nologin' + +# Lift the /etc/passwd rewrite out of a hook that cannot be sourced: the hook mounts +# filesystems, starts udev and ends in an endless loop. +extract_passwd_block() +{ + local hook="$1" + awk ' + /^sed .*\/etc\/passwd$/ { copy = 1 } + copy { print } + copy && /^__ENDL$/ { found = 1; exit } + END { if (!found) exit 1 } + ' "$hook" +} + +# Run the extracted block against a scratch passwd file and print the result. The block names +# /etc/passwd literally, so the path is redirected into the scratch tree first, and the run is +# refused if any reference to the real file survives: CI runs this as root. +run_rewrite() +{ + local hook="$1" shipped="$2" + local dir="${BATS_TEST_TMPDIR}/rewrite" + local passwd="$dir/passwd" block script + + rm -rf "$dir" + mkdir -p "$dir" + printf '%s\n%s\n' "$shipped" "$DECOY" >"$passwd" + + block="$(extract_passwd_block "$(repo_path "$hook")")" || + { echo "$hook: the /etc/passwd rewrite was not found" >&2; return 99; } + + [ "$(grep -o -F '/etc/passwd' <<<"$block" | wc -l)" -eq 2 ] || + { echo "$hook: expected 2 references to /etc/passwd" >&2; return 98; } + script="${block//\/etc\/passwd/$passwd}" + case "$script" in + */etc/passwd*) echo "$hook: a reference to the real /etc/passwd survived" >&2; return 97 ;; + esac + + bash -c "set -e +$script" || return 1 + cat "$passwd" +} + +assert_root_home_is_slash() +{ + local hook="$1" shipped="$2" passwd + passwd="$(run_rewrite "$hook" "$shipped")" + + [ "$(grep -c '^root:' <<<"$passwd")" -eq 1 ] + [ "$(grep '^root:' <<<"$passwd")" = 'root:x:0:0::/:/bin/bash' ] + grep -qxF "$DECOY" <<<"$passwd" +} + +assert_hook() +{ + local hook="$1" + [ -r "$(repo_path "$hook")" ] || skip "$hook is required" + assert_root_home_is_slash "$hook" "$DRACUT_049_057" + assert_root_home_is_slash "$hook" "$DRACUT_107" +} + +@test "the legacy hook gives root the home directory /" { + assert_hook 'xCAT-genesis-builder/xcat-cmdline.sh' +} + +@test "the el dracut 105 hook gives root the home directory /" { + assert_hook 'xCAT-genesis-builder/dracut_105/el/xcat-cmdline.sh' +} + +@test "the ubuntu dracut 105 hook gives root the home directory /" { + assert_hook 'xCAT-genesis-builder/dracut_105/ubuntu/xcat-cmdline.sh' +} diff --git a/xCAT-test/bats/go_xcat_genesis_package_names.bats b/xCAT-test/bats/go_xcat_genesis_package_names.bats new file mode 100644 index 000000000..871ae884b --- /dev/null +++ b/xCAT-test/bats/go_xcat_genesis_package_names.bats @@ -0,0 +1,129 @@ +#!/usr/bin/env bats +# +# go-xcat installs and uninstalls a fixed list of package names, and it keeps one list per +# packaging format. The Genesis packages are named after the architecture, and the two formats +# spell it differently: the rpm is xCAT-genesis-scripts-ppc64, the deb is +# xcat-genesis-scripts-ppc64el. +# +# The lists are built by go-xcat itself here, not read as text: the deb list exists only when +# "type dpkg" succeeds, so a shell function decides which branch each run takes. + +load 'helpers/go_xcat' +load 'helpers/shell_source' + +setup() +{ + go_xcat_require_source + SCRIPTS_DEBIAN="$(repo_path 'xCAT-genesis-scripts/debian')" + SPEC="$(repo_path 'xCAT-genesis-builder/xCAT-genesis-base.spec')" + [ -d "$SCRIPTS_DEBIAN" ] || skip "$SCRIPTS_DEBIAN is required" + [ -r "$SPEC" ] || skip "$SPEC is required" + export SCRIPTS_DEBIAN SPEC +} + +# Run the array definitions of go-xcat and print the two lists it built, one per line. +package_lists() +{ + local want_dpkg="$1" list_body + list_body="$(awk ' + /^GO_XCAT_INSTALL_LIST=\(/ { copy = 1 } + /^PATH=/ { exit } + copy { print } + ' "$GO_XCAT_SOURCE")" + [ -n "$list_body" ] || { echo 'go-xcat package arrays not found' >&2; return 3; } + ( + if [ "$want_dpkg" = 1 ]; then + dpkg() { :; } + fi + # A real dpkg on the build host would select the deb branch on every run. + PATH="" + eval "$list_body" + printf 'install %s\n' "${GO_XCAT_INSTALL_LIST[*]}" + printf 'uninstall %s\n' "${GO_XCAT_UNINSTALL_LIST[*]}" + ) +} + +package_list() +{ + package_lists "$1" | sed -n "s/^$2 //p" +} + +# The package names of a list, sorted, that start with a prefix. +named() +{ + local prefix="$1" word + for word in $(cat); do + case "$word" in + "$prefix"*) printf '%s\n' "$word" ;; + esac + done | sort +} + +# The deb names come from the packaging: one control file per Debian architecture names the +# genesis-scripts package, and its Depends names the genesis-base package that carries the +# Genesis tree for that same architecture. +control_scripts_packages() +{ + grep -h '^Package:' "$SCRIPTS_DEBIAN"/control-* | awk '{ print $2 }' | sort +} + +control_base_packages() +{ + grep -h '^Depends:' "$SCRIPTS_DEBIAN"/control-* | + grep -o 'xcat-genesis-base-[a-z0-9]\+' | sort +} + +# The Genesis target architectures of the spec, which are not Debian architecture names. +spec_target_arches() +{ + awk '$1 == "%define" && $2 == "tarch" { print $3 }' "$SPEC" | sort -u +} + +# The names of a list that carry an architecture the spec does not define. +unknown_target_arches() +{ + local prefix="$1" name arch + while read -r name; do + arch="${name#"$prefix"}" + spec_target_arches | grep -qx "$arch" || printf '%s\n' "$name" + done +} + +@test "the package lists of go-xcat can be built for both packaging formats" { + [ -n "$(control_scripts_packages)" ] + [ -n "$(spec_target_arches)" ] + + run package_list 1 install + [ "$status" -eq 0 ] + [[ " $output " == *' xcat-client '* ]] + + run package_list 0 install + [ "$status" -eq 0 ] + [[ " $output " == *' xCAT-client '* ]] +} + +@test "the deb install list names the genesis packages the Debian control files declare" { + list="$(package_list 1 install)" + [ "$(printf '%s' "$list" | named 'xcat-genesis-scripts-')" = "$(control_scripts_packages)" ] + [ "$(printf '%s' "$list" | named 'xcat-genesis-base-')" = "$(control_base_packages)" ] +} + +@test "the deb uninstall list names the genesis packages the Debian control files declare" { + list="$(package_list 1 uninstall)" + [ "$(printf '%s' "$list" | named 'xcat-genesis-scripts-')" = "$(control_scripts_packages)" ] + [ "$(printf '%s' "$list" | named 'xcat-genesis-base-')" = "$(control_base_packages)" ] +} + +@test "the rpm install list names only Genesis target architectures" { + list="$(package_list 0 install)" + for prefix in xCAT-genesis-scripts- xCAT-genesis-base-; do + [ -z "$(printf '%s' "$list" | named "$prefix" | unknown_target_arches "$prefix")" ] + done +} + +@test "the rpm uninstall list names only Genesis target architectures" { + list="$(package_list 0 uninstall)" + for prefix in xCAT-genesis-scripts- xCAT-genesis-base-; do + [ -z "$(printf '%s' "$list" | named "$prefix" | unknown_target_arches "$prefix")" ] + done +} diff --git a/xCAT-test/bats/helpers/shell_source.bash b/xCAT-test/bats/helpers/shell_source.bash index 61a0bea8d..4f4384fb2 100644 --- a/xCAT-test/bats/helpers/shell_source.bash +++ b/xCAT-test/bats/helpers/shell_source.bash @@ -130,3 +130,13 @@ extract_first_matching_line() } ' "$file" } + +# grep that fails when the pattern IS present. +# +# Do not write "! grep ..." for this. bash ignores errexit for a command inverted with "!", +# so such a line never fails a test unless it is the last line of one. +refute_grep() +{ + ! grep "$@" + return $? +} diff --git a/xCAT-test/unit/autotest_check_lines_are_understood.t b/xCAT-test/unit/autotest_check_lines_are_understood.t new file mode 100644 index 000000000..1f4e54861 --- /dev/null +++ b/xCAT-test/unit/autotest_check_lines_are_understood.t @@ -0,0 +1,105 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Copy qw(copy); +use File::Find qw(find); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +my $program = "$FindBin::Bin/../xcattest"; +my $casedir = "$FindBin::Bin/../autotest/testcase"; +die("xcattest is not at $program") unless -f $program; +die("no test cases under $casedir") unless -d $casedir; + +# A check line xcattest does not understand costs the case the assertion it describes, and the +# case says nothing about it: an unknown operator reports "Unrecognized testcase syntax", and a +# line whose content does not start with a word character is dropped while the case is loaded. +# Read the shipped check lines and let the harness report on them. +my @files; +find({ wanted => sub { push(@files, $File::Find::name) if -f $File::Find::name }, no_chdir => 1 }, $casedir); +die("no case files under $casedir") unless @files; + +my (%checks, %vars); +for my $file (sort @files) { + open(my $fh, '<', $file) or die("open $file: $!"); + while (my $line = <$fh>) { + chomp($line); + next unless $line =~ /^check\s*:\s*(\S.*)$/; + my $check = $1; + + # __GETNODEATTR(...)__ and its siblings read the xCAT database, one lsdef for each + # check. The shape of the line is what this test reads, so a fixed value stands in. + $check =~ s/__\w+\([^)]*\)__/placeholder/g; + $vars{$1} = 1 while ($check =~ /\$\$(\w+)/g); + push(@{ $checks{$file} }, $check); + } + close($fh) or die("close $file: $!"); +} +die("no check lines under $casedir") unless keys %checks; + +# One case per shipped file, so a check that reports nothing is attributed to its own file. +my %case_of_file = map { $_ => 'syntax_' . do { my $n = $_; $n =~ s{^\Q$casedir\E/?}{}; $n =~ s/[^A-Za-z0-9_-]/_/g; $n } } keys %checks; + +my $fixture = ''; +for my $file (sort keys %checks) { + $fixture .= "start:$case_of_file{$file}\n"; + $fixture .= "cmd:true\n"; + $fixture .= "check:$_\n" for @{ $checks{$file} }; + $fixture .= "end\n"; +} + +# xcattest derives its result directory from the location of the program, so the copy under the +# scratch tree keeps every file the run writes inside that tree. +my $root = tempdir(CLEANUP => 1); +make_path("$root/bin", "$root/cases"); +copy($program, "$root/bin/xcattest") or die("copy xcattest: $!"); +chmod 0755, "$root/bin/xcattest"; +open(my $fixture_fh, '>', "$root/cases/fixture") or die("write the fixture case: $!"); +print $fixture_fh $fixture; +close($fixture_fh) or die("close the fixture case: $!"); + +# Every variable a check line names has to resolve, or xcattest drops the whole case. +# A "local" here would be undone at the end of its own statement, before the run. +$ENV{"XCATTEST_$_"} = 'placeholder' for keys %vars; +$ENV{XCATTEST_CASEDIR} = "$root/cases"; +# Some shipped patterns warn when perl compiles them, and the warnings say nothing about the +# operator. The log file carries what this test reads, so the warnings go to the scratch tree. +open(my $stderr_save, '>&', \*STDERR) or die("save STDERR: $!"); +open(STDERR, '>', "$root/stderr") or die("redirect STDERR: $!"); +system($^X, "$root/bin/xcattest", '-q', '-t', join(',', sort values %case_of_file)); +open(STDERR, '>&', $stderr_save) or die("restore STDERR: $!"); + +my ($logname) = glob("$root/share/xcat/tools/autotest/result/xcattest.log.*"); +die("the harness wrote no log under $root") unless $logname; +open(my $log_fh, '<', $logname) or die("open $logname: $!"); +my @log = <$log_fh>; +close($log_fh) or die("close $logname: $!"); +chomp(@log); + +# Count what the harness reported for each case, and keep the lines it did not understand. +my (%reported, @unrecognized, $current); +for my $line (@log) { + $current = $1 if ($line =~ /^------START::(\S+)::/); + next unless defined $current; + $reported{$current}++ if ($line =~ /^CHECK:/ or $line =~ /^Unrecognized testcase syntax:/); + push(@unrecognized, "$current: $line") if ($line =~ /^Unrecognized testcase syntax:/); + $current = undef if ($line =~ /^------END::/); +} + +is(join("\n", @unrecognized), '', + 'every check line in the shipped cases uses an operator xcattest understands'); + +my @silent; +for my $file (sort keys %checks) { + my $case = $case_of_file{$file}; + my $fed = scalar @{ $checks{$file} }; + my $got = $reported{$case} || 0; + push(@silent, "$file: $fed check lines, $got reported") if ($got != $fed); +} +is(join("\n", @silent), '', + 'every check line in the shipped cases reports a result, so none is dropped while the case loads'); + +done_testing(); diff --git a/xCAT-test/unit/builddebs_lock.t b/xCAT-test/unit/builddebs_lock.t index 5166dcba9..cba190133 100644 --- a/xCAT-test/unit/builddebs_lock.t +++ b/xCAT-test/unit/builddebs_lock.t @@ -7,9 +7,7 @@ # builds of DIFFERENT checkouts share nothing and must run concurrently. The historic # host-global lock got that backwards and made the devel and stable CD lanes collide. # -# This drives the real lock. The predecessor extracted a marked region out of -# build-ubunturepo with a regex and ran that; now the lock is a function, so it is -# called directly. +# This drives the real lock. The lock is a function, so it is called directly. use strict; use warnings; diff --git a/xCAT-test/unit/debian_control_arch_coverage.t b/xCAT-test/unit/debian_control_arch_coverage.t new file mode 100644 index 000000000..824df71cb --- /dev/null +++ b/xCAT-test/unit/debian_control_arch_coverage.t @@ -0,0 +1,129 @@ +#!/usr/bin/env perl +# xCAT and xCATsn name their Debian architectures explicitly. An architecture missing from that +# list is not a build failure: it is a package apt cannot find at all. +# +# The list is compared against the architectures the DEB build itself supports, taken from +# build-utils/lib/XCAT/BuildUtils or, failing that, the documented set. +use strict; +use warnings; + +use FindBin; +use Test::More; + +my $root = "$FindBin::Bin/../.."; + +# The Debian architectures xCAT ships. dpkg names, not rpm ones. +my @arches = qw(amd64 ppc64el riscv64); + +my @controls = grep { -f } ("$root/xCAT/debian/control", "$root/xCATsn/debian/control"); +plan skip_all => 'no Debian control files in this tree' unless @controls; + +for my $ctl (@controls) { + open my $fh, '<', $ctl or die "read $ctl: $!"; + local $/; my $text = <$fh>; close $fh; + (my $short = $ctl) =~ s{^\Q$root\E/}{}; + my @lines = ($text =~ /^Architecture:\s*(.+)$/mg); + my @explicit = grep { !/^(?:any|all)$/ } map { s/^\s+|\s+$//gr } @lines; + ok(scalar(@explicit), "$short names architectures explicitly") or next; + for my $line (@explicit) { + my %have = map { $_ => 1 } split /\s+/, $line; + my @missing = grep { !$have{$_} } @arches; + is_deeply(\@missing, [], "$short covers @arches"); + } +} + +# The genesis dependency must follow the architecture. xCAT and xCATsn are built once per +# architecture from one control file, and xcat-genesis-scripts-amd64 is Architecture: all, so an +# unrestricted Depends on it installs the x86_64 Genesis tree on every architecture. +# +# xCAT-genesis-scripts keeps one control file per Debian architecture, named for it. Its package +# name and its genesis-base dependency must carry that same architecture: the base deb +# builddeb-genesis-base builds for ppc64el is xcat-genesis-base-ppc64el, not -ppc64. + +# Return the folded value of a control field, or undef. +sub control_field { + my ($text, $name) = @_; + return $1 if $text =~ /^\Q$name\E:[ \t]*(.*(?:\n[ \t]+.*)*)/m; + return; +} + +# Split a dependency field into [package name, architecture restriction] pairs. Alternatives +# separated by "|" are returned one by one, because a restriction binds to one alternative. +sub dependency_terms { + my ($field) = @_; + my @terms; + return @terms unless defined $field; + $field =~ s/\n/ /g; + for my $dep (split /,/, $field) { + for my $alt (split /\|/, $dep) { + next unless $alt =~ /^\s*([A-Za-z0-9][A-Za-z0-9+.-]*)\s*(?:\([^)]*\))?\s*(?:\[([^\]]*)\])?/; + push @terms, [ $1, $2 ]; + } + } + return @terms; +} + +# dpkg-gencontrol drops a dependency whose architecture restriction excludes the build +# architecture. No restriction means the dependency reaches every architecture. +sub term_applies { + my ($restriction, $arch) = @_; + return 1 unless defined $restriction; + my @tokens = grep { length } split /\s+/, $restriction; + return 1 unless @tokens; + my $negated = ($tokens[0] =~ /^!/) ? 1 : 0; + my %named = map { my $t = $_; $t =~ s/^!//; $t =~ s/^any-//; ($t => 1) } @tokens; + return $negated ? (exists $named{$arch} ? 0 : 1) : (exists $named{$arch} ? 1 : 0); +} + +# The architectures xCAT-genesis-scripts is packaged for, taken from its per-architecture control +# files. riscv64 has none on purpose: its Genesis is the OpenEmbedded image. +my $scripts_debian = "$root/xCAT-genesis-scripts/debian"; +my @scripts_arches = sort map { m{/control-(.+)$} ? $1 : () } glob("$scripts_debian/control-*"); + +SKIP: { + skip 'xCAT-genesis-scripts has no per-architecture control files', 1 unless @scripts_arches; + + for my $arch (@scripts_arches) { + my $ctl = "$scripts_debian/control-$arch"; + open my $fh, '<', $ctl or die "read $ctl: $!"; + local $/; my $text = <$fh>; close $fh; + + my ($package) = ($text =~ /^Package:\s*(\S+)/m); + is($package, "xcat-genesis-scripts-$arch", + "control-$arch builds xcat-genesis-scripts-$arch"); + + my @bases = grep { /^xcat-genesis-base-/ } + map { $_->[0] } dependency_terms(control_field($text, 'Depends')); + is_deeply(\@bases, ["xcat-genesis-base-$arch"], + "xcat-genesis-scripts-$arch depends on xcat-genesis-base-$arch"); + } + + for my $ctl (@controls) { + open my $fh, '<', $ctl or die "read $ctl: $!"; + local $/; my $text = <$fh>; close $fh; + (my $short = $ctl) =~ s{^\Q$root\E/}{}; + + my ($arch_line) = ($text =~ /^Architecture:\s*(.+)$/m); + next unless defined $arch_line; + my @built = grep { !/^(?:any|all)$/ } split /\s+/, $arch_line; + + my @genesis = grep { $_->[0] =~ /^xcat-genesis-scripts-/ } + dependency_terms(control_field($text, 'Depends')); + + for my $arch (@built) { + my @reaching = map { $_->[0] } + grep { term_applies($_->[1], $arch) } @genesis; + my @foreign = grep { $_ ne "xcat-genesis-scripts-$arch" } @reaching; + is_deeply(\@foreign, [], + "$short on $arch depends on no other architecture's genesis scripts"); + + my %packaged = map { $_ => 1 } @scripts_arches; + next unless $packaged{$arch}; + ok(scalar(grep { $_ eq "xcat-genesis-scripts-$arch" } @reaching), + "$short on $arch depends on xcat-genesis-scripts-$arch"); + } + } +} + + +done_testing(); diff --git a/xCAT-test/unit/genesis_base_spec_buildrequires.t b/xCAT-test/unit/genesis_base_spec_buildrequires.t new file mode 100644 index 000000000..621f3a001 --- /dev/null +++ b/xCAT-test/unit/genesis_base_spec_buildrequires.t @@ -0,0 +1,42 @@ +#!/usr/bin/env perl +# The genesis spec is the build root manifest: what it does not build-require, the buildroot +# only holds by accident, and dracut_install then installs nothing. +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path slurp_repo_file); + +my $relative = 'xCAT-genesis-builder/xCAT-genesis-base.spec'; +plan skip_all => "$relative not found" unless -f repo_path($relative); +plan tests => 4; + +my @lines = split /\n/, slurp_repo_file($relative); + +# getcert, getdestiny, getipmi and getadapter all run the openssl command. el8 and el9 +# held it in the buildroot as a dependency of something else; el10 does not. +my @openssl = grep { /^BuildRequires:\s*openssl\s*$/ } @lines; +is(scalar(@openssl), 1, 'the spec build-requires openssl'); + +my ($buildarch) = grep { $lines[$_] =~ /^BuildArch:\s*noarch/ } 0 .. $#lines; +ok(defined $buildarch, 'the spec sets BuildArch: noarch'); + +# rpm reads the spec a second time with the target set to noarch, so %{_target_cpu} is +# "noarch" from BuildArch onwards. %{tarch} keeps the real architecture. +my @late_target_cpu = grep { $lines[$_] =~ /_target_cpu/ } ($buildarch + 1) .. $#lines; +is(scalar(@late_target_cpu), 0, + '%{_target_cpu} is not read after BuildArch: noarch') + or diag(join "\n", map { ($_ + 1) . ": $lines[$_]" } @late_target_cpu); + +my ($openssl_line) = grep { $lines[$_] =~ /^BuildRequires:\s*openssl\s*$/ } 0 .. $#lines; +my $guarded = 0; +if (defined $openssl_line) { + for my $i (reverse 0 .. $openssl_line - 1) { + last if $lines[$i] =~ /^%endif/; + $guarded = 1, last if $lines[$i] =~ /^%if/; + } +} +is($guarded, 0, 'openssl is build-required on every release'); diff --git a/xCAT-test/unit/genesis_payload_verification.t b/xCAT-test/unit/genesis_payload_verification.t new file mode 100644 index 000000000..d1334b24b --- /dev/null +++ b/xCAT-test/unit/genesis_payload_verification.t @@ -0,0 +1,186 @@ +#!/usr/bin/env perl +# Drive verify-genesis-payload against payload trees that each leave out one thing the image +# needs. +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $verifier = repo_path('xCAT-genesis-builder/verify-genesis-payload'); +plan skip_all => 'verify-genesis-payload not found' unless -f $verifier; +plan tests => 22; + +my $tmpdir = tempdir(CLEANUP => 1); +my $module_seq = 0; + +# A complete payload: OpenSSH 9.9 sshd plus its session helper, tmux plus a UTF-8 locale. +my $good = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1, mktemp => 1); +my ($rc, $err) = run($good, 'usr/sbin/dhclient'); +is($rc, 0, 'a complete payload passes') or diag($err); + +# doxcat calls dhclient with ISC flags. dhclient.conf and dhclient-script are not enough. +my $nodhcp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 0, mktemp => 1); +($rc, $err) = run($nodhcp, 'usr/sbin/dhclient'); +isnt($rc, 0, 'a payload without dhclient fails'); +like($err, qr{usr/sbin/dhclient}, 'the missing dhclient is named'); + +# sshd 9.9 execs /usr/libexec/openssh/sshd-session for every connection. +my $nohelper = build_payload(sshd_execs_session => 1, session_helper => 0, tmux => 1, locale => 1, dhclient => 1, mktemp => 1); +($rc, $err) = run($nohelper, 'usr/sbin/dhclient'); +isnt($rc, 0, 'a payload whose sshd execs sshd-session but does not ship it fails'); +like($err, qr{sshd-session}, 'the missing sshd-session is named'); + +# OpenSSH 8 does not use the helper, so el8 must still pass without it. +my $openssh8 = build_payload(sshd_execs_session => 0, session_helper => 0, tmux => 1, locale => 1, dhclient => 1, mktemp => 1); +($rc, $err) = run($openssh8, 'usr/sbin/dhclient'); +is($rc, 0, 'an OpenSSH 8 payload passes without sshd-session') or diag($err); + +# tmux without a UTF-8 locale is what stopped doxcat from ever running. +my $nolocale = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 0, dhclient => 1, mktemp => 1); +($rc, $err) = run($nolocale, 'usr/sbin/dhclient'); +isnt($rc, 0, 'a payload with tmux and no UTF-8 locale fails'); +like($err, qr{C\.utf8}, 'the missing locale is named'); + +# getdestiny makes its request file with mktemp. +my $nomktemp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1, mktemp => 0); +($rc, $err) = run($nomktemp, 'usr/sbin/dhclient'); +isnt($rc, 0, 'a payload without mktemp fails'); +like($err, qr{usr/bin/mktemp}, 'the missing mktemp is named'); + +# dracut_install reports a missing binary and returns, so every name the dracut module +# installs has to be checked against the payload. +my $module = write_module_setup([qw(openssl wget tar)]); +my $full = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)]); +($rc, $err) = run_with_commands($module, $full); +is($rc, 0, 'a payload carrying every command the module names passes') or diag($err); + +my $noopenssl = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 1, mktemp => 1, commands => [qw(wget tar)]); +($rc, $err) = run_with_commands($module, $noopenssl); +isnt($rc, 0, 'a payload without openssl fails'); +like($err, qr/openssl/, 'the missing openssl is named'); + +# dracut_install installs an absolute path at that same path, so a name starting with "/" is a +# command the payload must carry. doxcat, getdestiny and the firmware wrappers all run awk. +my $noawk = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)], absent => ['usr/bin/awk']); +($rc, $err) = run_with_commands($module, $noawk); +isnt($rc, 0, 'a payload without the absolute path /usr/bin/awk fails'); +like($err, qr{/usr/bin/awk}, 'the missing /usr/bin/awk is named'); + +# The module names data files by absolute path too. Genesis resolves service names with +# /etc/services. +my $noservices = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)], absent => ['etc/services']); +($rc, $err) = run_with_commands($module, $noservices); +isnt($rc, 0, 'a payload without the absolute path /etc/services fails'); +like($err, qr{/etc/services}, 'the missing /etc/services is named'); + +# The DHCP client is release-dependent, so the module installs it inside a conditional. Those +# names are not the contract; the spec passes the one it wants as a required path. +my $conditional = write_module_setup(['wget'], ['dhclient']); +my $nodhclient = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, + dhclient => 0, mktemp => 1, commands => ['wget']); +($rc, $err) = run_with_commands($conditional, $nodhclient); +is($rc, 0, 'a name installed under a condition is not required') or diag($err); + +# A module the verifier cannot read names for covers nothing, so say so instead of passing. +my $unparsable = "$tmpdir/module-setup-unparsable.sh"; +write_text($unparsable, "#!/bin/bash\nsetup() {\n dracut_install wget\n}\n"); +($rc, $err) = run_with_commands($unparsable, $full); +is($rc, 2, 'a module the verifier finds no command names in is a usage error'); +like($err, qr/command name/, 'the empty command list is named'); + +($rc, $err) = run_with_commands("$tmpdir/no-such-module", $full); +is($rc, 2, 'a module file that cannot be read is a usage error'); + +($rc, $err) = run("$tmpdir/does-not-exist"); +is($rc >> 0, 2, 'a missing payload directory is a usage error'); + +#--- +# build_payload: make a payload tree with the pieces the verifier reasons about. +#--- +sub build_payload { + my (%opt) = @_; + my $root = tempdir(DIR => $tmpdir, CLEANUP => 1); + make_path("$root/usr/sbin", "$root/usr/bin", "$root/usr/libexec/openssh"); + write_text("$root/usr/sbin/sshd", + $opt{sshd_execs_session} + ? "OpenSSH_9.9p1\n/usr/libexec/openssh/sshd-session\n" + : "OpenSSH_8.0p1\n"); + write_text("$root/usr/libexec/openssh/sshd-session", "helper\n") if $opt{session_helper}; + write_text("$root/usr/bin/tmux", "tmux\n") if $opt{tmux}; + if ($opt{locale}) { + make_path("$root/usr/lib/locale/C.utf8"); + write_text("$root/usr/lib/locale/C.utf8/LC_CTYPE", "ctype\n"); + } + write_text("$root/usr/sbin/dhclient", "dhclient\n") if $opt{dhclient}; + write_text("$root/usr/bin/mktemp", "mktemp\n") if $opt{mktemp}; + write_text("$root/usr/bin/$_", "$_\n") for @{ $opt{commands} || [] }; + + # The module written by write_module_setup names these two by absolute path. + my %absent = map { $_ => 1 } @{ $opt{absent} || [] }; + for my $path (qw(usr/bin/awk etc/services)) { + next if $absent{$path}; + my ($dir) = $path =~ m{^(.*)/}; + make_path("$root/$dir"); + write_text("$root/$path", "$path\n"); + } + return $root; +} + +#--- +# run: run the verifier and return its exit status and stderr. +#--- +sub run { + my ($root, @required) = @_; + my $errfile = "$tmpdir/err.$$"; + my $cmd = join ' ', map { "'$_'" } ($verifier, $root, @required); + system("/bin/bash $cmd >/dev/null 2>$errfile"); + my $status = $? >> 8; + my $err = -f $errfile ? read_text($errfile) : ''; + unlink $errfile; + return ($status, $err); +} + +#--- +# write_module_setup: a dracut module whose install() names commands at the top level, and +# optionally more inside a conditional. +#--- +sub write_module_setup { + my ($top, $conditional) = @_; + my $path = "$tmpdir/module-setup." . ++$module_seq . ".sh"; + my $text = "#!/bin/bash\n\ninstall() {\n"; + $text .= " dracut_install " . join(' ', @$top) . " # a trailing comment\n"; + $text .= " dracut_install /usr/bin/awk /etc/services\n"; + if ($conditional) { + $text .= " if command -v " . $conditional->[0] . " >/dev/null 2>&1; then\n"; + $text .= " dracut_install " . join(' ', @$conditional) . "\n"; + $text .= " fi\n"; + } + $text .= "}\n"; + write_text($path, $text); + return $path; +} + +#--- +# run_with_commands: run the verifier with the command list read back from a dracut module. +#--- +sub run_with_commands { + my ($module, $root) = @_; + my $errfile = "$tmpdir/err.commands.$$"; + my $cmd = join ' ', map { "'$_'" } ($verifier, '--commands-from', $module, $root); + system("/bin/bash $cmd >/dev/null 2>$errfile"); + my $status = $? >> 8; + my $err = -f $errfile ? read_text($errfile) : ''; + unlink $errfile; + return ($status, $err); +} diff --git a/xCAT-test/unit/genesis_spec_target_arch.t b/xCAT-test/unit/genesis_spec_target_arch.t new file mode 100644 index 000000000..fa69c4c19 --- /dev/null +++ b/xCAT-test/unit/genesis_spec_target_arch.t @@ -0,0 +1,46 @@ +#!/usr/bin/env perl +# The genesis specs name their package after the target arch: xCAT-genesis-scripts- and +# xCAT-genesis-base-. %{tarch} comes from an %ifarch ladder, and an arch missing from that +# ladder leaves the macro unexpanded instead of failing, so rpm builds a package whose Name +# carries the macro. +# +# Expand each spec with rpmspec for every arch xCAT supports and assert the Name carries that arch. +use strict; +use warnings; + +use FindBin; +use Test::More; + +my $root = "$FindBin::Bin/../.."; + +sub command_exists { my ($c) = @_; return system("command -v $c >/dev/null 2>&1") == 0 } + +plan skip_all => 'rpmspec is not installed' unless command_exists('rpmspec'); + +# arch under test => the tarch the spec must resolve it to (x86 and ppc64 are historical names +# genesis keeps; see genesis_tarch_from_targetarch in buildrpms.pl). +my %tarch = ( + x86_64 => 'x86_64', + i686 => 'x86', + ppc64le => 'ppc64', + aarch64 => 'aarch64', + riscv64 => 'riscv64', +); + +my %spec = ( + 'xCAT-genesis-scripts' => "$root/xCAT-genesis-scripts/xCAT-genesis-scripts.spec", + 'xCAT-genesis-base' => "$root/xCAT-genesis-builder/xCAT-genesis-base.spec", +); + +for my $pkg (sort keys %spec) { + my $spec = $spec{$pkg}; + ok(-f $spec, "$pkg spec is present") or next; + for my $arch (sort keys %tarch) { + my $name = `rpmspec --target $arch -q --qf '%{NAME}' --define 'version 2.19.0' --define 'release snap0' @{[quotemeta $spec]} 2>/dev/null`; + chomp $name; + is($name, "$pkg-$tarch{$arch}", "$pkg on $arch is named $pkg-$tarch{$arch}"); + unlike($name, qr/%\{/, "$pkg on $arch leaves no unexpanded macro in its name"); + } +} + +done_testing(); diff --git a/xCAT-test/unit/genesis_testcase_helpers.t b/xCAT-test/unit/genesis_testcase_helpers.t new file mode 100644 index 000000000..2747bf314 --- /dev/null +++ b/xCAT-test/unit/genesis_testcase_helpers.t @@ -0,0 +1,210 @@ +#!/usr/bin/env perl +# Drive the genesis test case helpers. genesistest.pl needs a management node, so lift the +# routines out and run them with rpm, dpkg and cat shadowed. +use strict; +use warnings; + +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $helper = repo_path('xCAT-test/autotest/testcase/genesis/genesistest.pl'); +my $shell = repo_path('xCAT-test/autotest/testcase/genesis/test.sh'); +plan skip_all => 'genesis testcase helpers not found' unless -f $helper && -f $shell; +plan tests => 16; + +my $tmpdir = tempdir(CLEANUP => 1); +my $source = read_text($helper); + +eval_subs($source, qw(get_os get_arch check_genesis_file)); + +# The destiny status check used to be wait_for_boot(), which waited for "booted" and ignored +# its argument. Take whichever name the script carries, so this test fails on the status the +# check waits for and not on a missing subroutine. +my $waiter_name = waiter_name($source); +eval_subs($source, $waiter_name); +my $waiter = \&{"GenesisTest::$waiter_name"}; + +# get_os drives every later branch. AlmaLinux and Rocky release files say neither "Red Hat" +# nor "suse" nor "ubuntu", so the management node read as unknown and the check was skipped. +is(os_for("AlmaLinux release 9.8 (Olive Jaguar)\n"), 'redhat', 'AlmaLinux is a redhat family node'); +is(os_for("Rocky Linux release 9.5 (Blue Onyx)\n"), 'redhat', 'Rocky is a redhat family node'); +is(os_for("Red Hat Enterprise Linux release 9.5\n"), 'redhat', 'RHEL is still a redhat family node'); +is(os_for("SUSE Linux Enterprise Server 15 SP6\n"), 'sles', 'SLES is still detected'); +is(os_for("NAME=\"Ubuntu\"\nID=ubuntu\n"), 'ubuntu', 'Ubuntu is still detected'); + +# check_genesis_file answers with a return value. The caller used to read $? instead, so a +# management node with no genesis packages reported success. +{ + no warnings 'once'; + local $GenesisTest::os = 'redhat'; + is(rpm_check("xCAT-genesis-base-x86_64-2.19.0-snap1.noarch\nxCAT-genesis-scripts-x86_64-2.19.0-snap1.noarch\n"), + 0, 'both genesis packages installed reports success'); + is(rpm_check("xCAT-genesis-scripts-x86_64-2.19.0-snap1.noarch\n"), + 1, 'a missing genesis-base reports failure'); + eval_subs($source, qw(report_genesis_files)); + is(report_files("xCAT-genesis-scripts-x86_64-2.19.0-snap1.noarch\n"), + 1, 'report_genesis_files propagates the failure to its caller'); +} + +# Genesis generates new host keys at every boot, and each case boots the node several times. +{ + no warnings 'once'; + eval_subs($source, qw(forget_host_keys testxdsh)); + local $GenesisTest::noderange = 'xcat71-cn'; + my $run = run_testxdsh(3, genesis_prompt => 1, cmdline => 'destiny=shell'); + is($run->{status}, 0, 'testxdsh succeeds when the node answers in the Genesis shell'); + like($run->{makeknownhosts}, qr/\bxcat71-cn\b/, 'the node host keys are forgotten first'); + like($run->{makeknownhosts}, qr/-r/, 'makeknownhosts is asked to remove them'); +} + +# xCAT sets nodelist.status from the destiny the node reports with getdestiny: "shell" for the +# shell destiny, "configuring" for runcmd. "booted" belongs to an operating system install. +{ + no warnings 'once'; + local $GenesisTest::noderange = 'xcat71-cn'; + is(wait_status('shell', 'shell'), 0, + 'a node that reports the shell destiny ends the wait'); + is(wait_status('configuring', 'configuring'), 0, + 'a node that reports the runcmd destiny ends the wait'); + isnt(wait_status('powering-on', 'shell'), 0, + 'a node that never reports its destiny fails the wait'); +} + +# The shell case ignored the result of the wait, so it went on to xdsh whatever the node had +# reported and rested entirely on the xdsh probes. +{ + no warnings 'once'; + eval_subs($source, qw(run_nodeset_shell_test)); + local $GenesisTest::noderange = 'xcat71-cn'; + is(run_shell_test(status => 'shell'), 0, + 'the shell case passes when the node reports the shell destiny'); + isnt(run_shell_test(status => 'powering-on'), 0, + 'the shell case fails when the node never reports the shell destiny'); +} + +#--- +# wait_status: drive the destiny status check with lsdef shadowed to report one status. The +# extracted package neuters sleep, so the failure path does not wait five minutes. +#--- +sub wait_status { + my ($reported, $expected) = @_; + local $ENV{PATH} = stub_bin(lsdef => + "#!/bin/sh\nprintf 'xcat71-cn: status=%s\\n' " . shell_quote($reported)) . ":$ENV{PATH}"; + return $waiter->($expected); +} + +#--- +# run_shell_test: drive the shell case with every command it runs shadowed. xdsh always answers +# as a Genesis node, so the only thing under test is what the case does with the node status. +#--- +sub run_shell_test { + my (%opt) = @_; + my $dir = tempdir(DIR => $tmpdir, CLEANUP => 1); + write_text("$dir/nodeset", "#!/bin/sh\nexit 0\n"); + write_text("$dir/rpower", "#!/bin/sh\nexit 0\n"); + write_text("$dir/makeknownhosts", "#!/bin/sh\nexit 0\n"); + write_text("$dir/lsdef", "#!/bin/sh\nprintf 'xcat71-cn: status=%s\\n' " . shell_quote($opt{status}) . "\n"); + write_text("$dir/xdsh", "#!/bin/sh\nfor a in \"\$@\"; do\n case \"\$a\" in\n */cmdline|/proc/cmdline) printf '%s\\n' 'destiny=shell'; exit 0;;\n esac\ndone\nprintf '%s\\n' '[xCAT Genesis running on node]'\n"); + chmod 0755, map { "$dir/$_" } qw(nodeset rpower makeknownhosts lsdef xdsh); + local $ENV{PATH} = "$dir:$ENV{PATH}"; + return GenesisTest::run_nodeset_shell_test(); +} + +#--- +# run_testxdsh: drive testxdsh with makeknownhosts and xdsh shadowed. xdsh is asked twice -- +# once for the prompt, once for the file -- and the stub answers both from its arguments. +#--- +sub run_testxdsh { + my ($value, %opt) = @_; + my $dir = tempdir(DIR => $tmpdir, CLEANUP => 1); + my $log = "$dir/makeknownhosts.log"; + write_text("$dir/makeknownhosts", "#!/bin/sh\necho \"\$@\" >> '$log'\n"); + my $prompt = $opt{genesis_prompt} ? '[xCAT Genesis running on node]' : 'sh-5.1'; + write_text("$dir/xdsh", "#!/bin/sh\nfor a in \"\$@\"; do\n case \"\$a\" in\n */cmdline|/proc/cmdline) printf '%s\\n' '$opt{cmdline}'; exit 0;;\n esac\ndone\nprintf '%s\\n' '$prompt'\n"); + chmod 0755, "$dir/makeknownhosts", "$dir/xdsh"; + local $ENV{PATH} = "$dir:$ENV{PATH}"; + my $status = GenesisTest::testxdsh($value); + return { status => $status, makeknownhosts => (-f $log ? read_text($log) : '') }; +} + +#--- +# eval_subs: lift named subs out of the script and compile them into a scratch package, so +# they can be run without a management node. Bails out when a sub stops being extractable. +#--- +sub eval_subs { + my ($text, @names) = @_; + my $code = "package GenesisTest;\nno strict;\nno warnings;\nour \$os;\nour \$check_genesis_file;\nour \$noderange;\n"; + # The waits are minutes long. Neuter sleep so the extracted routines run at test speed. + $code .= "use subs qw(sleep);\nsub sleep { \$GenesisTest::SLEPT += (\$_[0] || 0); return 1; }\n"; + $code .= "sub send_msg { push \@GenesisTest::MSG, \$_[1]; return 0; }\n"; + foreach my $name (@names) { + my ($body) = $text =~ /^(sub \Q$name\E \{.*?^\})$/ms; + die("sub $name() not found in $helper") unless defined $body; + $code .= "$body\n"; + } + $code .= "1;\n"; + eval $code or die("cannot compile the extracted helpers: $@"); +} + +#--- +# waiter_name: the name the script gives its destiny status check. +#--- +sub waiter_name { + my ($text) = @_; + foreach my $name (qw(wait_for_node_status wait_for_boot)) { + return $name if $text =~ /^sub \Q$name\E \{/m; + } + die("no destiny status check found in $helper"); +} + +#--- +# os_for: run get_os with `cat` shadowed so it reads the release text under test. +#--- +sub os_for { + my ($release) = @_; + local $ENV{PATH} = stub_bin(cat => "#!/bin/sh\nprintf '%s' " . shell_quote($release)) . ":$ENV{PATH}"; + return GenesisTest::get_os(); +} + +#--- +# rpm_check: run check_genesis_file with `rpm` shadowed so `rpm -qa` lists the given packages. +#--- +sub rpm_check { + my ($installed) = @_; + local $ENV{PATH} = stub_bin(rpm => "#!/bin/sh\nprintf '%s' " . shell_quote($installed)) . ":$ENV{PATH}"; + return GenesisTest::check_genesis_file('x86_64'); +} + +sub report_files { + my ($installed) = @_; + local $ENV{PATH} = stub_bin(rpm => "#!/bin/sh\nprintf '%s' " . shell_quote($installed)) . ":$ENV{PATH}"; + return GenesisTest::report_genesis_files('x86_64'); +} + +#--- +# shell_quote: single-quote a string for /bin/sh. +#--- +sub shell_quote { + my ($v) = @_; + $v =~ s/'/'\\''/g; + return "'$v'"; +} + +#--- +# stub_bin: a directory holding one shadow command, ahead of the real one on PATH. +#--- +sub stub_bin { + my (%cmd) = @_; + my $dir = tempdir(DIR => $tmpdir, CLEANUP => 1); + while (my ($name, $body) = each %cmd) { + write_text("$dir/$name", $body); + chmod 0755, "$dir/$name"; + } + return $dir; +} diff --git a/xCAT-test/unit/kvm_createstorage_model.t b/xCAT-test/unit/kvm_createstorage_model.t new file mode 100644 index 000000000..7128f151e --- /dev/null +++ b/xCAT-test/unit/kvm_createstorage_model.t @@ -0,0 +1,123 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# The scratch package below declares these; the test names them once each. +no warnings 'once'; + +my $source = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/kvm.pm"; +open(my $source_fh, '<', $source) or die "open $source: $!"; +my $content = do { local $/; <$source_fh> }; +close($source_fh) or die "close $source: $!"; + +my @routines; +for my $name (qw(createstorage build_diskstruct guest_arch_profile getUnits + default_storagemodel)) { + my ($routine) = $content =~ /^(sub \Q$name\E\s*\{.*?^\})/ms; + die("could not extract $name from kvm.pm") unless $routine; + push(@routines, $routine); +} + +# kvm.pm needs a management node to load, so createstorage runs in a scratch package. +# get_filepath_by_url is the routine that reaches libvirt; it records the device name it is +# asked for, which is the name createstorage gives the volume of the node. +my $harness = <<'PERL'; +package KVMStore; +our ($node, $confdata, $clonemethod, @asked); +sub getstorageformat { my ($cfginfo) = @_; return $cfginfo->{storageformat}; } +sub get_filepath_by_url { my %args = @_; push(@asked, $args{dev}); return $args{dev}; } +sub oldCreateStorage { push(@asked, 'oldCreateStorage'); } +sub get_multiple_paths_by_url { return {}; } +PERL + +eval $harness . join("\n", @routines) . "\n1;\n"; ## no critic (BuiltinFunctions::ProhibitStringyEval) +die("could not load the kvm storage routines: $@") if $@; + +# The name createstorage gives the volume of one node. $stale is a capture left live in this +# block by an earlier successful match, which is the state createstorage runs in when a +# routine on the call path matched a pattern that has a group. +sub volume_dev { + my (%args) = @_; + my $storage = $args{storage} // 'dir:///var/lib/libvirt/images/'; + my $cfginfo = { + node => 'cn1', + host => 'hyp1', + storage => $storage, + storagemodel => $args{storagemodel}, + }; + @KVMStore::asked = (); + # The match must run in this block, and nothing may match after it: perl restores $1 when + # the block that set it ends, and any later successful match replaces what it holds. + my $subject = 'left by an earlier match: ' . ($args{stale} // ''); + $subject =~ /match: (.*)/ if defined $args{stale}; + # A match without a group empties $1, which is the clean state the other cases need. + $subject =~ /^left/ unless defined $args{stale}; + KVMStore::createstorage($storage, undef, '30G', $cfginfo, 1); + return $KVMStore::asked[0]; +} + +# dohyp sets storagemodel to scsi for every node it dispatches, whatever the architecture, +# before mkvm reaches createstorage. That default is what names the volume of a node whose +# vmstoragemodel is empty, and a riscv64 node depends on it: the riscv64 virt machine has no +# IDE controller, so its volume must be sd*. +is(volume_dev(storagemodel => 'scsi'), 'sda', + 'the scsi storage model names an sd* volume'); + +# A capture from a match made elsewhere must not name the volume. These are the values a +# routine on the mkvm call path can leave in $1. +is(volume_dev(storagemodel => 'scsi', stale => '/var/lib/libvirt/images/'), 'sda', + 'a path left by an earlier match does not name the volume'); +is(volume_dev(storagemodel => 'scsi', stale => 'virtio'), 'sda', + 'a model name left by an earlier match does not name the volume'); +is(volume_dev(storagemodel => 'virtio', stale => 'scsi'), 'vda', + 'an earlier match does not override vmstoragemodel either'); + +# The model stated on the vmstorage value, and vmstoragemodel, still name the volume. +is(volume_dev(storage => 'dir:///var/lib/libvirt/images/=scsi'), 'sda', + 'a model on the vmstorage value names an sd* volume'); +is(volume_dev(storagemodel => 'virtio'), 'vda', + 'vmstoragemodel=virtio names a vd* volume'); + +# createstorage on its own defaults to ide. Nothing in the product reaches this today: dohyp +# gives every node the default storage model first. +is(volume_dev(), 'hda', 'createstorage alone defaults to an hd* volume'); + +# A node with no vmstoragemodel takes its sd* name from that default, so the two are driven +# together. +is(volume_dev(storagemodel => KVMStore::default_storagemodel()), 'sda', + 'the default storage model names an sd* volume'); + +# build_diskstruct reads $1 the same way, for a disk backed by a plain file. The device name +# and the bus of that disk must come from the node, not from a match made elsewhere. +sub file_disk { + my (%args) = @_; + local $KVMStore::node = 'cn1'; + local $KVMStore::confdata = { + vm => { cn1 => [ { host => 'hyp1', storage => '/var/lib/libvirt/images/cn1.img' } ] }, + nodetype => { cn1 => [ { arch => $args{arch} } ] }, + hyp1 => { cpumodel => 'x86_64' }, + }; + my $chatter = ''; + my $disks; + my $subject = 'left by an earlier match: ' . ($args{stale} // ''); + $subject =~ /match: (.*)/ if defined $args{stale}; + $subject =~ /^left/ unless defined $args{stale}; + { + open(my $capture, '>', \$chatter) or die "capture stdout: $!"; + local *STDOUT = $capture; + ($disks) = KVMStore::build_diskstruct(undef); + } + return $disks->[0]; +} + +is(file_disk(arch => 'x86_64')->{target}->{bus}, 'ide', + 'a file-backed disk of an x86_64 node is ide'); +is(file_disk(arch => 'x86_64', stale => 'virtio')->{target}->{bus}, 'ide', + 'a model name left by an earlier match does not choose the bus of a file-backed disk'); +is(file_disk(arch => 'riscv64', stale => 'ide')->{target}->{dev}, 'sda', + 'a riscv64 file-backed disk keeps its sd* name whatever an earlier match left behind'); + +done_testing(); diff --git a/xCAT-test/unit/kvm_diskstruct_bus.t b/xCAT-test/unit/kvm_diskstruct_bus.t new file mode 100644 index 000000000..a02c1c2b3 --- /dev/null +++ b/xCAT-test/unit/kvm_diskstruct_bus.t @@ -0,0 +1,90 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# The scratch package below declares these; the test names them once each. +no warnings 'once'; + +my $source = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/kvm.pm"; +open(my $source_fh, '<', $source) or die "open $source: $!"; +my $content = do { local $/; <$source_fh> }; +close($source_fh) or die "close $source: $!"; + +my @routines; +for my $name (qw(build_diskstruct guest_arch_profile getUnits)) { + my ($routine) = $content =~ /^(sub \Q$name\E\s*\{.*?^\})/ms; + die("could not extract $name from kvm.pm") unless $routine; + push(@routines, $routine); +} + +# kvm.pm needs a management node to load, so the disk builder runs in a scratch package. +# get_multiple_paths_by_url is the only routine it calls that reaches libvirt; it answers +# from $pool, which holds what a storage pool reports for one node. +my $harness = <<'PERL'; +package KVMDisk; +our ($node, $confdata, $pool); +sub get_multiple_paths_by_url { return $pool; } +PERL + +eval $harness . join("\n", @routines) . "\n1;\n"; ## no critic (BuiltinFunctions::ProhibitStringyEval) +die("could not load the kvm disk builder: $@") if $@; + +# Build the disks of a node of $arch whose vmstorage is a libvirt pool holding the volumes +# in $pool: a path => { device, format } map, the shape get_multiple_paths_by_url returns. +sub pool_disks { + my ($arch, $pool) = @_; + local $KVMDisk::node = 'cn1'; + local $KVMDisk::confdata = { + vm => { cn1 => [ { + host => 'hyp1', + storage => 'dir:///var/lib/libvirt/images/', + storagecache => 'writeback', + } ] }, + nodetype => { cn1 => [ { arch => $arch } ] }, + hyp1 => { cpumodel => 'x86_64' }, + }; + local $KVMDisk::pool = $pool; + my $chatter = ''; + my $disks; + { + open(my $capture, '>', \$chatter) or die "capture stdout: $!"; + local *STDOUT = $capture; + ($disks) = KVMDisk::build_diskstruct(undef); + } + die('build_diskstruct returned no disks') unless ref $disks eq 'ARRAY'; + return $disks; +} + +# One volume in the pool, named ... The disk is the first element; +# the optical drive build_diskstruct always appends is the second. +sub pool_disk { + my ($arch, $device) = @_; + my $path = "/var/lib/libvirt/images/cn1.$device.qcow2"; + return pool_disks($arch, { $path => { device => $device, format => 'qcow2' } })->[0]; +} + +# A disk on a libvirt storage pool states the bus of the device name it is given. libvirt +# reads the same names the same way: hd* is ide, sd* is scsi, vd* is virtio. +is(pool_disk('x86_64', 'hda')->{target}->{bus}, 'ide', + 'an hd* disk on a storage pool is ide'); +is(pool_disk('x86_64', 'sda')->{target}->{bus}, 'scsi', + 'an sd* disk on a storage pool is scsi'); +is(pool_disk('x86_64', 'vda')->{target}->{bus}, 'virtio', + 'a vd* disk on a storage pool is virtio'); + +# The device name is the name of the volume in the pool, and stays it. The riscv64 virt +# machine has no IDE controller, so a riscv64 node depends on that name being sd*. +my $riscv = pool_disk('riscv64', 'sda'); +is($riscv->{target}->{dev}, 'sda', 'a riscv64 pool disk keeps the sd* name of its volume'); +is($riscv->{target}->{bus}, 'scsi', 'a riscv64 pool disk is scsi, not ide'); + +my $riscv_all = pool_disks('riscv64', + { '/var/lib/libvirt/images/cn1.sda.qcow2' => { device => 'sda', format => 'qcow2' } }); +is($riscv_all->[1]->{device}, 'cdrom', 'the riscv64 guest still gets an optical drive'); +like($riscv_all->[1]->{target}->{dev}, qr/^sd/, + 'the riscv64 optical drive is named sd*, not hd*'); + +done_testing(); diff --git a/xCAT-test/unit/kvm_guest_arch.t b/xCAT-test/unit/kvm_guest_arch.t new file mode 100644 index 000000000..34ba7649a --- /dev/null +++ b/xCAT-test/unit/kvm_guest_arch.t @@ -0,0 +1,122 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# The scratch package below declares these; the test names them once each. +no warnings 'once'; + +my $source = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/kvm.pm"; +open(my $source_fh, '<', $source) or die "open $source: $!"; +my $content = do { local $/; <$source_fh> }; +close($source_fh) or die "close $source: $!"; + +my @routines; +for my $name (qw(build_xmldesc guest_arch_profile build_oshash build_diskstruct getUnits)) { + my ($routine) = $content =~ /^(sub \Q$name\E\s*\{.*?^\})/ms; + die("could not extract $name from kvm.pm") unless $routine; + push(@routines, $routine); +} + +# kvm.pm needs a management node to load, so the domain builder runs in a scratch package. +# Only the routines that reach libvirt or the xCAT database are replaced; the domain builder +# itself is the code under test. +my $harness = <<'PERL'; +package KVMArch; +use XML::Simple qw(XMLout); +our ($node, $confdata, $updatetable, $hypconn); +sub getNodeUUID { return '00000000-0000-0000-0000-000000000001'; } +sub get_multiple_paths_by_url { return {}; } +sub build_nicstruct { return []; } +sub genpassword { return 'password'; } +PERL + +eval $harness . join("\n", @routines) . "\n1;\n"; ## no critic (BuiltinFunctions::ProhibitStringyEval) +die("could not load the kvm domain builder: $@") if $@; + +# Build one domain for a node of $guest_arch on a hypervisor that reports $hyp_cpumodel. +sub domain_xml { + my ($guest_arch, $hyp_cpumodel) = @_; + local $KVMArch::node = 'cn1'; + local $KVMArch::confdata = { + vm => { cn1 => [ { host => 'hyp1', memory => 8192, cpus => 4 } ] }, + nodetype => { cn1 => [ { arch => $guest_arch, os => 'rocky10.2' } ] }, + hyp1 => { cpumodel => $hyp_cpumodel }, + }; + local $KVMArch::updatetable = {}; + my $xml = KVMArch::build_xmldesc('cn1'); + die("build_xmldesc returned no XML for $guest_arch on $hyp_cpumodel") + unless defined $xml and !ref $xml; + return $xml; +} + +sub os_type_element { + my ($xml) = @_; + my ($attrs) = $xml =~ m{]*)>hvm}s; + return defined $attrs ? $attrs : ''; +} + +# A riscv64 node on an x86_64 hypervisor. The guest architecture is not the host +# architecture, so the domain runs under emulation and states its own machine type. +my $riscv = domain_xml('riscv64', 'x86_64'); +like($riscv, qr/]*\btype="qemu"/, + 'a riscv64 guest on an x86_64 hypervisor is a qemu domain, not kvm'); +like(os_type_element($riscv), qr/\barch="riscv64"/, + 'the domain arch is the arch of the node'); +like(os_type_element($riscv), qr/\bmachine="virt"/, + 'a riscv64 guest uses the virt machine type'); +like($riscv, qr/]*\bfirmware="efi"/, + 'a riscv64 virt guest boots UEFI'); +unlike($riscv, qr/<(?:pae|acpi|apic)\b/, + 'pae, acpi and apic are x86 features and are left out of a riscv64 guest'); +unlike($riscv, qr/]*\btype="kvm"/, 'a POWER guest stays a kvm domain'); +like(os_type_element($power), qr/\barch="ppc64"/, 'ppc64le hypervisors keep arch ppc64'); +like(os_type_element($power), qr/\bmachine="pseries"/, 'ppc64le hypervisors keep machine pseries'); + +# x86_64 on x86_64 is unchanged: libvirt picks the arch and the machine type. +my $x86 = domain_xml('x86_64', 'x86_64'); +like($x86, qr/]*\btype="kvm"/, 'an x86_64 guest stays a kvm domain'); +unlike(os_type_element($x86), qr/\barch=/, 'an x86_64 guest states no arch'); +unlike(os_type_element($x86), qr/\bmachine=/, 'an x86_64 guest states no machine type'); +like($x86, qr/]*\bbus="usb"/, 'an x86_64 guest keeps the USB tablet'); + +# The disks of a riscv64 guest. The virt machine has no IDE controller, so an ide disk or an +# hd* optical drive makes libvirt refuse the domain. +sub disk_struct { + my ($guest_arch) = @_; + local $KVMArch::node = 'cn1'; + local $KVMArch::confdata = { + vm => { cn1 => [ { host => 'hyp1', storage => '/var/lib/libvirt/images/cn1.img' } ] }, + nodetype => { cn1 => [ { arch => $guest_arch } ] }, + hyp1 => { cpumodel => 'x86_64' }, + }; + my $chatter = ''; + my $disks; + { + open(my $capture, '>', \\$chatter) or die "capture stdout: $!"; + local *STDOUT = $capture; + ($disks) = KVMArch::build_diskstruct(undef); + } + return $disks; +} + +my $riscv_disks = disk_struct('riscv64'); +is($riscv_disks->[0]->{target}->{bus}, 'scsi', 'a riscv64 disk is scsi, not ide'); +like($riscv_disks->[0]->{target}->{dev}, qr/^sd/, 'a riscv64 disk is named sd*'); +is($riscv_disks->[1]->{device}, 'cdrom', 'the guest still gets an optical drive'); +like($riscv_disks->[1]->{target}->{dev}, qr/^sd/, 'a riscv64 optical drive is named sd*, not hd*'); + +my $x86_disks = disk_struct('x86_64'); +is($x86_disks->[0]->{target}->{bus}, 'ide', 'an x86_64 disk keeps the ide default'); +like($x86_disks->[1]->{target}->{dev}, qr/^hd/, 'an x86_64 optical drive keeps the hd* name'); + +done_testing(); diff --git a/xCAT-test/unit/mknb_genesis_staging.t b/xCAT-test/unit/mknb_genesis_staging.t new file mode 100644 index 000000000..81fb7ae77 --- /dev/null +++ b/xCAT-test/unit/mknb_genesis_staging.t @@ -0,0 +1,62 @@ +#!/usr/bin/env perl +# mknb stages the Genesis payload before it can build a netboot image. Those copies are the only +# point at which mknb learns that an installed Genesis image is unusable. +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use Test::More; + +BEGIN { $INC{'xCAT/Utils.pm'} = 1; $INC{'xCAT/MsgUtils.pm'} = 1; + $INC{'xCAT/Table.pm'} = 1; $INC{'xCAT/NetworkUtils.pm'} = 1; + $INC{'xCAT/TableUtils.pm'} = 1; $INC{'xCAT_monitoring/monitorctrl.pm'} = 1; } + +require "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/mknb.pm"; + +can_ok('xCAT_plugin::mknb', 'stage_genesis_payload') + or die('mknb has no stage_genesis_payload to drive'); + +# Drive the routine with a runner that fails exactly one copy, so each assertion names the +# copy it is about rather than the pair. +sub stage { + my (%opt) = @_; + my @ran; + my ($rc, $src) = xCAT_plugin::mknb::stage_genesis_payload( + genesis_type => $opt{type} // 'legacy', + genesis_dir => '/opt/xcat/share/xcat/netboot/genesis/x86_64', + tftpdir => '/tftpboot', + arch => 'x86_64', + tempdir => '/tmp/scratch', + run => sub { + my ($cmd) = @_; + push @ran, $cmd; + return ($opt{fail} && $cmd =~ /$opt{fail}/) ? 256 : 0; + }, + ); + return { rc => $rc, src => $src, ran => \@ran }; +} + +# --- legacy: both copies must be able to fail the step ----------------------- +my $ok = stage(); +is($ok->{rc}, 0, 'a legacy image whose copies both succeed stages cleanly'); +is(scalar @{ $ok->{ran} }, 2, 'the legacy path copies the root tree and the kernel'); + +my $nofs = stage(fail => qr{/fs/\*}); +isnt($nofs->{rc}, 0, 'an unreadable root tree fails the step'); +like($nofs->{src}, qr{/fs$}, 'and the failure names the root tree'); + +my $nokernel = stage(fail => qr{/kernel }); +isnt($nokernel->{rc}, 0, 'a missing kernel fails the step'); +like($nokernel->{src}, qr{/kernel$}, 'and the failure names the kernel, not the root tree'); + +# --- exported (OpenEmbedded) path ------------------------------------------- +my $nonb = stage(type => 'exported', fail => qr{/nbroot/\*}); +isnt($nonb->{rc}, 0, 'an unreadable nbroot fails the step'); +like($nonb->{src}, qr{/nbroot$}, 'and the failure names nbroot'); + +my $oknb = stage(type => 'exported'); +is($oknb->{rc}, 0, 'an exported image whose copy succeeds stages cleanly'); + +done_testing(); diff --git a/xCAT-test/unit/xcat_probe_package_payload.t b/xCAT-test/unit/xcat_probe_package_payload.t index 26659bcdf..bbd363e6c 100644 --- a/xCAT-test/unit/xcat_probe_package_payload.t +++ b/xCAT-test/unit/xcat_probe_package_payload.t @@ -12,7 +12,7 @@ use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/../../build-utils/lib"; use Test::More; -use XCAT::BuildUtils qw(XCAT_PROBE_HELPERS); +use XCAT::BuildUtils qw(XCAT_PROBE_HELPERS stage_probe_helpers); use XCAT::Test::File qw(repo_path slurp_repo_file); my @helpers = qw( @@ -29,7 +29,6 @@ my @affected_subcommands = qw( ); my $builder = slurp_repo_file('buildrpms.pl'); -my $debian_builder = slurp_repo_file('build-ubunturepo'); my $installed_probe_test = slurp_repo_file('xCAT-test/autotest/testcase/probe/xcatproble_list'); my $rpm_spec = slurp_repo_file('xCAT-probe/xCAT-probe.spec'); @@ -67,6 +66,12 @@ like( 'Debian package requires ss or the legacy netstat provider' ); +# The Debian builder stages the helpers by calling stage_probe_helpers, so run it and +# look at what it produced. The predecessor matched a `cp -f` line in build-ubunturepo, +# which passed whenever that text was reformatted and failed whenever it moved. +my $staged_probe_dir = File::Spec->catdir(tempdir(CLEANUP => 1), 'lib', 'perl', 'xCAT'); +stage_probe_helpers(repo_path(File::Spec->catdir('perl-xCAT', 'xCAT')), $staged_probe_dir); + for my $helper (@helpers) { my $source = repo_path(File::Spec->catfile('perl-xCAT', 'xCAT', $helper)); ok(-f $source, "$helper source exists"); @@ -75,9 +80,8 @@ for my $helper (@helpers) { scalar(grep { $_ eq $helper } XCAT_PROBE_HELPERS), "the shared builder helper list carries $helper" ); - like( - $debian_builder, - qr{cp -f [^\n]*/perl-xCAT/xCAT/\Q$helper\E\s+[^\n]*/lib/perl/xCAT/}, + ok( + -f File::Spec->catfile($staged_probe_dir, $helper), "Debian builder stages $helper" ); like( @@ -144,7 +148,7 @@ sub copy_tree { my ($source, $destination) = @_; my $rc = system('cp', '-R', $source, $destination); is($rc, 0, "copied $source into the package fixture") - or BAIL_OUT("unable to create package fixture from $source"); + or die("unable to create package fixture from $source"); } sub run_command { diff --git a/xCAT-test/unit/xcat_riscv64_genesis_dependency.t b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t index 4a1930c4a..67bd0c774 100644 --- a/xCAT-test/unit/xcat_riscv64_genesis_dependency.t +++ b/xCAT-test/unit/xcat_riscv64_genesis_dependency.t @@ -12,8 +12,8 @@ use Test::More; # # The deb side named xcat-genesis-scripts-amd64 in a plain Depends, and that package is # Architecture: all, so apt installed the x86 Genesis scripts (and, through them, the x86 Genesis -# base) on a riscv64 management node. Restrict the dependency to the architectures that have a -# legacy Genesis, and leave amd64 and ppc64el untouched. +# base) on every management node that is not amd64. Name one scripts package per architecture that +# has a legacy Genesis, so riscv64 gets none and ppc64el gets its own. my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) @@ -37,10 +37,12 @@ foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { my ($recommends) = $control =~ /^Recommends:\s*(.*)$/m; ok( defined $recommends, "$name debian/control has a Recommends line" ); - my ($entry) = grep { /xcat-genesis-scripts/ } split( /\s*,\s*/, $depends ); - ok( defined $entry, "$name depends on a legacy Genesis scripts package" ); - like( $entry, qr/\[!riscv64\]/, - "$name excludes riscv64 from the legacy Genesis scripts dependency" ); + my @entries = grep { /xcat-genesis-scripts/ } split( /\s*,\s*/, $depends ); + ok( scalar(@entries), "$name depends on a legacy Genesis scripts package" ); + my @unqualified = grep { !/\[(?:amd64|ppc64el)\]\s*$/ } @entries; + is_deeply( \@unqualified, [], + "$name asks for the legacy Genesis scripts of an architecture that has them" ) + or diag( "unqualified: @unqualified" ); SKIP: { skip( "Dpkg::Deps is not available", 7 ) unless $have_dpkg_deps; @@ -56,8 +58,8 @@ foreach my $pkg ( [ 'xCAT', 'xcat' ], [ 'xCATsn', 'xcatsn' ] ) { "$name on riscv64 does not pull the legacy Genesis scripts" ); like( $reduced{amd64}, qr/xcat-genesis-scripts-amd64/, "$name on amd64 still pulls them" ); - like( $reduced{ppc64el}, qr/xcat-genesis-scripts-amd64/, - "$name on ppc64el still pulls them" ); + like( $reduced{ppc64el}, qr/xcat-genesis-scripts-ppc64el/, + "$name on ppc64el pulls the ppc64el ones" ); # The restriction must not take anything else with it: every other dependency of the # amd64 package must survive on riscv64. diff --git a/xCAT-test/unit/xcattest_report_every_check.t b/xCAT-test/unit/xcattest_report_every_check.t new file mode 100644 index 000000000..2dd3e9824 --- /dev/null +++ b/xCAT-test/unit/xcattest_report_every_check.t @@ -0,0 +1,142 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Copy qw(copy); +use File::Path qw(make_path); +use File::Temp qw(tempdir); +use Test::More; + +my $program = "$FindBin::Bin/../xcattest"; +die("xcattest is not at $program") unless -f $program; + +#--- +=head3 run_harness + + Descriptions: Run xcattest over one fixture case file and return its log lines. + Arguments: + $case_text - the content of the fixture case file + @names - the case names to run + Returns: a reference to the array of log lines, and the failed-cases report lines +=cut + +#--- +sub run_harness { + my ($case_text, @names) = @_; + + # xcattest derives its result directory from the location of the program, so the copy + # under the scratch tree keeps every file the run writes inside that tree. + my $root = tempdir(CLEANUP => 1); + make_path("$root/bin", "$root/cases"); + copy($program, "$root/bin/xcattest") or die("copy xcattest: $!"); + chmod 0755, "$root/bin/xcattest"; + + open(my $case_fh, '>', "$root/cases/fixture") or die("write the fixture case: $!"); + print $case_fh $case_text; + close($case_fh) or die("close the fixture case: $!"); + + local $ENV{XCATTEST_CASEDIR} = "$root/cases"; + system($^X, "$root/bin/xcattest", '-q', '-t', join(',', @names)); + + my $slurp = sub { + my ($path) = @_; + open(my $fh, '<', $path) or die("open $path: $!"); + my @lines = <$fh>; + close($fh) or die("close $path: $!"); + chomp(@lines); + return @lines; + }; + + my ($log) = glob("$root/share/xcat/tools/autotest/result/xcattest.log.*"); + die("the harness wrote no running log under $root") unless $log; + my ($failed) = glob("$root/share/xcat/tools/autotest/result/failedcases.*"); + die("the harness wrote no failed-cases report under $root") unless $failed; + + return ([ $slurp->($log) ], [ $slurp->($failed) ]); +} + +#--- +=head3 reported_checks + + Descriptions: Select the check results the harness reported. + Arguments: + $lines - a reference to the array of log lines + Returns: a reference to the array of CHECK lines, in the order they were reported +=cut + +#--- +sub reported_checks { + my ($lines) = @_; + return [ grep { /^CHECK:/ } @{$lines} ]; +} + +# The second command fails its first check. The check after it on the same command, and the +# checks of every command after it, describe the same run and must report their own result. +my $mixed = <<'CASE'; +start:mixedchecks +description:a failed check between checks that pass +cmd:echo alpha +check:rc==0 +cmd:echo beta +check:rc!=0 +check:output=~beta +cmd:echo gamma +check:output=~gamma +end +CASE + +my ($log, $failed) = run_harness($mixed, 'mixedchecks'); + +is_deeply(reported_checks($log), + [ "CHECK:rc == 0\t[Pass]", + "CHECK:rc != 0\t[Failed]", + "CHECK:output =~ beta\t[Pass]", + "CHECK:output =~ gamma\t[Pass]" ], + 'every check reports its own result, and a failed check does not silence the checks after it'); + +is_deeply(reported_checks($failed), reported_checks($log), + 'the failed-cases report carries the same check results as the running log'); + +ok(scalar(grep { /^------END::mixedchecks::Failed::/ } @{$log}), + 'a check that passes after a failed check does not make the case pass'); + +# A case that fails more than one check names every one of them. +my $twofails = <<'CASE'; +start:twofailedchecks +description:two commands, each with a check that fails +cmd:echo one +check:rc!=0 +cmd:echo two +check:rc!=0 +end +CASE + +($log, $failed) = run_harness($twofails, 'twofailedchecks'); + +is_deeply(reported_checks($log), + [ "CHECK:rc != 0\t[Failed]", "CHECK:rc != 0\t[Failed]" ], + 'both failed checks are reported, not just the first'); + +# A case where every check passes is unchanged. +my $allpass = <<'CASE'; +start:allcheckspass +description:every check passes +cmd:echo alpha +check:rc==0 +check:output=~alpha +cmd:echo beta +check:output=~beta +end +CASE + +($log, $failed) = run_harness($allpass, 'allcheckspass'); + +is_deeply(reported_checks($log), + [ "CHECK:rc == 0\t[Pass]", "CHECK:output =~ alpha\t[Pass]", "CHECK:output =~ beta\t[Pass]" ], + 'a case whose checks all pass reports every check'); + +ok(scalar(grep { /^------END::allcheckspass::Passed::/ } @{$log}), + 'a case whose checks all pass still reports Passed'); + +done_testing(); diff --git a/xCAT-test/xcattest b/xCAT-test/xcattest index bc89b4532..25eaf9d6c 100755 --- a/xCAT-test/xcattest +++ b/xCAT-test/xcattest @@ -1417,8 +1417,11 @@ sub run_case { log_this($running_log_fd, ("ElapsedTime:$diffduration sec", "RETURN rc = $rc", "OUTPUT:", @output)); push(@caselog, ("ElapsedTime:$diffduration sec", "RETURN rc = $rc", "OUTPUT:", @output)); + # $checkfail is the result of this check, $failflag the result of the case. One + # variable for both makes a failed check read as failing every later check. + my $checkfail = 0; foreach my $check (@{ $cases_ref->[ $case_name_index_map_ref->{$case} ]->{check}->[$j] }) { - last if ($failflag); + $checkfail = 0; if ($check =~ /rc\s*([=!]+)\s*(\d+)/) { my $lvalue = $rc; @@ -1426,12 +1429,11 @@ sub run_case { my $rvalue = $2; if ((($op eq '!=') && ($lvalue == $rvalue)) || (($op eq '==') && ($lvalue != $rvalue))) { - $failflag = 1; + $checkfail = 1; } - if ($failflag) { + if ($checkfail) { log_this($running_log_fd, "CHECK:rc $op $rvalue\t[Failed]"); push(@caselog, "CHECK:rc $op $rvalue\t[Failed]"); - last; } else { log_this($running_log_fd, "CHECK:rc $op $rvalue\t[Pass]"); push(@caselog, "CHECK:rc $op $rvalue\t[Pass]"); @@ -1446,17 +1448,16 @@ sub run_case { || (($op eq '!~') && ($lvalue =~ /$rvalue/)) || (($op eq '==') && ($lvalue ne $rvalue)) || (($op eq '!=') && ($lvalue eq $rvalue))) { - $failflag = 1; + $checkfail = 1; } elsif (($op ne '=~') && ($op ne '!~') && ($op ne '==') && ($op ne '!=')) { - $failflag = 1; + $checkfail = 1; log_this($running_log_fd, "CHECK:output unrecognized operator: $op\t[Failed]"); push(@caselog, "CHECK:output unrecognized operator: $op\t[Failed]"); - last; + next; } - if ($failflag) { + if ($checkfail) { log_this($running_log_fd, "CHECK:output $op $rvalue\t[Failed]"); push(@caselog, "CHECK:output $op $rvalue\t[Failed]"); - last; } else { log_this($running_log_fd, "CHECK:output $op $rvalue\t[Pass]"); push(@caselog, "CHECK:output $op $rvalue\t[Pass]"); @@ -1464,7 +1465,7 @@ sub run_case { } elsif ($check =~ /output\s*~~\s*(\S.*)/) { my $op = "~~"; - #my $failflag = 1; + # This operator only sets $checkfail to 0, so the check always reports Pass. my $rvalue = $1; $rvalue = getfunc($rvalue); @@ -1481,7 +1482,7 @@ sub run_case { my $min = $num * 0.9; $line =~ /.*:.*: (\d+) /; if ($1 < $max && $1 > $min) { - $failflag = 0; + $checkfail = 0; last; } } else { @@ -1489,19 +1490,20 @@ sub run_case { } } } - if ($failflag) { + if ($checkfail) { log_this($running_log_fd, "CHECK:output $op $rvalue\t[Failed]"); push(@caselog, "CHECK:output $op $rvalue\t[Failed]"); - last; } else { log_this($running_log_fd, "CHECK:output $op $rvalue\t[Pass]"); push(@caselog, "CHECK:output $op $rvalue\t[Pass]"); } } else { - $failflag = 1; + $checkfail = 1; log_this($running_log_fd, "Unrecognized testcase syntax: CHECK:$check\t[Failed]"); push(@caselog, "Unrecognized testcase syntax: CHECK:$check\t[Failed]"); } + } continue { + $failflag = 1 if ($checkfail); } foreach my $cmdcheck (@{ $cases_ref->[ $case_name_index_map_ref->{$case} ]->{cmdcheck}->[$j] }) { if ($cmdcheck) { diff --git a/xCAT/debian/control b/xCAT/debian/control index 0037da394..edda1c42b 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -9,7 +9,7 @@ Homepage: https://xcat.org/ Package: xcat Architecture: amd64 ppc64el riscv64 -Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] +Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [amd64], xcat-genesis-scripts-ppc64el (>= 2.13-snap000000000000) [ppc64el] Recommends: net-tools, kea, tftp-hpa, syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT setup diff --git a/xCATsn/debian/control b/xCATsn/debian/control index 460081543..d4e508f5c 100644 --- a/xCATsn/debian/control +++ b/xCATsn/debian/control @@ -8,7 +8,7 @@ Homepage: https://xcat.org/ Package: xcatsn Architecture: amd64 ppc64el riscv64 -Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [!riscv64] +Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, nmap, ipmitool-xcat (>= 1.8.18-4), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) [amd64], xcat-genesis-scripts-ppc64el (>= 2.13-snap000000000000) [ppc64el] Recommends: net-tools, kea, tftp-hpa, syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64, xcat-genesis-openembedded-s390x Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT service node setup