2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-25 17:24:07 +00:00

Merge master into fix/ubuntu-genesis-native-sbuild

master changed the same three files this branch changes, so the pull request could not
merge. Conflicts and how each was resolved:

xCAT-genesis-builder/builddeb-genesis-base. Both sides teach the build root to cope with
a package whose name moved between releases. master added optional_packages(), which
keeps a package only where apt has a candidate, and called it for util-linux-extra. This
branch added add_first_available(), which takes the first name apt carries and fails when
it carries none, and calls it for bind9-dnsutils/dnsutils and util-linux-extra/util-linux,
plus add_if_available() for tzdata-legacy. The branch covers master's case and two more,
so its helpers are kept and optional_packages() goes with its only caller. Every other
master change to this file, including the DHCP client fix, is preserved.

xCAT-test/unit/genesis_payload_verification.t. master has four assertions this branch does
not: two payloads missing an absolute path. Its version is kept. The branch replaced
plan skip_all with a fail(), because skipping covers nothing when the file under test is
the gate itself, and that change is applied to master's version.

xCAT-test/unit/genesis_ubuntu_build_root.t. master's added assertions drive
optional_packages() directly, which the resolved builder no longer has. This branch's
version matches the implementation that survives, and it already dies rather than skipping
when the builder is missing, so it is kept whole.

