From a603b9d6c2c8d54024d3273dd55e5aef6dee7f98 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:49:07 -0300 Subject: [PATCH 1/9] fix(xcat-core): otherpkgs sends the package manager output to syslog as one truncated message The otherpkgs postscript passes the whole package manager transaction to logger as a single message argument. rsyslog escapes every newline to #012 and cuts the message at 8 KiB, which is where the Failed:, Error: and summary lines sit. A line that starts with -- is also read as a logger option. The seven sites in xCAT/postscripts/otherpkgs that log $result now pipe it to logger. logger without a message argument reads standard input and sends one message per line. postscripts_otherpkgs.bats drives the upgrade block and the two preremove blocks with logger and the package manager shadowed, and counts the messages. Against the unfixed script the three tests report one message where three are expected. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 155 ++++++++++++++++++++++ xCAT/postscripts/otherpkgs | 14 +- 2 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 xCAT-test/bats/postscripts_otherpkgs.bats diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats new file mode 100644 index 000000000..1a1d3a6f2 --- /dev/null +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -0,0 +1,155 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + OTHERPKGS="$(repo_path 'xCAT/postscripts/otherpkgs')" + [ -r "$OTHERPKGS" ] || skip "$OTHERPKGS is required" + LOGGER_LOG="${BATS_TEST_TMPDIR}/logger.log" + CMD_LOG="${BATS_TEST_TMPDIR}/cmd.log" + : >"$LOGGER_LOG" + : >"$CMD_LOG" + export OTHERPKGS LOGGER_LOG CMD_LOG +} + +# logger writes one line per message it sends. A message that carries newlines stays on one +# line, with the newlines shown as "\n", so the line count is the message count. +shadow_logger() +{ + logger() + { + local msg="" + while [ $# -gt 0 ]; do + case "$1" in + -p | -t) shift 2 ;; + *) + msg="$*" + break + ;; + esac + done + if [ -n "$msg" ]; then + printf '%s\n' "${msg//$'\n'/\\n}" >>"$LOGGER_LOG" + else + local line + while IFS= read -r line; do + printf '%s\n' "$line" >>"$LOGGER_LOG" + done + fi + } +} + +logger_call() +{ + sed -n "${1}p" "$LOGGER_LOG" +} + +logger_calls() +{ + wc -l <"$LOGGER_LOG" | tr -d ' ' +} + +cmd_call() +{ + sed -n "${1}p" "$CMD_LOG" +} + +# A package manager that answers with PKG_STATUS and prints a three line transaction. +shadow_pkg_manager() +{ + fake_pkg() + { + printf '%s\n' "$*" >>"$CMD_LOG" + printf -- '--> Running transaction check\n' + printf 'Installed: foo-1.0\n' + printf 'Error: nothing provides bar\n' + return "${PKG_STATUS:-0}" + } + zypper() { fake_pkg "$@"; } + apt-get() { fake_pkg "$@"; } + xcat_apt_get() { fake_pkg "$@"; } + apt_get_update_if_repos_changed() { :; } +} + +# Answers with the shell if-block that starts at the first line after the anchor. +otherpkgs_block() +{ + local tail="${BATS_TEST_TMPDIR}/tail-$$" + awk -v anchor="$1" 'index($0, anchor) { copy = 1 } copy { print }' "$OTHERPKGS" >"$tail" + extract_shell_if_block "$tail" "$2" +} + +run_upgrade_block() +{ + local block + block="$(otherpkgs_block '#now update the existing rpms' 'if [ $hasyum -eq 1 ]; then')" || return 99 + local hasyum=0 haszypper=0 hasapt=0 + eval "$1=1" + local envlist="" yumcmd=fake_pkg VERBOSE= log_label=otherpkgs RETURNVAL=0 REPOFILE=/dev/null result="" + shadow_logger + shadow_pkg_manager + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +run_repo_preremove_block() +{ + local block + block="$(otherpkgs_block '#Now we have parsed the input' 'if [ "$repo_pkgs_preremove" != "" ]; then')" || return 99 + local hasyum=0 haszypper=0 hasapt=0 + eval "$1=1" + local envlist="" yumcmd=fake_pkg VERBOSE= log_label=otherpkgs RETURNVAL=0 REPOFILE=/dev/null result="" + local repo_pkgs_preremove="oldfoo" + shadow_logger + shadow_pkg_manager + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +run_plain_preremove_block() +{ + local block + block="$(otherpkgs_block '#Now we have parsed the input' 'if [ "$plain_pkgs_preremove" != "" ]; then')" || return 99 + local envlist="" VERBOSE= log_label=otherpkgs RETURNVAL=0 result="" + local sremovecommand=fake_pkg plain_pkgs_preremove="oldfoo" + shadow_logger + shadow_pkg_manager + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +@test "otherpkgs sends the package manager transaction to syslog one line per message" { + run run_upgrade_block hasyum + [ "$status" -eq 0 ] + [ "$(logger_calls)" -eq 3 ] + [ "$(logger_call 1)" = "--> Running transaction check" ] + [ "$(logger_call 2)" = "Installed: foo-1.0" ] + [ "$(logger_call 3)" = "Error: nothing provides bar" ] +} + +@test "the zypper and apt upgrade paths also send one message per output line" { + run run_upgrade_block haszypper + [ "$status" -eq 0 ] + [ "$(logger_calls)" -eq 3 ] + + : >"$LOGGER_LOG" + run run_upgrade_block hasapt + [ "$status" -eq 0 ] + [ "$(logger_calls)" -eq 3 ] +} + +@test "the remove paths also send one message per output line" { + local manager + for manager in hasyum haszypper hasapt; do + : >"$LOGGER_LOG" + run run_repo_preremove_block "$manager" + [ "$status" -eq 0 ] + [ "$(logger_calls)" -eq 3 ] + done + + : >"$LOGGER_LOG" + run run_plain_preremove_block + [ "$status" -eq 0 ] + [ "$(logger_calls)" -eq 3 ] +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index c89256e86..7e8549357 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -884,7 +884,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi @@ -898,7 +898,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi @@ -912,7 +912,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi @@ -933,7 +933,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi @@ -946,7 +946,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi @@ -960,7 +960,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi @@ -976,7 +976,7 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R fi - logger -p local4.info -t $log_label "$result" + printf '%s\n' "$result" | logger -p local4.info -t $log_label if [ $VERBOSE ]; then echo "$result" fi From 42746fae5ae850bcd0ed11362685bf460487f2bb Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:50:09 -0300 Subject: [PATCH 2/9] fix(xcat-core): otherpkgs logs a failed package install as installed In xCAT/postscripts/otherpkgs the success message runs after the branch that records a non-zero return, not inside an else. A node that fails to install its otherpkgs therefore reports both "... failed." and "... installed." to syslog, and a reader that greps for the success line sees a clean install. The same shape covers the four install sites and the four removal sites. Each success message now sits in the else branch of the status test. postscripts_otherpkgs.bats drives the install and postremove blocks with a package manager that fails, and uses refute_grep to require that no "installed."/"removed." message is sent. Against the unfixed script both tests find the success message. refute_grep is new in helpers/shell_source.bash, because bash ignores errexit for a command inverted with "!". Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/helpers/shell_source.bash | 10 ++++ xCAT-test/bats/postscripts_otherpkgs.bats | 68 +++++++++++++++++++++++ xCAT/postscripts/otherpkgs | 38 ++++++++----- 3 files changed, 101 insertions(+), 15 deletions(-) diff --git a/xCAT-test/bats/helpers/shell_source.bash b/xCAT-test/bats/helpers/shell_source.bash index 61a0bea8d..4f4384fb2 100644 --- a/xCAT-test/bats/helpers/shell_source.bash +++ b/xCAT-test/bats/helpers/shell_source.bash @@ -130,3 +130,13 @@ extract_first_matching_line() } ' "$file" } + +# grep that fails when the pattern IS present. +# +# Do not write "! grep ..." for this. bash ignores errexit for a command inverted with "!", +# so such a line never fails a test unless it is the last line of one. +refute_grep() +{ + ! grep "$@" + return $? +} diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index 1a1d3a6f2..0883cacb7 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -153,3 +153,71 @@ run_plain_preremove_block() [ "$status" -eq 0 ] [ "$(logger_calls)" -eq 3 ] } + +run_install_block() +{ + local block + block="$(otherpkgs_block '#installation using yum/dnf or zypper' 'if [ "$repo_pkgs" != "" ]; then')" || return 99 + local hasyum=0 haszypper=0 hasapt=0 + eval "$1=1" + local envlist="" yumcmd=fake_pkg VERBOSE= log_label=otherpkgs RETURNVAL=0 REPOFILE=/dev/null result="" + local repo_pkgs="foo bar" + shadow_logger + shadow_pkg_manager + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +run_repo_postremove_block() +{ + local block + block="$(otherpkgs_block '#remove more rpms if specified with' 'if [ "$repo_pkgs_postremove" != "" ]; then')" || return 99 + local hasyum=0 haszypper=0 hasapt=0 + eval "$1=1" + local envlist="" yumcmd=fake_pkg VERBOSE= log_label=otherpkgs RETURNVAL=0 REPOFILE=/dev/null result="" + local repo_pkgs_postremove="oldfoo" + shadow_logger + shadow_pkg_manager + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +@test "a failed package install is not logged as installed" { + local manager + for manager in hasyum haszypper hasapt; do + : >"$LOGGER_LOG" + PKG_STATUS=1 run run_install_block "$manager" + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=1'* ]] + refute_grep -q 'foo bar installed\.' "$LOGGER_LOG" + grep -q 'failed\.' "$LOGGER_LOG" + done +} + +@test "a successful package install is logged as installed" { + local manager + for manager in hasyum haszypper hasapt; do + : >"$LOGGER_LOG" + run run_install_block "$manager" + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=0'* ]] + grep -q 'foo bar installed\.' "$LOGGER_LOG" + refute_grep -q 'failed\.' "$LOGGER_LOG" + done +} + +@test "a failed package removal is not logged as removed" { + local manager + for manager in hasyum haszypper hasapt; do + : >"$LOGGER_LOG" + PKG_STATUS=1 run run_repo_postremove_block "$manager" + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=1'* ]] + refute_grep -q 'oldfoo removed\.' "$LOGGER_LOG" + done + + : >"$LOGGER_LOG" + run run_repo_postremove_block hasyum + [ "$status" -eq 0 ] + grep -q 'oldfoo removed\.' "$LOGGER_LOG" +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 7e8549357..d3018eb7d 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -994,8 +994,9 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R logger -p local4.err -t $log_label "$envlist $yumcmd -y install $repo_pkgs failed." + else + logger -p local4.info -t $log_label "$repo_pkgs installed." fi - logger -p local4.info -t $log_label "$repo_pkgs installed." if [ $VERBOSE ]; then echo "$result" fi @@ -1008,8 +1009,9 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R logger -p local4.err -t $log_label "$envlist zypper install -y $repo_pkgs 2>&1 failed." - fi - logger -p local4.info -t $log_label "$repo_pkgs installed." + else + logger -p local4.info -t $log_label "$repo_pkgs installed." + fi if [ $VERBOSE ]; then echo "$result" fi @@ -1029,8 +1031,9 @@ EOF` if [ $R -ne 0 ]; then RETURNVAL=$R logger -p local4.err -t $log_label "install $repo_pkgs failed." - fi - logger -p local4.info -t $log_label "$repo_pkgs installed." + else + logger -p local4.info -t $log_label "$repo_pkgs installed." + fi if [ $VERBOSE ]; then echo "$result" fi @@ -1066,8 +1069,9 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R - fi - logger -p local4.info -t $log_label "$plain_pkgs installed." + else + logger -p local4.info -t $log_label "$plain_pkgs installed." + fi if [ $VERBOSE ]; then echo "$result" fi @@ -1090,8 +1094,9 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R - fi - logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." + else + logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." + fi if [ $VERBOSE ]; then echo "$result" fi @@ -1103,8 +1108,9 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R - fi - logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." + else + logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." + fi if [ $VERBOSE ]; then echo "$result" fi @@ -1117,8 +1123,9 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R - fi - logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." + else + logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." + fi if [ $VERBOSE ]; then echo "$result" fi @@ -1133,8 +1140,9 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R - fi - logger -p local4.info -t $log_label "$plain_pkgs_postremove removed." + else + logger -p local4.info -t $log_label "$plain_pkgs_postremove removed." + fi if [ $VERBOSE ]; then echo "$result" fi From 70e31f2f15d9eb0a6f721bbd9a0385a0599f29ce Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:54:59 -0300 Subject: [PATCH 3/9] fix(xcat-core): the otherpkgs url repository guard tests a literal string Line 672 of xCAT/postscripts/otherpkgs reads [ -n "OTHERPKGDIR_INTERNET" ]. The $ is missing, so the test is on the name of the variable and is always true. The guard cannot select the case it was written for. The guard now tests $OTHERPKGDIR_INTERNET. postscripts_otherpkgs.bats extracts the condition from the script and runs it with the variable empty and with an http entry. Against the unfixed script the empty case is true. The block is inert when the variable is empty, because the loop inside it iterates an empty array, so the run of the postscript does not change. The guard is still wrong. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 15 +++++++++++++++ xCAT/postscripts/otherpkgs | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index 0883cacb7..4dc227d59 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -221,3 +221,18 @@ run_repo_postremove_block() [ "$status" -eq 0 ] grep -q 'oldfoo removed\.' "$LOGGER_LOG" } + +@test "the url repository guard is false when OTHERPKGDIR has no http entry" { + local guard cond + guard="$(extract_first_matching_line "$OTHERPKGS" 'OTHERPKGDIR_INTERNET" *[]] *; *then')" || return 99 + cond="${guard#*if }" + cond="${cond%%;then*}" + + OTHERPKGDIR_INTERNET="" + run eval "$cond" + [ "$status" -ne 0 ] + + OTHERPKGDIR_INTERNET="http://192.0.2.1/repo," + run eval "$cond" + [ "$status" -eq 0 ] +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index d3018eb7d..0d66899ca 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -669,7 +669,7 @@ while [ $op_index -le $OTHERPKGS_INDEX ]; do urlrepoindex=0 #add repo for url repos in otherpkgdir - if [ -n "OTHERPKGDIR_INTERNET" ];then + if [ -n "$OTHERPKGDIR_INTERNET" ];then OIFS=$IFS IFS=',' OTHERPKGDIRLIST_INTERNET=($OTHERPKGDIR_INTERNET) From 7bd04f012d10fcd244304dc546240af096ff8990 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:55:25 -0300 Subject: [PATCH 4/9] fix(xcat-core): otherpkgs writes a trailing space into the generated baseurl Line 297 of xCAT/postscripts/otherpkgs appends each http OTHERPKGDIR entry as "${dir} ,". The list is split on the comma alone, so every url keeps the space, and the url repository block writes "baseurl= " into the yum repository file and "deb " into the apt source. The separator no longer carries the space. postscripts_otherpkgs.bats runs the split and the url repository block and compares the generated baseurl and deb lines with the url. Against the unfixed script both differ by the trailing space. dnf 4.20 strips trailing whitespace from an ini value, and apt splits a source line on whitespace, so no package manager reads the space today. The generated file is still wrong. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 29 +++++++++++++++++++++++ xCAT/postscripts/otherpkgs | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index 4dc227d59..a21bb0893 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -236,3 +236,32 @@ run_repo_postremove_block() run eval "$cond" [ "$status" -eq 0 ] } + +# Runs the OTHERPKGDIR split and then the url repository block, and leaves the repository +# files the url block wrote under BATS_TEST_TMPDIR. +run_url_repo_block() +{ + local split url_block + split="$(extract_shell_if_block "$OTHERPKGS" 'if [ -n "$OTHERPKGDIR" ]; then')" || return 99 + url_block="$(otherpkgs_block '#add repo for url repos in otherpkgdir' 'OTHERPKGDIR_INTERNET')" || return 99 + local OTHERPKGDIR="$1" OTHERPKGDIR_INTERNET="" OTHERPKGDIR_LOCAL="" + local hasyum="${2:-1}" haszypper=0 hasapt="${3:-0}" + local repo_base="$BATS_TEST_TMPDIR" urlrepoindex=0 + eval "$split" + eval "$url_block" + printf 'urlrepoindex=%s\n' "$urlrepoindex" +} + +@test "the generated yum baseurl carries no trailing space" { + run run_url_repo_block 'http://192.0.2.1/repo-a,/install/post/otherpkgs,http://192.0.2.1/repo-b' + [ "$status" -eq 0 ] + [[ "$output" == *'urlrepoindex=2'* ]] + [ "$(grep '^baseurl=' "${BATS_TEST_TMPDIR}/xCAT-otherpkgs0.repo")" = "baseurl=http://192.0.2.1/repo-a" ] + [ "$(grep '^baseurl=' "${BATS_TEST_TMPDIR}/xCAT-otherpkgs1.repo")" = "baseurl=http://192.0.2.1/repo-b" ] +} + +@test "the generated apt source carries no trailing space" { + run run_url_repo_block 'http://192.0.2.1/repo-a' 0 1 + [ "$status" -eq 0 ] + [ "$(cat "${BATS_TEST_TMPDIR}/xCAT-otherpkgs0.list")" = "deb http://192.0.2.1/repo-a" ] +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 0d66899ca..507de1078 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -294,7 +294,7 @@ if [ -n "$OTHERPKGDIR" ]; then do dirtype=${dir:0:4} if [ $dirtype = 'http' ]; then - OTHERPKGDIR_INTERNET="${OTHERPKGDIR_INTERNET}${dir} ," + OTHERPKGDIR_INTERNET="${OTHERPKGDIR_INTERNET}${dir}," else OTHERPKGDIR_LOCAL=$dir fi From 73c293f03e8a1ef603e6ec572d2515e99575439b Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:56:14 -0300 Subject: [PATCH 5/9] fix(xcat-core): otherpkgs refreshes a zypper repository it did not add The local otherpkgs repository is written with the alias xcat-otherpkgs$localrepoindex, where localrepoindex is urlrepoindex plus the array index. Lines 833 and 843 of xCAT/postscripts/otherpkgs then refresh and delete xcat-otherpkgs$index. With http OTHERPKGDIR entries present urlrepoindex is not zero, so zypper names a repository that block did not add: the refresh fails for a repository that exists, and the delete removes another one. Both lines now use $localrepoindex. The log and the echo of a failed SDK repository add name $bname, which is what zypper ar used; they said bname without the $. postscripts_otherpkgs.bats drives the add, the refresh and the delete with zypper shadowed and urlrepoindex set to 2, and reads the alias back out of the repository file the script wrote. Against the unfixed script the refresh and the delete name xcat-otherpkgs0 while the file says xcat-otherpkgs2, and the SDK log carries no repository name. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 69 +++++++++++++++++++++++ xCAT/postscripts/otherpkgs | 8 +-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index a21bb0893..3a7ffb4cc 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -265,3 +265,72 @@ run_url_repo_block() [ "$status" -eq 0 ] [ "$(cat "${BATS_TEST_TMPDIR}/xCAT-otherpkgs0.list")" = "deb http://192.0.2.1/repo-a" ] } + +# Drives the lines that name the local otherpkgs repository for zypper: the alias written into +# the repository file, and the alias the refresh and the delete use. +run_zypper_local_repo() +{ + local urlrepoindex="$1" index="$2" + local repo_base="$BATS_TEST_TMPDIR" mounted=1 whole_path=/install/post/otherpkgs/sles15/x86_64 + local OSVER=sles15 VERBOSE= localrepoindex REPOFILE rc=1 result="" path=/pkgdir + zypper() + { + printf '%s\n' "$*" >>"$CMD_LOG" + case "$1" in + ar) sed -n '1s/^\[\(.*\)\]$/added=\1/p' "$3" >>"$CMD_LOG" ;; + esac + return "${ZYPPER_STATUS:-0}" + } + array_set_element() { :; } + # The extraction is kept apart from the eval: a failing zypper must not read as a failed + # extraction. + local pattern line + for pattern in \ + 'localrepoindex=' \ + 'REPOFILE="[$]repo_base/xCAT-otherpkgs[$]localrepoindex.repo"' \ + 'echo "[[]xcat-otherpkgs[$]localrepoindex[]]"' \ + 'result=`zypper ar -c [$]REPOFILE`' \ + 'zypper --non-interactive refresh xcat-otherpkgs' \ + 'result=`zypper sd xcat-otherpkgs'; do + line="$(extract_first_matching_line "$OTHERPKGS" "$pattern")" || return 99 + eval "$line" + done + return 0 +} + +@test "zypper refreshes the otherpkgs repository it added" { + run run_zypper_local_repo 2 0 + [ "$status" -eq 0 ] + [ "$(cmd_call 2)" = "added=xcat-otherpkgs2" ] + [ "$(cmd_call 3)" = "--non-interactive refresh xcat-otherpkgs2" ] +} + +@test "zypper deletes the otherpkgs repository it added when the refresh fails" { + ZYPPER_STATUS=1 run run_zypper_local_repo 2 0 + [ "$status" -eq 0 ] + [ "$(cmd_call 2)" = "added=xcat-otherpkgs2" ] + [ "$(cmd_call 4)" = "sd xcat-otherpkgs2" ] +} + +run_sdk_block() +{ + local block + block="$(otherpkgs_block '#adds SDK repository' 'if [ "$SDKDIR" != "" ]; then')" || return 99 + local SDKDIR=/install/sles15/x86_64/sdk1 OSVER=sles15 mounted=1 VERBOSE= log_label=otherpkgs result="" + local NFSSERVER=192.0.2.1 HTTPPORT=80 + eval "$(extract_shell_function "$OTHERPKGS" pmatch)" || return 99 + shadow_logger + zypper() + { + printf 'zypper failed\n' + return 1 + } + eval "$block" + return 0 +} + +@test "a failed SDK repository add is logged with the repository name" { + run run_sdk_block + [ "$status" -eq 0 ] + grep -q 'xCAT-sles15-sdk1' "$LOGGER_LOG" +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 507de1078..49383801c 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -576,9 +576,9 @@ if ( ! ( pmatch "$OSVER" "sles10*" ) && [ $haszypper -eq 1 ] ); then result=`zypper ar $sdk_src xCAT-$OSVER-$bname 2>&1` if [ $? -ne 0 ]; then if ( ! pmatch "$result" "*exists*" ); then - logger -t $log_label -p local4.info "otherpkgs: zypper ar $sdk_src xCAT-$OSVER-bname\n $result" + logger -t $log_label -p local4.info "otherpkgs: zypper ar $sdk_src xCAT-$OSVER-$bname\n $result" if [ $VERBOSE ]; then - echo "otherpkgs: zypper ar $sdk_src xCAT-$OSVER-bname" + echo "otherpkgs: zypper ar $sdk_src xCAT-$OSVER-$bname" echo " $result" fi fi @@ -830,7 +830,7 @@ EOF` result=`zypper ar -c $REPOFILE` fi - result=`zypper --non-interactive refresh xcat-otherpkgs$index 2>&1` + result=`zypper --non-interactive refresh xcat-otherpkgs$localrepoindex 2>&1` if [ $? -eq 0 ]; then rc=0 array_set_element repo_path $index $path @@ -840,7 +840,7 @@ EOF` rc=0 array_set_element repo_path $index $path else - result=`zypper sd xcat-otherpkgs$index` + result=`zypper sd xcat-otherpkgs$localrepoindex` fi fi elif [ $hasapt -eq 1 ]; then From 402a6f4a45c70594435f916b934f10e39b7a22a3 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:58:14 -0300 Subject: [PATCH 6/9] fix(xcat-core): otherpkgs prints the whole package list on every run The otherpkgs postscript writes two diagnostic lines for each package sublist: the split package array with its size, and the detected package manager. On a list with tens of entries these lines fill the updatenode output and the node log, and hide the install results. The two echo commands sit in xCAT/postscripts/otherpkgs, after the IFS split that builds pkgsarray, and no condition guards them. Every other diagnostic in the script runs only when VERBOSE is set. This change puts the two lines in the same "if [ $VERBOSE ]" block the rest of the script uses. xCAT-test/bats/postscripts_otherpkgs.bats runs the split and the lines that follow it. One case asserts no output when VERBOSE is empty, and fails on the parent commit. A second case asserts both lines are still printed when VERBOSE is set. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 24 +++++++++++++++++++++++ xCAT/postscripts/otherpkgs | 6 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index 3a7ffb4cc..26495412e 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -334,3 +334,27 @@ run_sdk_block() [ "$status" -eq 0 ] grep -q 'xCAT-sles15-sdk1' "$LOGGER_LOG" } + +# Drives the package list split and the two diagnostic lines that follow it. The range ends on +# the "for" line that starts the package loop, and that line is dropped: the loop is not part of +# what is measured. +run_pkglist_diagnostics() +{ + local range + range="$(extract_line_range "$OTHERPKGS" 'pkgsarray=' '^[[:space:]]*for x in')" || return 99 + local pkglist="foo bar" hasyum=1 yumcmd=dnf hasapt=0 haszypper=0 oifs=$IFS + eval "$(printf '%s\n' "$range" | sed '$d')" +} + +@test "the package list diagnostics print nothing when VERBOSE is not set" { + VERBOSE= run run_pkglist_diagnostics + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "the package list diagnostics print the list and the package manager under VERBOSE" { + VERBOSE=1 run run_pkglist_diagnostics + [ "$status" -eq 0 ] + [[ "$output" == *'pkgsarray: foo bar, 2'* ]] + [[ "$output" == *'yum/dnf: 1 (dnf), apt: 0, zypper: 0'* ]] +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 49383801c..70a90376a 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -708,8 +708,10 @@ while [ $op_index -le $OTHERPKGS_INDEX ]; do IFS=$',' pkgsarray=($pkglist) IFS=$oifs - echo "pkgsarray: ${pkgsarray[@]}, ${#pkgsarray[@]}" - echo "yum/dnf: $hasyum ($yumcmd), apt: $hasapt, zypper: $haszypper" + if [ $VERBOSE ]; then + echo "pkgsarray: ${pkgsarray[@]}, ${#pkgsarray[@]}" + echo "yum/dnf: $hasyum ($yumcmd), apt: $hasapt, zypper: $haszypper" + fi for x in ${pkgsarray[@]} do #check if the file name starts with -- or -. From e058c9afd9020bbcfe327c403d38191a6b65b85f Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:59:09 -0300 Subject: [PATCH 7/9] fix(xcat-core): otherpkgs does not log a failed rpm install or removal A failed package install through the rpm or dpkg fallback leaves no record in syslog. The same holds for the four package removal branches that run after the install. The postscript sets its return code, but a reader of the node log sees nothing, because the package manager output goes to the console only when VERBOSE is set. In xCAT/postscripts/otherpkgs the fallback install block, and the yum, zypper, apt and rpm removal blocks, set RETURNVAL on a non-zero status and stop there. Only the three repository install branches send a message with local4.err. This change adds the same local4.err message to the five branches that have none. Each message names the command that ran, as the repository install branches do. xCAT-test/bats/postscripts_otherpkgs.bats drives each block with a package manager that returns a failure. Two cases assert the message is present, and both fail on the parent commit. Two more assert the success path still logs "installed." and "removed." alone. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 70 +++++++++++++++++++++++ xCAT/postscripts/otherpkgs | 5 ++ 2 files changed, 75 insertions(+) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index 26495412e..dd0c2be05 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -358,3 +358,73 @@ run_pkglist_diagnostics() [[ "$output" == *'pkgsarray: foo bar, 2'* ]] [[ "$output" == *'yum/dnf: 1 (dnf), apt: 0, zypper: 0'* ]] } + +run_plain_install_block() +{ + local block + block="$(otherpkgs_block '#Handle the rest with rpm' 'if [ "$plain_pkgs" != "" -a -n "$OTHERPKGDIR" ]; then')" || return 99 + local envlist="" VERBOSE= log_label=otherpkgs RETURNVAL=0 result="" + local supdatecommand=fake_pkg plain_pkgs="foo bar" mounted=1 + local OTHERPKGDIR="$BATS_TEST_TMPDIR" + shadow_logger + shadow_pkg_manager + # The block changes directory. A subshell keeps the test in its own directory. + ( + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" + ) +} + +run_plain_postremove_block() +{ + local block + block="$(otherpkgs_block '#remove more rpms if specified with' 'if [ "$plain_pkgs_postremove" != "" ]; then')" || return 99 + local envlist="" VERBOSE= log_label=otherpkgs RETURNVAL=0 result="" + local sremovecommand=fake_pkg plain_pkgs_postremove="oldfoo" + shadow_logger + shadow_pkg_manager + eval "$block" + printf 'RETURNVAL=%s\n' "$RETURNVAL" +} + +@test "a failed rpm fallback install is logged as failed" { + PKG_STATUS=1 run run_plain_install_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=1'* ]] + refute_grep -q 'foo bar installed\.' "$LOGGER_LOG" + grep -q 'foo bar failed\.' "$LOGGER_LOG" +} + +@test "a successful rpm fallback install is logged as installed" { + run run_plain_install_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=0'* ]] + grep -q 'foo bar installed\.' "$LOGGER_LOG" + refute_grep -q 'failed\.' "$LOGGER_LOG" +} + +@test "a failed package removal is logged as failed" { + local manager + for manager in hasyum haszypper hasapt; do + : >"$LOGGER_LOG" + PKG_STATUS=1 run run_repo_postremove_block "$manager" + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=1'* ]] + grep -q 'oldfoo failed\.' "$LOGGER_LOG" + done + + : >"$LOGGER_LOG" + PKG_STATUS=1 run run_plain_postremove_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=1'* ]] + refute_grep -q 'oldfoo removed\.' "$LOGGER_LOG" + grep -q 'oldfoo failed\.' "$LOGGER_LOG" +} + +@test "a successful package removal is logged as removed only" { + run run_plain_postremove_block + [ "$status" -eq 0 ] + [[ "$output" == *'RETURNVAL=0'* ]] + grep -q 'oldfoo removed\.' "$LOGGER_LOG" + refute_grep -q 'failed\.' "$LOGGER_LOG" +} diff --git a/xCAT/postscripts/otherpkgs b/xCAT/postscripts/otherpkgs index 70a90376a..e666dccb9 100755 --- a/xCAT/postscripts/otherpkgs +++ b/xCAT/postscripts/otherpkgs @@ -1071,6 +1071,7 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R + logger -p local4.err -t $log_label "$envlist $supdatecommand $plain_pkgs failed." else logger -p local4.info -t $log_label "$plain_pkgs installed." fi @@ -1096,6 +1097,7 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R + logger -p local4.err -t $log_label "$envlist $yumcmd -y remove $repo_pkgs_postremove failed." else logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." fi @@ -1110,6 +1112,7 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R + logger -p local4.err -t $log_label "$envlist zypper remove -y $repo_pkgs_postremove failed." else logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." fi @@ -1125,6 +1128,7 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R + logger -p local4.err -t $log_label "$envlist apt-get -y remove $repo_pkgs_postremove failed." else logger -p local4.info -t $log_label "$repo_pkgs_postremove removed." fi @@ -1142,6 +1146,7 @@ EOF` R=$? if [ $R -ne 0 ]; then RETURNVAL=$R + logger -p local4.err -t $log_label "$envlist $sremovecommand $plain_pkgs_postremove failed." else logger -p local4.info -t $log_label "$plain_pkgs_postremove removed." fi From b6e2810860bceb7ec386e1095540f252299e79f0 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:57:33 -0300 Subject: [PATCH 8/9] fix(xcat-core): the otherpkgs bats anchors match more than one block Four cases in xCAT-test/bats/postscripts_otherpkgs.bats fail on the integrated tree. extract_shell_if_block reports that the upgrade anchor occurs 4 times and the url repository anchor occurs 2 times, where 1 is expected. "if [ $hasyum -eq 1 ]; then" opens the upgrade block and also the yum branch of the preremove, the install and the postremove blocks. "OTHERPKGDIR_INTERNET" matches the guard that opens the url repository block and an assignment inside that block. The earlier helper took the first match and said nothing. otherpkgs_block now passes NTH and TOTAL through to extract_shell_if_block. The two upgrade cases take occurrence 1 of 4, so a fifth identical line fails the test instead of moving it. The two url repository cases anchor on the guard line itself, which occurs once. Both blocks the earlier helper took are the blocks the assertions describe, so no case measured the wrong code. On the integrated tree bats -r xCAT-test/bats gives 113 ok and 0 not ok. On this branch, where the helper is not yet hardened, the file gives 18 ok. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index dd0c2be05..cb419777b 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -72,18 +72,24 @@ shadow_pkg_manager() apt_get_update_if_repos_changed() { :; } } -# Answers with the shell if-block that starts at the first line after the anchor. +# otherpkgs_block ANCHOR START [NTH TOTAL] +# Answers with the shell if-block that starts at the first line after the anchor. NTH and TOTAL +# go to extract_shell_if_block, which counts START in the text from the anchor to the end of the +# file. Name a START that occurs once where one exists; pass NTH and TOTAL where the candidate +# lines are the same text. otherpkgs_block() { local tail="${BATS_TEST_TMPDIR}/tail-$$" awk -v anchor="$1" 'index($0, anchor) { copy = 1 } copy { print }' "$OTHERPKGS" >"$tail" - extract_shell_if_block "$tail" "$2" + extract_shell_if_block "$tail" "$2" "$3" "$4" } run_upgrade_block() { local block - block="$(otherpkgs_block '#now update the existing rpms' 'if [ $hasyum -eq 1 ]; then')" || return 99 + # The upgrade block opens with the same line as the yum branch of the preremove, the install + # and the postremove blocks. The upgrade block is the first of the four. + block="$(otherpkgs_block '#now update the existing rpms' 'if [ $hasyum -eq 1 ]; then' 1 4)" || return 99 local hasyum=0 haszypper=0 hasapt=0 eval "$1=1" local envlist="" yumcmd=fake_pkg VERBOSE= log_label=otherpkgs RETURNVAL=0 REPOFILE=/dev/null result="" @@ -243,7 +249,7 @@ run_url_repo_block() { local split url_block split="$(extract_shell_if_block "$OTHERPKGS" 'if [ -n "$OTHERPKGDIR" ]; then')" || return 99 - url_block="$(otherpkgs_block '#add repo for url repos in otherpkgdir' 'OTHERPKGDIR_INTERNET')" || return 99 + url_block="$(otherpkgs_block '#add repo for url repos in otherpkgdir' 'if [ -n "$OTHERPKGDIR_INTERNET" ];then')" || return 99 local OTHERPKGDIR="$1" OTHERPKGDIR_INTERNET="" OTHERPKGDIR_LOCAL="" local hasyum="${2:-1}" haszypper=0 hasapt="${3:-0}" local repo_base="$BATS_TEST_TMPDIR" urlrepoindex=0 From e19a10396eda625143a165fbb7c1b50547d6bfea Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 17 Sep 2026 07:34:03 -0300 Subject: [PATCH 9/9] test(xcat-core): the otherpkgs zypper test deletes the repository in both cases run_zypper_local_repo extracted six lines of the zypper branch of otherpkgs and evaluated each one in turn. The branch it measures is an if/else: the repository is deleted only when the refresh fails. Evaluating the lines separately ran the delete every time, so the test showed that zypper sd is reachable and never that a repository which refreshes is kept. The helper now evaluates the whole branch, from the #use zypper comment to the apt branch that follows, with pmatch lifted from the same file. The success case asserts rc=0 and no zypper sd; the failure case keeps its delete assertion. Flipping the refresh test in otherpkgs to "if [ $? -ne 0 ]" turns both cases red. The same mutation left the previous helper green, because it never evaluated that line. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/bats/postscripts_otherpkgs.bats | 34 +++++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/xCAT-test/bats/postscripts_otherpkgs.bats b/xCAT-test/bats/postscripts_otherpkgs.bats index cb419777b..500ffe5fb 100644 --- a/xCAT-test/bats/postscripts_otherpkgs.bats +++ b/xCAT-test/bats/postscripts_otherpkgs.bats @@ -272,8 +272,9 @@ run_url_repo_block() [ "$(cat "${BATS_TEST_TMPDIR}/xCAT-otherpkgs0.list")" = "deb http://192.0.2.1/repo-a" ] } -# Drives the lines that name the local otherpkgs repository for zypper: the alias written into -# the repository file, and the alias the refresh and the delete use. +# Drives the zypper branch that adds the local otherpkgs repository, refreshes it and deletes +# it again when the refresh fails. The branch is evaluated whole, so the success case shows +# that a repository which refreshes is kept. run_zypper_local_repo() { local urlrepoindex="$1" index="$2" @@ -288,32 +289,47 @@ run_zypper_local_repo() return "${ZYPPER_STATUS:-0}" } array_set_element() { :; } - # The extraction is kept apart from the eval: a failing zypper must not read as a failed - # extraction. + eval "$(extract_shell_function "$OTHERPKGS" pmatch)" || return 99 + + # The three lines that name the repository are straight-line assignments, so each one is + # taken on its own. The zypper branch that follows is a branch: it is taken whole, because + # what is measured is which arm runs. Keep the extraction apart from the eval, so a failing + # zypper does not read as a failed extraction. local pattern line for pattern in \ 'localrepoindex=' \ 'REPOFILE="[$]repo_base/xCAT-otherpkgs[$]localrepoindex.repo"' \ - 'echo "[[]xcat-otherpkgs[$]localrepoindex[]]"' \ - 'result=`zypper ar -c [$]REPOFILE`' \ - 'zypper --non-interactive refresh xcat-otherpkgs' \ - 'result=`zypper sd xcat-otherpkgs'; do + 'echo "[[]xcat-otherpkgs[$]localrepoindex[]]"'; do line="$(extract_first_matching_line "$OTHERPKGS" "$pattern")" || return 99 eval "$line" done + + # The range ends on the apt branch that follows; its two lines are dropped. + local branch + branch="$(extract_line_range "$OTHERPKGS" '#use zypper' '#use apt')" || return 99 + branch="$(printf '%s\n' "$branch" | head -n -2)" + case "$branch" in + *'zypper sd xcat-otherpkgs'*) ;; + *) return 99 ;; + esac + eval "$branch" + printf 'rc=%s\n' "$rc" return 0 } -@test "zypper refreshes the otherpkgs repository it added" { +@test "zypper keeps the otherpkgs repository it added when the refresh succeeds" { run run_zypper_local_repo 2 0 [ "$status" -eq 0 ] + [ "$output" = "rc=0" ] [ "$(cmd_call 2)" = "added=xcat-otherpkgs2" ] [ "$(cmd_call 3)" = "--non-interactive refresh xcat-otherpkgs2" ] + refute_grep -q '^sd ' "$CMD_LOG" } @test "zypper deletes the otherpkgs repository it added when the refresh fails" { ZYPPER_STATUS=1 run run_zypper_local_repo 2 0 [ "$status" -eq 0 ] + [ "$output" = "rc=1" ] [ "$(cmd_call 2)" = "added=xcat-otherpkgs2" ] [ "$(cmd_call 4)" = "sd xcat-otherpkgs2" ] }