prove -j4 -r xCAT-test/unit passes: 212 files, 5892 tests.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-18 07:56:24 -03:00
78 changed files with 3585 additions and 993 deletions
-711
View File
@@ -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=<path> - 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=<filename> - provide an LOG file option to redirect some output into log file
#
# DEST=<directory> - 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=<keyid|email> (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
+6 -3
View File
@@ -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.
#
@@ -117,9 +117,12 @@ my $VERSION = read_line("$ROOT/Version") // die "Cannot read $ROOT/Version\n";
my $EPOCH = source_date_epoch();
# A Release file, when present, is authoritative: buildrpms.pl writes one, and a
# pipeline that builds both must stamp the rpms and the debs with the same release.
# The tracked file holds snap000000000000, which no build writes -- snap_release()
# renders a real time. A tree where buildrpms.pl has not run still carries it, so
# treat the placeholder as an unstamped tree and derive the release from the commit.
my $FILE_RELEASE = do {
my $r = read_line("$ROOT/Release");
($r && $r =~ /\S/) ? $r : undef;
($r && $r =~ /\S/ && $r !~ /\Asnap0+\z/) ? $r : undef;
};
my $RELEASE = $opts{release} || $FILE_RELEASE || snap_release($EPOCH);
my $PKGVER = deb_version($VERSION, $RELEASE);
@@ -554,7 +557,7 @@ xcat-dep's C<sbuild-all.pl> creates on the Ubuntu build host. The build refuses
in a root of another release, and it reads its own log: dracut reports a command it
cannot install with a C<FAILED:> line and still exits 0.
Replaces C<build-ubunturepo>. The GSA upload paths, the C<PROMOTE>/C<PREGA> release
Replaced C<build-ubunturepo>, removed in 2.19. The GSA upload paths, the C<PROMOTE>/C<PREGA> 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.
+3
View File
@@ -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),
@@ -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
--------------------------
@@ -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.
+3 -7
View File
@@ -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/<repo>/<repo> 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";
+7
View File
@@ -66,6 +66,13 @@ sub normalize_algorithm {
return;
}
sub algorithm_rr_type {
my ( $class, $algorithm ) = @_;
$algorithm = $class->normalize_algorithm($algorithm) or return;
return $ALGORITHMS{$algorithm};
}
sub new_install_default_algorithm {
my ( $class, %args ) = @_;
+66 -22
View File
@@ -3909,36 +3909,80 @@ sub fullpathbin
#--------------------------------------------------------------------------------
sub gettimezone
{
my ($class) = @_;
my ($class, %opt) = @_;
my $tz;
if (xCAT::Utils->isAIX()) {
$tz = $ENV{'TZ'};
} else { # all linux
my $localtime = "/etc/localtime";
my $zoneinfo = "/usr/share/zoneinfo";
return $ENV{'TZ'};
}
# all linux. %opt{root} prefixes every path, so a test drives this against a scratch tree.
my $root = defined($opt{root}) ? $opt{root} : '';
my $localtime = "$root/etc/localtime";
my $zoneinfo = "$root/usr/share/zoneinfo";
# On every current distribution /etc/localtime is a symlink into the zoneinfo tree and its
# target is the name. Read it before the scan: the scan compares /etc/localtime against every
# file in the tree, and a cloud image running on UTC may ship no /etc/localtime at all.
if (-l $localtime) {
my $zone = _zone_from_path(readlink($localtime));
return $zone if defined $zone;
}
if (-e $localtime) {
my $cmd = "find $zoneinfo -xtype f -exec cmp -s $localtime {} \\; -print | grep -v posix | grep -v SystemV | grep -v right | grep -v localtime ";
my $zone_result = xCAT::Utils->runcmd("$cmd", 0);
if ($::RUNCMD_RC != 0)
{
$tz = "Could not determine timezone checksum";
return $tz;
if ($::RUNCMD_RC == 0) {
my @zones = split /\n/, $zone_result;
my $zone = _zone_from_path($zones[0]);
return $zone if defined $zone;
}
my @zones = split /\n/, $zone_result;
$zones[0] =~ s/$zoneinfo\///;
if (!$zones[0]) { # if we still did not get one, then default
$tz = `cat /etc/timezone`;
chomp $tz;
} else {
$tz = $zones[0];
}
}
return $tz;
if (open(my $tz_fh, '<', "$root/etc/timezone")) {
my $zone = <$tz_fh>;
close($tz_fh);
if (defined $zone) {
chomp $zone;
return $zone if length($zone) and $zone !~ /\s/;
}
}
# The caller writes this value into a kickstart or an autoyast profile, where it must be one
# token. Name the zone the host is actually on rather than a sentence that stops the installer.
return 'UTC';
}
#--------------------------------------------------------------------------------
=head3 _zone_from_path
Returns the timezone name inside a path under a zoneinfo tree, for a symlink target or a
line of the zoneinfo scan. Both absolute and relative targets carry "/zoneinfo/", so the
name is whatever follows it.
Arguments:
A path, or undef
Returns:
The timezone name, or undef when the path names no zone
Globals:
none
Error:
None
Example:
my $zone = _zone_from_path("../usr/share/zoneinfo/America/Sao_Paulo");
Comments:
none
=cut
#--------------------------------------------------------------------------------
sub _zone_from_path
{
my ($path) = @_;
return undef unless defined($path) and length($path);
return undef unless $path =~ m{(?:^|/)zoneinfo/(.+)\z};
my $zone = $1;
$zone =~ s{^posix/}{};
return undef if $zone eq '' or $zone eq 'localtime' or $zone =~ /\s/;
return $zone;
}
#--------------------------------------------------------------------------------
+31 -8
View File
@@ -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" <<EOF
#!/bin/bash
@@ -48,13 +48,50 @@ install() {
dracut_install mount.nfs sshd vi reboot lspci parted tmux mkfs mkfs.ext4 mkfs.xfs xfs_db
#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
dracut_install dhclient lldpad
# getdestiny makes its request file with mktemp.
dracut_install mktemp
dracut_install lldpad
# RHEL 10 packages no ISC dhcp-client. Install whichever client the build root carries;
# doxcat chooses between them at run time.
if command -v dhclient >/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
@@ -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 &>/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
+34 -4
View File
@@ -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}/"
+3 -1
View File
@@ -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
+4 -4
View File
@@ -5,11 +5,11 @@ Maintainer: xCAT <xcat-user@lists.sourceforge.net>
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
+47 -6
View File
@@ -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
+19
View File
@@ -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 "<xcatrequest>
@@ -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}
+36 -13
View File
@@ -37,10 +37,10 @@ sub ddns_tsig_algorithm {
my $settings = $ctx->{omapi_settings} || xCAT::DHCP::OmapiPolicy->settings();
# Keep old Net::DNS on MD5 unless the administrator explicitly selects a
# different OMAPI algorithm. Old Net::DNS can sign non-MD5 updates only
# through a KEY RR, which ddns_sign_update builds below.
return "hmac-md5" if (!net_dns_uses_keyfile() && !$settings->{algorithm_explicit});
# $ctx->{tsig_algorithm} is the algorithm the named.conf key stanza already declares.
# The Net::DNS version does not select the algorithm: old Net::DNS signs every algorithm
# except MD5 through a KEY RR, which ddns_sign_update builds.
return $settings->{algorithm} if $settings->{algorithm_explicit};
return $ctx->{tsig_algorithm} || $settings->{algorithm};
}
@@ -65,13 +65,17 @@ sub ddns_sign_update {
return;
}
if ($settings->{algorithm} eq 'hmac-md5') {
# named matches the key by name and by algorithm. Sign with the algorithm the key stanza
# declares, not with the OMAPI default.
my $algorithm = ddns_tsig_algorithm($ctx);
if ($algorithm eq 'hmac-md5') {
$update->sign_tsig($settings->{key_name}, $ctx->{privkey});
return;
}
my $owner = xCAT::DHCP::OmapiPolicy->key_owner($settings);
my $keyrr = Net::DNS::RR->new("$owner IN KEY 512 3 $settings->{key_rr_type} $ctx->{privkey}");
my $owner = xCAT::DHCP::OmapiPolicy->key_owner($settings);
my $rr_type = xCAT::DHCP::OmapiPolicy->algorithm_rr_type($algorithm);
my $keyrr = Net::DNS::RR->new("$owner IN KEY 512 3 $rr_type $ctx->{privkey}");
$update->sign_tsig($keyrr);
}
@@ -1345,10 +1349,6 @@ sub update_namedconf {
$ctx->{tsig_algorithm} = $omapi_settings->{algorithm};
push @newnamed, ddns_key_contents($ctx);
$ctx->{restartneeded} = 1;
} elsif ($algorithmnow && !net_dns_uses_keyfile() && lc($algorithmnow) ne "hmac-md5") {
$ctx->{tsig_algorithm} = "hmac-md5";
push @newnamed, ddns_key_contents($ctx);
$ctx->{restartneeded} = 1;
} else {
push @newnamed, @keyblock;
}
@@ -1579,6 +1579,26 @@ sub update_namedconf {
}
}
#-------------------------------------------------------------------------------
=head3 ddns_update_request
Descriptions: Copy the records of one dynamic DNS update into a new, unsigned request.
Arguments: the update the caller built, the zone name
Returns: a Net::DNS::Update holding the same prerequisite and update records
=cut
#-------------------------------------------------------------------------------
sub ddns_update_request {
my ($update, $zone) = @_;
my $request = Net::DNS::Update->new($zone);
$request->push(pre => $_) for $update->answer;
$request->push(update => $_) for $update->authority;
return $request;
}
# Send a signed dynamic DNS update, retrying transient rejections. Right after a zone (re)load named
# can reply NOTAUTH, or SERVFAIL before the zone is ready to accept dynamic updates; both are
# recoverable, so retry a few times (pausing on SERVFAIL). Returns 0 only when the update was
@@ -1589,8 +1609,11 @@ sub send_ddns_update {
my ($ctx, $resolver, $update, $zone, $entry) = @_;
for my $attempt (1 .. 3) {
ddns_sign_update($ctx, $update);
my $reply = $resolver->send($update);
# sign_tsig appends the TSIG to the additional section. A second signature on the same
# packet sends two TSIG records, and named answers FORMERR. Sign a copy for each attempt.
my $request = ddns_update_request($update, $zone);
ddns_sign_update($ctx, $request);
my $reply = $resolver->send($request);
if (!$reply) {
xCAT::SvrUtils::sendmsg([ 1, "No reply received when sending DNS update to zone $zone" ], $callback);
return 1;
+56 -14
View File
@@ -191,6 +191,8 @@ my %INSTALL_BOOT_FILES = (
'ppc64' => [
[ 'install/netboot/ubuntu-installer/{darch}/vmlinux', 'install/netboot/ubuntu-installer/{darch}/initrd.gz' ],
[ 'install/vmlinux', 'install/netboot/initrd.gz' ],
[ 'casper/hwe-vmlinux', 'casper/hwe-initrd' ],
[ 'casper/vmlinux', 'casper/initrd' ],
],
'riscv64' => [
[ 'casper/vmlinux', 'casper/initrd' ],
@@ -379,6 +381,53 @@ sub _no_grub2_loader {
return;
}
#-------------------------------------------------------
=head3 install_prescript
Descriptions: Return the pre-install script an Ubuntu or Debian install runs.
Arguments:
$platform - the distribution family, for example ubuntu
$arch - the architecture of the node
$subiquity - true when the osimage template is a Subiquity autoinstall
Returns: the full path of the pre-install script
=cut
#-------------------------------------------------------
sub install_prescript
{
my ($platform, $arch, $subiquity) = @_;
my $base = "$::XCATROOT/share/xcat/install/scripts/pre.$platform";
# pre.ubuntu.ppc64 writes a partman recipe, which only the debian-installer
# reads. Subiquity gets its POWER partitioning from pre.ubuntu.subiquity.
return "$base.subiquity" if ($subiquity);
return "$base.ppc64" if (defined($arch) and $arch =~ /ppc64/i and $platform eq "ubuntu");
return $base;
}
#-------------------------------------------------------
=head3 install_media_is_bootable
Descriptions: Report whether copied media carries an install kernel and initrd.
Arguments:
$arch - the xCAT architecture of the node
$darch - the dpkg architecture
$pkgdir - the directory copycds wrote the media to
Returns: 1 when the media can boot a network install, 0 when it cannot
=cut
#-------------------------------------------------------
sub install_media_is_bootable
{
my ($arch, $darch, $pkgdir) = @_;
return install_boot_files($arch, $darch, $pkgdir) ? 1 : 0;
}
sub is_ubuntu_live_media
{
my $media_path = shift;
@@ -1132,18 +1181,10 @@ sub mkinstall {
);
}
# maybe Debian will decide to use subiquity at some point?
my $prescript = "$::XCATROOT/share/xcat/install/scripts/pre.$platform";
if (using_subiquity($os,$tmplfile)) {
$prescript = $prescript . ".subiquity";
}
my $prescript =
install_prescript($platform, $arch, using_subiquity($os, $tmplfile));
my $postscript = "$::XCATROOT/share/xcat/install/scripts/post.$platform";
# for powerkvm VM ubuntu LE#
if ($arch =~ /ppc64/i and $platform eq "ubuntu") {
$prescript = "$::XCATROOT/share/xcat/install/scripts/pre.$platform.ppc64";
}
if (-r "$prescript") {
$preerr = xCAT::Template->subvars($prescript,
@@ -1176,10 +1217,11 @@ sub mkinstall {
next;
}
if ($arch =~ /ppc64/i and !(-e "$pkgdir/install/netboot/initrd.gz") and
!(-e "$pkgdir/install/netboot/ubuntu-installer/$darch/initrd.gz")) {
xCAT::MsgUtils->report_node_error($callback, $node,
"The network boot initrd.gz is not found in $pkgdir/install/netboot. This is provided by Ubuntu, please download and retry."
# The POWER live-server ISO keeps its installer under casper and ships no netboot
# tree, so the media that can boot it is the media install_boot_files resolves.
unless (install_media_is_bootable($arch, $darch, $pkgdir)) {
xCAT::MsgUtils->report_node_error($callback, $node,
"No install kernel and initrd were found on the media in $pkgdir."
);
next;
}
+99 -32
View File
@@ -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 <os> 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 <os> 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 <os> 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};
+44 -13
View File
@@ -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;
@@ -278,6 +278,60 @@ storage:
path: /
device: root-part-fs
EOF
elif [ "$(uname -m)" = "ppc64le" ] || [ "$(uname -m)" = "ppc64" ]; then
# POWER firmware reads neither an ESP nor a bios_grub partition. It loads the
# boot loader from a PReP partition, and curtin writes grub to the partition
# flagged prep, not to the disk.
cat <<EOF >/tmp/partitionfile
storage:
version: 1
config:
- id: disk-detected
type: disk
ptable: gpt
path: $INSTALL_DISK
wipe: superblock-recursive
preserve: false
- id: prep-part
type: partition
device: disk-detected
size: 8M
flag: prep
number: 1
preserve: false
grub_device: true
- id: swap-part
type: partition
device: disk-detected
size: 2G
number: 2
preserve: false
- id: root-part
type: partition
device: disk-detected
size: -1
wipe: superblock
number: 3
preserve: false
- id: swap-part-fs
type: format
fstype: swap
volume: swap-part
preserve: false
- id: root-part-fs
type: format
fstype: ext4
volume: root-part
preserve: false
- id: swap-part-mount
type: mount
path: none
device: swap-part-fs
- id: root-part-mount
type: mount
path: /
device: root-part-fs
EOF
else
cat <<EOF >/tmp/partitionfile
storage:
@@ -90,9 +90,9 @@ autoinstall:
installmac="#SUBIQUITYINSTALLMAC#";
mkdir -p /target/etc/netplan;
if [ -z "${installnic}" ]; then
printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " dhcp4: true" >/target/etc/netplan/00-xcat-install.yaml;
printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >/target/etc/netplan/00-xcat-install.yaml;
else
printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " set-name: ${installnic}" " dhcp4: true" >/target/etc/netplan/00-xcat-install.yaml;
printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " set-name: ${installnic}" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >/target/etc/netplan/00-xcat-install.yaml;
fi;
chmod 600 /target/etc/netplan/00-xcat-install.yaml;
printf ''%s\n'' ''#HOSTNAME#'' >/target/etc/hostname;
@@ -0,0 +1,20 @@
bash
ifupdown
nfs-common
openssl
isc-dhcp-client
libc-bin
linux-image-generic
openssh-server
openssh-client
wget
vim
rsync
busybox-static
gawk
bind9-dnsutils
tar
gzip
xz-utils
cpio
chrony
@@ -0,0 +1,20 @@
bash
ifupdown
nfs-common
openssl
isc-dhcp-client
libc-bin
linux-image-generic
openssh-server
openssh-client
wget
vim
rsync
busybox-static
gawk
bind9-dnsutils
tar
gzip
xz-utils
cpio
chrony
@@ -0,0 +1,20 @@
bash
ifupdown
nfs-common
openssl
isc-dhcp-client
libc-bin
linux-image-generic
openssh-server
openssh-client
wget
vim
rsync
busybox-static
gawk
bind9-dnsutils
tar
gzip
xz-utils
cpio
chrony
@@ -0,0 +1,20 @@
bash
ifupdown
nfs-common
openssl
isc-dhcp-client
libc-bin
linux-image-generic
openssh-server
openssh-client
wget
vim
rsync
busybox-static
gawk
bind9-dnsutils
tar
gzip
xz-utils
cpio
chrony
+2 -2
View File
@@ -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
+2 -4
View File
@@ -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
@@ -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 <node> 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;
}
+50 -5
View File
@@ -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.<arch>, 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.<arch> 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-<node> 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
@@ -57,7 +57,7 @@ check:rc==0
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str=`/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/retry_install.sh $$CN __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute 1`; if [[ $str =~ "failed" ]]; then exit 0; else exit 1; fi; fi
check:rc==0
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str1=`lsdef $$CN | grep vmothersetting | cut -d '=' -f 2`;str2="machine:invalid"; if [[ "__GETNODEATTR($$CN,arch)__" =~ "ppc64" ]]; then str3="machine:pseries-rhel7.6.0"; elif [[ "__GETNODEATTR($$CN,arch)__" =~ "x86_64" ]]; then str3="machine:pc";fi; if [ $str1 == $str2 ]; then str5=$str3; else str4=`echo $str1 | sed -e "s/$str2//"`;str5=$str4$str3;fi; chdef $$CN vmothersetting=$str5; fi
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str1=`lsdef $$CN | grep vmothersetting | cut -d '=' -f 2`;str2="machine:invalid"; if [[ "__GETNODEATTR($$CN,arch)__" =~ "ppc64" ]]; then str3="machine:pseries-rhel7.6.0"; elif [[ "__GETNODEATTR($$CN,arch)__" =~ "x86_64" ]]; then str3="machine:pc"; elif [[ "__GETNODEATTR($$CN,arch)__" =~ "riscv64" ]]; then str3="machine:virt";fi; if [ "$str1" == "$str2" ]; then str5=$str3; else str4=`echo $str1 | sed -e "s/$str2//"`;str5=$str4$str3;fi; chdef $$CN vmothersetting=$str5; fi
check:rc==0
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str=`lsdef $$CN | grep vmothersetting`; if [[ $str =~ "machine" ]]; then exit 0; else exit 1; fi; fi
check:rc==0
@@ -68,7 +68,7 @@ check:rc==0
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str=`/opt/xcat/share/xcat/tools/autotest/testcase/commoncmd/retry_install.sh $$CN __GETNODEATTR($$CN,os)__-__GETNODEATTR($$CN,arch)__-netboot-compute`; echo $str; if [[ $str =~ "The provision succeeded" ]]; then exit 0; else exit 1; fi; fi
check:rc==0
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str1=`lsdef $$CN | grep vmothersetting | cut -d '=' -f 2`;str2=";"; if [[ "__GETNODEATTR($$CN,arch)__" =~ "ppc64" ]]; then str3="machine:pseries-7.6.0"; elif [[ "__GETNODEATTR($$CN,arch)__" =~ "x86_64" ]]; then str3="machine:pc";fi; if [ $str1 == $str3 ]; then chdef $$CN vmothersetting=; else str4=`echo $str1 | sed -e "s/$str2$str3//"`; chdef $$CN vmothersetting=$str4;fi; fi
cmd:if [ "__GETNODEATTR($$CN,mgt)__" == "kvm" ]; then str1=`lsdef $$CN | grep vmothersetting | cut -d '=' -f 2`;str2=";"; if [[ "__GETNODEATTR($$CN,arch)__" =~ "ppc64" ]]; then str3="machine:pseries-rhel7.6.0"; elif [[ "__GETNODEATTR($$CN,arch)__" =~ "x86_64" ]]; then str3="machine:pc"; elif [[ "__GETNODEATTR($$CN,arch)__" =~ "riscv64" ]]; then str3="machine:virt";fi; if [ "$str1" == "$str3" ]; then chdef $$CN vmothersetting=; else str4=`echo $str1 | sed -e "s/$str2$str3//"`; chdef $$CN vmothersetting=$str4;fi; fi
check:rc==0
cmd:if [ "__GETNODEATTR($$CN,arch)__" != "ppc64" -a "__GETNODEATTR($$CN,mgt)__" != "ipmi" -a "__GETNODEATTR($$CN,mgt)__" != "openbmc" ];then echo "CN node $$CN is a VM, mgt is __GETNODEATTR($$CN,mgt)__, starting to recreate the vm"; echo "rpower $$CN off"; rpower $$CN off; sleep 3; echo "rpower $$CN stat"; rpower $$CN stat; var=`expr substr "__GETNODEATTR($$CN,vmstorage)__" 1 3`; echo "The disk type of $$CN is $var"; if [ "$var" = "phy" ]; then echo "mkvm $$CN"; mkvm $$CN; echo "rmvm $$CN -f -p"; rmvm $$CN -f -p; echo "mkvm $$CN"; mkvm $$CN; exit $?; elif [ "$var" = "dir" ]; then echo "mkvm $$CN -s 30G -f"; mkvm $$CN -s 30G -f; echo "rmvm $$CN -f -p"; rmvm $$CN -f -p; echo "mkvm $$CN -s 30G -f"; mkvm $$CN -s 30G -f; exit $?; elif ["$var" = "nfs" -o "$var" = "lvm" ];then echo "Need to fix disk type $var"; exit 2; else echo "Unsupported disk type $var"; exit 3;fi;else echo "CN node $$CN is not a VM; do not need to recreate it";fi
check:rc==0
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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=
@@ -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
+8 -8
View File
@@ -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
@@ -0,0 +1,102 @@
#!/usr/bin/env bats
#
# reg_linux_diskless_installation_flat corrupts the KVM machine type of the compute node, checks
# that the node fails to boot, and then restores it. The restore reads a machine type from a
# ladder that names ppc64 and x86_64 only, so on any other architecture it writes an empty
# vmothersetting and the check after it fails.
#
# The two commands are lifted out of the case file and RUN, with lsdef and chdef shadowed, so
# the assertions read the value the case would write. The extraction fails the test when it
# stops matching, so a rewrite fails loudly instead of covering nothing.
load 'helpers/shell_source'
setup()
{
CASE="$(repo_path 'xCAT-test/autotest/testcase/installation/reg_linux_diskless_installation_flat')"
[ -r "$CASE" ] || skip "$CASE is required"
export CASE
}
# The command that restores the machine type, and the one that removes it again afterwards.
restore_command()
{
extract_first_matching_line "$CASE" \
'^cmd:.*str2="machine:invalid".*chdef [$][$]CN vmothersetting=[$]str5'
}
remove_command()
{
extract_first_matching_line "$CASE" \
'^cmd:.*str2=";".*~ "ppc64".*chdef [$][$]CN vmothersetting='
}
# Render one command the way xcattest does, then run it with lsdef and chdef shadowed. bash
# resolves a function ahead of PATH, so the case's own backticks read the stub.
#
# Sets OUT to everything the command printed and WRITTEN to the value it gave chdef.
run_case_command()
{
local cmd="$1" arch="$2" lsdef_value="$3"
cmd="${cmd#cmd:}"
cmd="${cmd//__GETNODEATTR(\$\$CN,arch)__/$arch}"
cmd="${cmd//__GETNODEATTR(\$\$CN,mgt)__/kvm}"
cmd="${cmd//\$\$CN/cn1}"
OUT="$(bash -c "lsdef() { echo ' vmothersetting=$lsdef_value'; }
chdef() { echo \"CHDEF:[\$*]\"; }
$cmd" 2>&1)" || true
# The brackets keep an empty value apart from no call at all: the cleanup is meant to write
# an empty vmothersetting, and a command that never reaches chdef must not read as that.
CHDEF_CALLS="$(grep -c '^CHDEF:' <<<"$OUT" || true)"
WRITTEN="$(sed -n 's/^CHDEF:\[cn1 vmothersetting=\(.*\)\]$/\1/p' <<<"$OUT")"
}
# The machine type each architecture must end up with. riscv64 guests run the qemu "virt"
# machine; kvm.pm sets it in guest_arch_profile.
#
# The restore writes the machine type; the cleanup after it takes the same machine type away
# again and leaves every other setting. Both read the same ladder, so both are checked against
# the same value.
assert_arch()
{
local arch="$1" machine="$2" restore remove
restore="$(restore_command)"
remove="$(remove_command)"
# The node carries the corrupt value only.
run_case_command "$restore" "$arch" 'machine:invalid'
[ "$CHDEF_CALLS" -eq 1 ]
[ "$WRITTEN" = "machine:$machine" ]
# The node carries a setting of its own beside the corrupt value.
run_case_command "$restore" "$arch" 'cpumode:host-passthrough;machine:invalid'
[ "$CHDEF_CALLS" -eq 1 ]
[ "$WRITTEN" = "cpumode:host-passthrough;machine:$machine" ]
# The cleanup, with nothing but the machine type to remove.
run_case_command "$remove" "$arch" "machine:$machine"
[ "$(grep -c 'unary operator expected' <<<"$OUT")" -eq 0 ]
[ "$CHDEF_CALLS" -eq 1 ]
[ "$WRITTEN" = "" ]
# The cleanup, with a setting of its own that must survive it.
run_case_command "$remove" "$arch" "cpumode:host-passthrough;machine:$machine"
[ "$(grep -c 'unary operator expected' <<<"$OUT")" -eq 0 ]
[ "$CHDEF_CALLS" -eq 1 ]
[ "$WRITTEN" = "cpumode:host-passthrough" ]
}
@test "ppc64le: the restore writes the machine type and the cleanup takes it away" {
assert_arch ppc64le pseries-rhel7.6.0
}
@test "x86_64: the restore writes the machine type and the cleanup takes it away" {
assert_arch x86_64 pc
}
@test "riscv64: the restore writes the machine type and the cleanup takes it away" {
assert_arch riscv64 virt
}
+93
View File
@@ -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" <<CONTROL
Source: ${package}
Section: alien
Priority: extra
Maintainer: xCAT <xcat-user@lists.sourceforge.net>
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"
}
@@ -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)" ]]
}
+68
View File
@@ -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
}
+122
View File
@@ -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" ]
}
@@ -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 </dev/null
}
@test "getcert stops and names openssl when the image ships none" {
# The el10 legacy image ships no openssl.
run run_getcert "$(stub_dir 0)" 10 60
[ "$status" -ne 124 ]
[ "$status" -ne 0 ]
[[ "$output" == *openssl* ]]
}
@test "getcert retries the certificate request, then gives up and names the key" {
# doxcat writes /etc/xcat/certkey.pem in the background, so the first requests can fail.
export COUNT_REQUESTS=1
run run_getcert "$(stub_dir 1)" 30 5
[ "$status" -ne 124 ]
[ "$status" -ne 0 ]
[[ "$output" == *certkey.pem* ]]
tries=0
[ -f "$COUNTER" ] && tries="$(grep -c req "$COUNTER")"
[ "$tries" -gt 1 ]
}
@@ -0,0 +1,111 @@
#!/usr/bin/env bats
#
# Run the nodeset_shell_incorrectmasterip check against a scratch tftp root, with the xCAT
# commands and the net tools shadowed.
load 'helpers/shell_source'
setup()
{
SCRIPT="$(repo_path 'xCAT-test/autotest/testcase/genesis/test.sh')"
[ -r "$SCRIPT" ] || skip "$SCRIPT is required"
HOST_ARCH="$(uname -m)"
# grub2.pm names the boot loader grub2.<arch>, 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 <loader>` 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" <<DRIVER
chdef() { echo "\$@" >> '$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 ]
}
+89
View File
@@ -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'
}
@@ -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
}
+10
View File
@@ -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 $?
}
@@ -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();
+1 -3
View File
@@ -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;
@@ -0,0 +1,68 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../../build-utils/lib";
use Test::More;
use XCAT::BuildUtils qw(read_line snap_release);
# builddebs.pl takes the Release file as authoritative. The tracked file holds the
# placeholder snap000000000000, and only buildrpms.pl overwrites it with the commit
# time. A pipeline that does not run buildrpms.pl keeps the placeholder.
#
# The release decision is extracted from builddebs.pl and run here, so the assertions
# measure the shipped code rather than a copy of it.
my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my $builder = File::Spec->catfile( $repo_root, 'builddebs.pl' );
die "builddebs.pl not found\n" unless -f $builder;
my $src = do { local $/; open my $fh, '<', $builder or die $!; <$fh> };
# die rather than skip: a rewrite that stops this matching must fail loudly
# instead of silently covering nothing.
my ($block) = $src =~ /^(my \$FILE_RELEASE\b.*?^my \$RELEASE\s*=.*?;\n)/ms;
die('could not extract the release decision from builddebs.pl')
unless defined $block;
my $dir = tempdir( CLEANUP => 1 );
my $run = 0;
# Resolve the release the way builddebs.pl does, for one Release file content and one
# --release option. Returns the release string.
sub release_for {
my ( $file_content, $opt_release ) = @_;
$run++;
my $ROOT = File::Spec->catdir( $dir, "run$run" );
mkdir $ROOT or die $!;
if ( defined $file_content ) {
open( my $fh, '>', File::Spec->catfile( $ROOT, 'Release' ) ) or die $!;
print {$fh} $file_content;
close($fh);
}
my $EPOCH = 1756000000;
my %opts;
$opts{release} = $opt_release if defined $opt_release;
my $got = eval "$block\n\$RELEASE";
die $@ if $@;
return $got;
}
my $from_epoch = snap_release(1756000000);
is( release_for("snap000000000000\n"), $from_epoch,
'the tracked placeholder is not a release, so the commit time is used' );
is( release_for("snap202608240826\n"), 'snap202608240826',
'a release buildrpms.pl wrote is still authoritative' );
is( release_for(undef), $from_epoch,
'no Release file gives the commit time' );
is( release_for( "snap000000000000\n", 'snap209901010000' ), 'snap209901010000',
'--release still wins over the placeholder' );
done_testing();
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env perl
# makedns must sign every update with the algorithm the named.conf key stanza declares.
# named matches a TSIG key by name AND algorithm, so a stanza that does not agree with the
# signature makes named reject every update and makedns exit 1.
#
# Run this test with XCATROOT set to the tree under test. xCAT::Table does
# "use lib $::XCATROOT/lib/perl", so an installed /opt/xcat shadows the modules under test:
# XCATROOT=$PWD/xCAT-server prove xCAT-test/unit/ddns_named_key_algorithm.t
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use lib "$FindBin::Bin/../../perl-xCAT";
use File::Temp qw(tempfile);
use Test::More;
$ENV{XCATCFG} ||= 'SQLite:/tmp';
$ENV{XCATROOT} ||= "$FindBin::Bin/../../xCAT-server";
my $ddns_plugin_path =
"$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/ddns.pm";
if ( -f $ddns_plugin_path ) {
require $ddns_plugin_path;
}
else {
require xCAT_plugin::ddns;
}
# KEY RR algorithm numbers, as ddns_sign_update writes them for old Net::DNS.
my %ALGORITHM_OF_RR_TYPE = (
157 => 'hmac-md5',
161 => 'hmac-sha1',
162 => 'hmac-sha224',
163 => 'hmac-sha256',
164 => 'hmac-sha384',
165 => 'hmac-sha512',
);
my $SECRET = 'c2VjcmV0LXNlY3JldC1zZWNyZXQtc2VjcmV0LXNlY3I=';
my $with_key = <<"NAMED";
options {
};
key "xcat_key" {
\talgorithm hmac-sha256;
\tsecret "$SECRET";
};
NAMED
my $without_key = "options {\n};\n";
subtest 'old Net::DNS keeps the configured algorithm in named.conf' => sub {
my $result = run_makedns(
named_conf => $with_key,
site_algorithm => 'hmac-sha256',
net_dns => '1.25',
);
is( $result->{named_algorithm}, 'hmac-sha256',
'named.conf keeps the algorithm the key was generated with' );
is( $result->{signing_algorithm}, 'hmac-sha256',
'the update is signed with that algorithm' );
is( $result->{restartneeded}, 0, 'named is not restarted' );
};
subtest 'old Net::DNS does not downgrade an unconfigured site' => sub {
my $result = run_makedns(
named_conf => $with_key,
site_algorithm => undef,
net_dns => '1.25',
);
is( $result->{named_algorithm}, 'hmac-sha256',
'named.conf keeps hmac-sha256 when the site table names no algorithm' );
is( $result->{signing_algorithm}, 'hmac-sha256',
'the update is signed with hmac-sha256' );
is( $result->{restartneeded}, 0, 'named is not restarted' );
};
subtest 'new Net::DNS signs through the key file' => sub {
my $result = run_makedns(
named_conf => $with_key,
site_algorithm => 'hmac-sha256',
net_dns => '1.47',
);
is( $result->{named_algorithm}, 'hmac-sha256',
'named.conf keeps hmac-sha256' );
is( $result->{signing_algorithm}, 'keyfile',
'the update is signed with /etc/xcat/ddns.key' );
};
subtest 'an explicit site algorithm replaces the stanza' => sub {
my $result = run_makedns(
named_conf => $with_key,
site_algorithm => 'hmac-sha512',
net_dns => '1.25',
);
is( $result->{named_algorithm}, 'hmac-sha512',
'named.conf takes the algorithm the administrator selected' );
is( $result->{signing_algorithm}, 'hmac-sha512',
'the update is signed with the selected algorithm' );
is( $result->{restartneeded}, 1, 'named is restarted for the new stanza' );
};
subtest 'a generated key stays on hmac-md5 for old Net::DNS' => sub {
my $result = run_makedns(
named_conf => $without_key,
site_algorithm => undef,
net_dns => '1.25',
);
is( $result->{named_algorithm}, 'hmac-md5',
'a key created for old Net::DNS uses hmac-md5' );
is( $result->{signing_algorithm}, 'hmac-md5',
'the update is signed with hmac-md5' );
};
done_testing();
#---------------------------------------------------------------------------
=head3 run_makedns
Description: Run update_namedconf over a scratch named.conf, then sign one update
with the context that run produced.
Arguments: named_conf (the file content), site_algorithm (undef for none),
net_dns (the Net::DNS version to report)
Returns: hash reference with named_algorithm, signing_algorithm, restartneeded
=cut
#---------------------------------------------------------------------------
sub run_makedns {
my (%args) = @_;
my ( $named_fh, $named_path ) = tempfile( UNLINK => 1 );
print {$named_fh} $args{named_conf};
close($named_fh) or die "Unable to close $named_path: $!";
my $settings = xCAT::DHCP::OmapiPolicy->settings(
site_values => {
dhcpomapialgorithm => $args{site_algorithm},
dhcpomapikeyname => undef,
dhcpomshellpath => undef,
}
);
die "Unusable OMAPI settings: $settings->{error}" if $settings->{error};
my $ctx = {
omapi_settings => $settings,
privkey => $SECRET,
zonesdir => '/tmp',
dbdir => '/tmp',
zonestotouch => {},
adzones => {},
dnsupdaters => [],
adservers => [],
restartneeded => 0,
};
no warnings qw(redefine once);
local *xCAT_plugin::ddns::get_conf = sub { return $named_path; };
local *xCAT_plugin::ddns::ensure_ddns_key_file = sub { return; };
local *xCAT::TableUtils::get_site_attribute = sub { return; };
local *xCAT::Utils::runcmd = sub { return (); };
local *xCAT::Utils::isAIX = sub { return 0; };
local *xCAT::Utils::isLinux = sub { return 1; };
local *xCAT::Table::new = sub { return bless {}, 'Local::DDNS::PasswdTable'; };
my $update = Local::DDNS::Update->new();
{
local $Net::DNS::VERSION = $args{net_dns};
xCAT_plugin::ddns::update_namedconf( $ctx, 0 );
xCAT_plugin::ddns::ddns_sign_update( $ctx, $update );
}
open( my $result_fh, '<', $named_path )
or die "Unable to read $named_path: $!";
local $/;
my $contents = <$result_fh>;
close($result_fh) or die "Unable to close $named_path: $!";
my ($named_algorithm) =
( $contents =~ /key\s+"?xcat_key"?[^{]*\{[^}]*?algorithm\s+([^;\s]+)\s*;/s );
return {
named_algorithm => defined($named_algorithm) ? lc($named_algorithm) : undef,
signing_algorithm => signing_algorithm($update),
restartneeded => $ctx->{restartneeded} ? 1 : 0,
};
}
#---------------------------------------------------------------------------
=head3 signing_algorithm
Description: Name the TSIG algorithm one recorded sign_tsig call selects.
Arguments: the recording update object
Returns: the algorithm name, or "keyfile" for the key-file interface
=cut
#---------------------------------------------------------------------------
sub signing_algorithm {
my ($update) = @_;
my $calls = $update->{sign_tsig_calls};
is( scalar( @{$calls} ), 1, 'the update is signed once' );
my $args = $calls->[0];
if ( @{$args} == 1 && !ref( $args->[0] ) ) {
return 'keyfile';
}
if ( @{$args} == 2 ) {
return 'hmac-md5';
}
my $rr_type = $args->[0]->algorithm;
return $ALGORITHM_OF_RR_TYPE{$rr_type} || "KEY RR algorithm $rr_type";
}
{
package Local::DDNS::Update;
sub new {
return bless { sign_tsig_calls => [] }, shift;
}
sub sign_tsig {
my ( $self, @args ) = @_;
push @{ $self->{sign_tsig_calls} }, \@args;
return;
}
}
{
package Local::DDNS::PasswdTable;
sub setAttribs {
return 1;
}
}
+8 -10
View File
@@ -104,8 +104,8 @@ subtest 'all Net::DNS thresholds share the dotted version policy' => sub {
'no Net::DNS threshold uses Perl numeric comparison' );
my @policy_calls = ( $source =~ /net_dns_uses_keyfile\(\)/g );
is( scalar(@policy_calls), 4,
'all four Net::DNS threshold sites use the shared policy' );
is( scalar(@policy_calls), 2,
'both Net::DNS threshold sites use the shared policy' );
};
subtest 'Net::DNS threshold controls DDNS policy and signing' => sub {
@@ -116,7 +116,6 @@ subtest 'Net::DNS threshold controls DDNS policy and signing' => sub {
foreach my $case (@net_dns_versions) {
my ( $version, $uses_keyfile ) = @{$case};
my $expected_algorithm = $uses_keyfile ? 'hmac-sha256' : 'hmac-md5';
is(
with_net_dns_version(
$version,
@@ -126,8 +125,8 @@ subtest 'Net::DNS threshold controls DDNS policy and signing' => sub {
);
}
),
$expected_algorithm,
"Net::DNS $version selects the expected implicit algorithm"
'hmac-sha256',
"Net::DNS $version keeps the algorithm the site table implies"
);
my $update = Local::DDNS::Update->new();
@@ -172,17 +171,16 @@ subtest 'Net::DNS threshold controls named key reconciliation' => sub {
my ( $version, $uses_keyfile ) = @{$case};
my ( $named_contents, $restartneeded ) =
reconcile_named_key($version);
my $expected_algorithm = $uses_keyfile ? 'hmac-sha256' : 'hmac-md5';
like(
$named_contents,
qr/^\s*algorithm\s+\Q$expected_algorithm\E\s*;/m,
"Net::DNS $version keeps the expected named key algorithm"
qr/^\s*algorithm\s+hmac-sha256\s*;/m,
"Net::DNS $version keeps the named key algorithm"
);
is(
$restartneeded ? 1 : 0,
$uses_keyfile ? 0 : 1,
"Net::DNS $version records the expected named restart state"
0,
"Net::DNS $version leaves named alone"
);
}
};
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env perl
# send_ddns_update retries a rejected dynamic DNS update. Net::DNS appends the TSIG to the
# additional section, so every attempt must sign its own request: named answers FORMERR to a
# message that carries two TSIG records (measured on BIND 9.18.33).
#
# Run this test with XCATROOT set to the tree under test. xCAT::Table does
# "use lib $::XCATROOT/lib/perl", so an installed /opt/xcat shadows the modules under test:
# XCATROOT=$PWD/xCAT-server prove xCAT-test/unit/ddns_update_retry.t
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
$ENV{XCATCFG} ||= 'SQLite:/tmp';
$ENV{XCATROOT} ||= "$FindBin::Bin/../../xCAT-server";
my $ddns_plugin_path =
"$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/ddns.pm";
if ( -f $ddns_plugin_path ) {
require $ddns_plugin_path;
}
else {
require xCAT_plugin::ddns;
}
my $SECRET = 'c2VjcmV0LXNlY3JldC1zZWNyZXQtc2VjcmV0LXNlY3I=';
my $ZONE = 'test.lab';
subtest 'every attempt of a rejected update carries one TSIG' => sub {
my $resolver = Local::DDNS::Resolver->new( replies => [ 'NOTAUTH', 'NOTAUTH', 'NOTAUTH' ] );
my $rc = send_update($resolver);
is( $rc, 1, 'a persistently rejected update reports failure' );
is( scalar( @{ $resolver->{sent} } ), 3, 'the update is sent three times' );
is_deeply(
[ map { $_->{tsig_count} } @{ $resolver->{sent} } ],
[ 1, 1, 1 ],
'each attempt carries exactly one TSIG record'
);
is_deeply(
[ map { $_->{updates} } @{ $resolver->{sent} } ],
[ ( ['n1.test.lab. 300 IN A 10.0.0.1'] ) x 3 ],
'each attempt carries the same update records'
);
};
subtest 'a retry can be accepted' => sub {
my $resolver = Local::DDNS::Resolver->new( replies => [ 'NOTAUTH', 'NOERROR' ] );
my $rc = send_update($resolver);
is( $rc, 0, 'the accepted retry reports success' );
is( scalar( @{ $resolver->{sent} } ), 2, 'the update is sent twice' );
};
done_testing();
#---------------------------------------------------------------------------
=head3 send_update
Description: Send one dynamic DNS update through send_ddns_update.
Arguments: the recording resolver
Returns: the send_ddns_update return code
=cut
#---------------------------------------------------------------------------
sub send_update {
my ($resolver) = @_;
my $settings = xCAT::DHCP::OmapiPolicy->settings(
site_values => {
dhcpomapialgorithm => 'hmac-sha256',
dhcpomapikeyname => undef,
dhcpomshellpath => undef,
}
);
die "Unusable OMAPI settings: $settings->{error}" if $settings->{error};
my $ctx = {
omapi_settings => $settings,
privkey => $SECRET,
};
my $update = Net::DNS::Update->new($ZONE);
$update->push( update => Net::DNS::RR->new('n1.test.lab. 300 IN A 10.0.0.1') );
no warnings qw(redefine once);
local *xCAT::SvrUtils::sendmsg = sub { return; };
# Net::DNS 1.36 removed sign_tsig($name, $secret) and signs from a key file. Report the
# version that signs from a KEY RR, so the test needs no key file.
local $Net::DNS::VERSION = '1.25';
return xCAT_plugin::ddns::send_ddns_update( $ctx, $resolver, $update, $ZONE, 'n1.test.lab' );
}
{
package Local::DDNS::Resolver;
# Answer FORMERR to a message with more than one TSIG record, as named does, and otherwise
# answer the next scripted rcode.
sub new {
my ( $class, %args ) = @_;
return bless { replies => $args{replies}, sent => [] }, $class;
}
sub send {
my ( $self, $packet ) = @_;
my @tsig = grep { $_->type eq 'TSIG' } $packet->additional;
push @{ $self->{sent} },
{
tsig_count => scalar(@tsig),
updates => [ map { $_->plain } $packet->authority ],
};
my $rcode = shift @{ $self->{replies} };
$rcode = 'SERVFAIL' unless defined $rcode;
$rcode = 'FORMERR' if @tsig > 1;
return Local::DDNS::Reply->new($rcode);
}
}
{
package Local::DDNS::Reply;
sub new {
my ( $class, $rcode ) = @_;
return bless { rcode => $rcode }, $class;
}
sub header { return $_[0]; }
sub rcode { return $_[0]->{rcode}; }
}
@@ -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();
+53 -4
View File
@@ -7,10 +7,9 @@ use File::Temp qw(tempdir);
use FindBin;
use Test::More;
# The installer kernel and initrd sit in a different place on every Ubuntu media layout:
# netboot trees name them after the Debian architecture, live images keep them under
# casper, and a hardware-enablement kernel ships beside the release one. Build each layout
# on disk and ask the resolver, rather than reading the table that describes them.
# The installer kernel and initrd sit in a different place on every Ubuntu media layout.
# Build each layout on disk and ask the resolver, rather than read the table that
# describes them.
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
@@ -103,6 +102,56 @@ is(
'riscv64 does not accept the kernel name the other live images use',
);
# The Ubuntu ppc64el live-server ISO carries no netboot tree. 22.04 and 24.04 ship the
# hardware-enablement pair under casper beside the release pair; 26.04 ships the release
# pair only.
is(
resolved('ppc64le', 'ppc64el', media('casper/vmlinux', 'casper/initrd')),
'casper/vmlinux|casper/initrd',
'the POWER live image keeps its kernel under casper',
);
is(
resolved('ppc64le', 'ppc64el',
media('casper/hwe-vmlinux', 'casper/hwe-initrd', 'casper/vmlinux', 'casper/initrd')),
'casper/hwe-vmlinux|casper/hwe-initrd',
'the POWER hardware-enablement kernel wins over the release kernel',
);
is(
resolved('ppc64le', 'ppc64el',
media('install/netboot/ubuntu-installer/ppc64el/vmlinux',
'install/netboot/ubuntu-installer/ppc64el/initrd.gz',
'casper/vmlinux', 'casper/initrd')),
'install/netboot/ubuntu-installer/ppc64el/vmlinux|install/netboot/ubuntu-installer/ppc64el/initrd.gz',
'a POWER netboot tree still wins over a live image on the same media',
);
# mkinstall asks this routine, so it accepts every media install_boot_files resolves.
can_ok('xCAT_plugin::debian', 'install_media_is_bootable');
is(
xCAT_plugin::debian::install_media_is_bootable('ppc64le', 'ppc64el',
media('casper/vmlinux', 'casper/initrd')),
1,
'a POWER live image is bootable media',
);
is(
xCAT_plugin::debian::install_media_is_bootable('ppc64le', 'ppc64el',
media('install/netboot/ubuntu-installer/ppc64el/vmlinux',
'install/netboot/ubuntu-installer/ppc64el/initrd.gz')),
1,
'a POWER netboot tree is bootable media',
);
is(
xCAT_plugin::debian::install_media_is_bootable('ppc64le', 'ppc64el', media('README')),
0,
'media with no installer is not bootable media',
);
is(
xCAT_plugin::debian::install_media_is_bootable('x86_64', 'amd64',
media('casper/vmlinuz', 'casper/initrd')),
1,
'an x86 live image is bootable media',
);
# --- nothing to boot -------------------------------------------------------
is(resolved('x86_64', 'amd64', media('casper/vmlinuz')), undef,
'a kernel without its initrd is not a match');
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use Test::More;
# Ubuntu has two installers and two pre-install scripts that are not interchangeable.
# pre.ubuntu.subiquity writes a curtin "storage:" document that the autoinstall
# early-commands append to /autoinstall.yaml. pre.ubuntu.ppc64 writes a partman recipe
# for the debian-installer, which is not YAML at all.
#
# mkinstall chose the subiquity script, then overwrote that choice for every ppc64 node,
# so a ppc64el Subiquity install appended a partman recipe to its autoinstall.yaml. The
# ppc64 script belongs to the debian-installer path only.
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
my $plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/debian.pm";
die "debian.pm not found\n" unless -r $plugin;
eval { require $plugin; 1 } or plan skip_all => "could not load debian.pm: $@";
can_ok('xCAT_plugin::debian', 'install_prescript')
or die('mkinstall still chooses the pre-install script inline, so nothing can drive it');
sub chosen {
my ($platform, $arch, $subiquity) = @_;
my $path = xCAT_plugin::debian::install_prescript($platform, $arch, $subiquity);
$path =~ s{.*/}{};
return $path;
}
# --- subiquity: the arch never changes the script -------------------------
is(chosen('ubuntu', 'x86_64', 1), 'pre.ubuntu.subiquity',
'an x86_64 subiquity install gets the subiquity pre-install script');
is(chosen('ubuntu', 'ppc64el', 1), 'pre.ubuntu.subiquity',
'a ppc64el subiquity install gets the subiquity pre-install script, not the partman one');
is(chosen('ubuntu', 'ppc64le', 1), 'pre.ubuntu.subiquity',
'the ppc64le spelling reaches the same script');
is(chosen('ubuntu', 'ppc64', 1), 'pre.ubuntu.subiquity',
'so does the bare ppc64 spelling');
# --- debian-installer: ppc64 keeps its own script -------------------------
is(chosen('ubuntu', 'ppc64el', 0), 'pre.ubuntu.ppc64',
'a ppc64el debian-installer install keeps the partman pre-install script');
is(chosen('ubuntu', 'ppc64', 0), 'pre.ubuntu.ppc64',
'and so does the bare ppc64 spelling');
is(chosen('ubuntu', 'x86_64', 0), 'pre.ubuntu',
'an x86_64 debian-installer install gets the plain script');
# --- the override is Ubuntu only -----------------------------------------
is(chosen('debian', 'ppc64el', 0), 'pre.debian',
'Debian on POWER has no ppc64 pre-install script to select');
done_testing();
@@ -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');
+26 -6
View File
@@ -21,7 +21,7 @@ if (!-f $verifier) {
done_testing();
exit;
}
plan tests => 18;
plan tests => 22;
my $tmpdir = tempdir(CLEANUP => 1);
my $module_seq = 0;
@@ -74,6 +74,22 @@ my $noopenssl = build_payload(sshd_execs_session => 1, session_helper => 1, tmux
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']);
@@ -114,12 +130,16 @@ sub build_payload {
}
write_text("$root/usr/sbin/dhclient", "dhclient\n") if $opt{dhclient};
write_text("$root/usr/bin/mktemp", "mktemp\n") if $opt{mktemp};
# The module also names two absolute paths, and dracut_install installs an absolute
# path at that same path. They are data files every payload carries.
make_path("$root/etc");
write_text("$root/usr/bin/awk", "awk\n");
write_text("$root/etc/services", "services\n");
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;
}
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env perl
# The genesis specs name their package after the target arch: xCAT-genesis-scripts-<tarch> and
# xCAT-genesis-base-<tarch>. %{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();
+210
View File
@@ -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;
}
+123
View File
@@ -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();
+90
View File
@@ -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 <node>.<device>.<format>. 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();
+122
View File
@@ -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{<type\b([^>]*)>hvm</type>}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/<domain\b[^>]*\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/<os\b[^>]*\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/<bios\b/,
'the SeaBIOS serial option is left out of a riscv64 guest');
unlike($riscv, qr/<input\b/,
'the riscv64 virt machine has no USB controller, so it gets no USB tablet');
# POWER is unchanged: the arch still comes from the hypervisor there.
my $power = domain_xml('ppc64le', 'ppc64le');
like($power, qr/<domain\b[^>]*\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/<domain\b[^>]*\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/<input\b[^>]*\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();
+62
View File
@@ -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();
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use Test::More;
# genimage builds the netboot initrd for ppc64el with mkinitrd, not dracut, and mkinitrd
# copies usr/bin/dig out of the rootimg. A release with no ppc64el package list falls back
# to compute.pkglist, which installs no dig, and genimage stops with
# "Failed to find usr/bin/dig". The package that holds dig changed name in 22.04.
use lib "$FindBin::Bin/../lib";
use XCAT::Test::File qw(repo_path);
my $netboot = 'xCAT-server/share/xcat/netboot/ubuntu';
sub packages {
my ($path) = @_;
my $full = repo_path($path);
return unless -r $full;
open(my $fh, '<', $full) or die "cannot read $full: $!";
my @packages = grep { length && !/^#/ } map { my $l = $_; chomp $l; $l =~ s/\s+//g; $l } <$fh>;
close($fh);
return \@packages;
}
foreach my $release (qw(20.04 22.04 24.04 26.04)) {
my $list = packages("$netboot/compute.ubuntu$release.ppc64el.pkglist");
ok($list, "$release has a ppc64el netboot package list");
SKIP: {
skip "no $release ppc64el package list to inspect", 4 unless $list;
ok(scalar(grep { $_ eq 'dnsutils' || $_ eq 'bind9-dnsutils' } @{$list}),
"the $release ppc64el image installs the dig that mkinitrd copies");
ok(scalar(grep { $_ eq 'linux-image-generic' } @{$list}),
"the $release ppc64el image installs a kernel");
ok(scalar(grep { $_ eq 'nfs-common' } @{$list}),
"the $release ppc64el image can mount its root over NFS");
is_deeply(packages("$netboot/compute.ubuntu$release.ppc64le.pkglist"), $list,
"the $release ppc64le list matches the ppc64el list");
}
}
done_testing();
+9 -2
View File
@@ -98,7 +98,7 @@ $tmpl_path = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu/compute.
unless -f $tmpl_path;
SKIP: {
skip 'compute.subiquity.tmpl not found', 4 unless -f $tmpl_path;
skip 'compute.subiquity.tmpl not found', 6 unless -f $tmpl_path;
my $tmpl = do { local $/; open my $fh, '<', $tmpl_path or die $!; <$fh> };
# The netplan-writing part of late-commands, verbatim: from the resolved values down to the
@@ -106,7 +106,7 @@ SKIP: {
my ($snippet) = $tmpl =~ /(installnic="#SUBIQUITYINSTALLNIC#".*?fi;)/s;
ok($snippet, 'the netplan late-command is rendered from the resolved values');
skip 'netplan late-command not found in the template', 3 unless $snippet;
skip 'netplan late-command not found in the template', 5 unless $snippet;
$snippet =~ s/''/'/g; # undo the YAML single-quote escaping
for my $case (
@@ -132,9 +132,16 @@ SKIP: {
' match:', qq( macaddress: "$case->{mac}"),
($case->{setname} ne '' ? " set-name: $case->{setname}" : ()),
' dhcp4: true',
' dhcp4-overrides:',
' use-domains: true',
);
is($netplan, join("\n", @expected) . "\n",
"the netplan written for $case->{name} matches the resolved values");
# systemd-networkd defaults UseDomains to no, so a netplan carrying dhcp4: true alone
# drops the search domain DHCP offers, and no single-label name resolves on the node.
like($netplan, qr/^\s+use-domains: true$/m,
"the netplan for $case->{name} asks networkd to use the DHCP search domain");
}
}
+36 -11
View File
@@ -31,18 +31,25 @@ is( system("bash -n $pre_path 2>/dev/null"), 0,
# redirected into a scratch tree. Both substitutions are asserted: if either stops
# matching, this bails out rather than silently covering nothing or writing to /tmp.
my ($storage_block) = $script =~ /(^if \[ -d \/sys\/firmware\/efi \]; then\n.*?\n^fi$)/ms;
BAIL_OUT('the firmware branch that writes the partition file no longer matches')
die('the firmware branch that writes the partition file no longer matches')
unless $storage_block;
my $brackets = () = $storage_block =~ /\[ /g;
BAIL_OUT("the partitioning block now has $brackets bracket tests; the shadow below covers one")
unless $brackets == 1;
die('the firmware test the shadow below answers is gone')
unless $storage_block =~ /\[ -d \/sys\/firmware\/efi \]/;
die('the block no longer asks uname for the machine architecture')
unless $storage_block =~ /uname -m/;
my $sandbox = File::Temp::tempdir( CLEANUP => 1 );
my $partfile = File::Spec->catfile( $sandbox, 'partitionfile' );
# One redirect per firmware branch. Counting the branches rather than naming a number
# keeps the guard true when a branch is added, and still fails loudly if a redirect
# escapes the sandbox.
my $branches = () = $storage_block =~ /^\s*cat <<EOF >\/tmp\/partitionfile$/mg;
my $rewrites = ( $storage_block =~ s{/tmp/partitionfile}{$partfile}g );
BAIL_OUT("expected two partition-file redirects to sandbox, rewrote $rewrites")
unless $rewrites == 2;
die("the block writes the partition file in $branches places and $rewrites were rewritten")
unless $branches >= 2 && $rewrites == $branches;
die('a partition-file path escaped the sandbox')
if $storage_block =~ m{/tmp/partitionfile};
my %YAML_FOR;
@@ -54,13 +61,16 @@ sub partition_config_for {
open( my $fh, '>', $script ) or die "Unable to write $script: $!";
# `[` is shadowed rather than the condition rewritten: bash resolves a function
# ahead of the builtin, so the script's own test runs unmodified.
my $machine = $firmware eq 'prep' ? 'ppc64le' : 'x86_64';
print {$fh} <<"SHELL";
INSTALL_DISK=/dev/sdz
logger() { :; }
uname() { builtin echo $machine; }
# Only the firmware probe is answered here; every other test falls through to the builtin.
[() {
case "\$1 \$2" in
"-d /sys/firmware/efi") return @{[ $firmware eq 'uefi' ? 0 : 1 ]} ;;
*) builtin echo "unexpected bracket test: \$*" >&2; builtin return 2 ;;
*) builtin [ "\$@" ;;
esac
}
$storage_block
@@ -69,9 +79,9 @@ SHELL
unlink $partfile;
system( 'bash', $script ) == 0
or BAIL_OUT("the extracted partitioning block failed to run for $firmware");
or die("the extracted partitioning block failed to run for $firmware");
open( my $out_fh, '<', $partfile )
or BAIL_OUT("the partitioning block wrote no file for $firmware: $!");
or die("the partitioning block wrote no file for $firmware: $!");
my $yaml = do { local $/; <$out_fh> };
close($out_fh);
$YAML_FOR{$firmware} = $yaml;
@@ -110,7 +120,22 @@ ok( !exists $bios->{'efi-part'}, 'BIOS installs get no EFI partition' );
is( $bios->{'bios-grub'}{flag}, 'bios_grub', 'they get a bios_grub partition instead' );
is( $bios->{'disk-detected'}{grub_device}, 'true', 'and grub is installed to the disk' );
foreach my $firmware ( [ UEFI => $uefi ], [ BIOS => $bios ] ) {
# POWER firmware reads neither an ESP nor a bios_grub partition. It boots from a PReP
# partition, and curtin installs grub to that partition rather than to the disk.
my $prep = partition_config_for('prep');
ok( !exists $prep->{'efi-part'}, 'POWER installs get no EFI partition' );
ok( !exists $prep->{'bios-grub'}, 'POWER installs get no bios_grub partition' );
is( $prep->{'prep-part'}{type}, 'partition', 'POWER installs get a PReP partition' );
is( $prep->{'prep-part'}{device}, 'disk-detected', 'on the detected install disk' );
is( $prep->{'prep-part'}{flag}, 'prep', 'flagged prep, which is what SLOF reads' );
is( $prep->{'prep-part'}{number}, '1', 'as the first partition' );
is( $prep->{'prep-part'}{grub_device}, 'true', 'and grub is installed to it' );
isnt( $prep->{'disk-detected'}{grub_device}, 'true',
'not to the disk, which leaves POWER with nothing to boot' );
ok( !exists $prep->{'prep-part-fs'},
'the PReP partition carries no filesystem' );
foreach my $firmware ( [ UEFI => $uefi ], [ BIOS => $bios ], [ PReP => $prep ] ) {
my ( $name, $config ) = @{$firmware};
is( $config->{'root-part-fs'}{fstype}, 'ext4', "$name root filesystem is ext4" );
is( $config->{'root-part-mount'}{path}, '/', "$name mounts root at /" );
@@ -122,7 +147,7 @@ foreach my $firmware ( [ UEFI => $uefi ], [ BIOS => $bios ] ) {
# Subiquity re-serializes autoinstall.yaml and appends this file, so the block has
# to start at column 0 -- asserted on what was written, not on the heredoc.
foreach my $firmware ( [ UEFI => 'uefi' ], [ BIOS => 'bios' ] ) {
foreach my $firmware ( [ UEFI => 'uefi' ], [ BIOS => 'bios' ], [ PReP => 'prep' ] ) {
my ( $name, $key ) = @{$firmware};
like( partition_yaml_for($key), qr/\Astorage:\n version: 1\n/,
"$name config starts at column 0 with storage: version: 1" );
+2 -1
View File
@@ -59,10 +59,11 @@ unlike($tmpl, qr/tr ''A-F'' ''a-f''/,
'the shell no longer normalizes the MAC (Template.pm resolves mac.mac entries)');
unlike($tmpl, qr/cut -d''\|'' -f1/,
'the shell no longer splits mac.mac entries (Template.pm resolves them for this node)');
like($tmpl, qr/printf ''%s\\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \\"\$\{installmac\}\\"" " set-name: \$\{installnic\}" " dhcp4: true" >\/target\/etc\/netplan\/00-xcat-install\.yaml;/, 'target netplan printf stays on one shell line');
like($tmpl, qr/printf ''%s\\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \\"\$\{installmac\}\\"" " set-name: \$\{installnic\}" " dhcp4: true" " dhcp4-overrides:" " use-domains: true" >\/target\/etc\/netplan\/00-xcat-install\.yaml;/, 'target netplan printf stays on one shell line');
like($tmpl, qr/" macaddress: \\"\$\{installmac\}\\""/, 'target netplan matches by MAC address');
like($tmpl, qr/" set-name: \$\{installnic\}"/, 'target netplan sets the expected installnic name');
like($tmpl, qr/"\s+dhcp4: true"/, 'target netplan enables DHCPv4 on installnic');
like($tmpl, qr/"\s+use-domains: true"/, 'target netplan takes the search domain DHCP offers, so short names resolve on the node');
like($tmpl, qr/printf ''%s\\n'' ''#HOSTNAME#'' >\/target\/etc\/hostname/, 'template writes target hostname before disabling cloud-init');
like($tmpl, qr/sed -i ''s\/\^127\\\.0\\\.1\\\.1\.\*\/127\.0\.1\.1 #HOSTNAME#\/'' \/target\/etc\/hosts/, 'template updates target hosts entry for hostname');
like($tmpl, qr/touch \/target\/etc\/cloud\/cloud-init\.disabled/, 'target cloud-init is disabled after target netplan is written');
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env perl
# gettimezone names the timezone that goes into a kickstart or an autoyast profile, where the
# value must be one token. It found the name by comparing /etc/localtime against every file under
# /usr/share/zoneinfo, and when that pipeline failed it returned the sentence "Could not determine
# timezone checksum" as if it were a name.
#
# A cloud image that runs on UTC ships no /etc/localtime, so the scan finds nothing there, and
# anaconda refuses a timezone command that carries more than one argument.
#
# Utils.pm cannot be loaded here, so the routine is extracted and driven against a scratch root
# with the two collaborators it calls replaced.
use strict;
use warnings;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
my $source = "$FindBin::Bin/../../perl-xCAT/xCAT/Utils.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(gettimezone _zone_from_path)) {
my ($routine) = $content =~ /^(sub \Q$name\E\s*\n?\{.*?^\})/ms;
die("could not extract $name from Utils.pm") unless $routine;
push(@routines, $routine);
}
# The routines call each other unqualified and the caller reaches them through the class, so
# they go back into the package they came from.
eval "package xCAT::Utils;\n" . join("\n", @routines); ## no critic (BuiltinFunctions::ProhibitStringyEval)
die("could not load the timezone routines: $@") if $@;
# The collaborators gettimezone calls. The scan runs `find`, which must never look at the host
# this test runs on, so it answers from the scratch root instead.
our $SCAN_OUT = '';
our $SCAN_RC = 1;
{
no warnings 'once';
*xCAT::Utils::isAIX = sub { return 0 };
*xCAT::Utils::runcmd = sub { $::RUNCMD_RC = $SCAN_RC; return $SCAN_OUT };
}
#-----------------------------------------------------------------------------------------------
=head3 scratch_root
Descriptions:
A root with a zoneinfo tree, and /etc/localtime as a symlink into it when a zone is named.
Arguments:
$zone - the zone to link /etc/localtime to, or undef for a root with no /etc/localtime
Returns:
The root directory.
=cut
#-----------------------------------------------------------------------------------------------
sub scratch_root {
my ($zone) = @_;
my $root = tempdir(CLEANUP => 1);
make_path("$root/etc", "$root/usr/share/zoneinfo/America");
for my $name ('UTC', 'America/Sao_Paulo') {
open(my $fh, '>', "$root/usr/share/zoneinfo/$name") or die "create $name: $!";
print {$fh} "TZif";
close($fh) or die "close $name: $!";
}
symlink("../usr/share/zoneinfo/$zone", "$root/etc/localtime") or die "symlink: $!"
if defined $zone;
return $root;
}
is(xCAT::Utils->gettimezone(root => scratch_root('America/Sao_Paulo')), 'America/Sao_Paulo',
'the /etc/localtime symlink names the zone');
# No /etc/localtime, so the scan reports nothing.
my $none = xCAT::Utils->gettimezone(root => scratch_root(undef));
is($none, 'UTC', 'a root with no /etc/localtime falls back to UTC');
unlike($none, qr/\s/,
'the value is one token, which is all the kickstart timezone command accepts');
# /etc/timezone is consulted before the fallback.
my $root = scratch_root(undef);
open(my $tz_fh, '>', "$root/etc/timezone") or die "create /etc/timezone: $!";
print {$tz_fh} "America/Sao_Paulo\n";
close($tz_fh) or die "close /etc/timezone: $!";
is(xCAT::Utils->gettimezone(root => $root), 'America/Sao_Paulo',
'/etc/timezone names the zone when there is no symlink');
# The scan still answers for a root whose /etc/localtime is a copy rather than a symlink.
$root = scratch_root(undef);
open(my $copy_fh, '>', "$root/etc/localtime") or die "create /etc/localtime: $!";
print {$copy_fh} "TZif";
close($copy_fh) or die "close /etc/localtime: $!";
{
local $SCAN_OUT = "$root/usr/share/zoneinfo/America/Sao_Paulo\n";
local $SCAN_RC = 0;
is(xCAT::Utils->gettimezone(root => $root), 'America/Sao_Paulo',
'the zoneinfo scan names the zone when /etc/localtime is a copy');
}
done_testing();
+10 -6
View File
@@ -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 {
@@ -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.
@@ -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();
+16 -14
View File
@@ -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) {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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