diff --git a/xCAT-test/autotest/testcase/commoncmd/nic_cfg.sh b/xCAT-test/autotest/testcase/commoncmd/nic_cfg.sh index 8c787455b..363849375 100755 --- a/xCAT-test/autotest/testcase/commoncmd/nic_cfg.sh +++ b/xCAT-test/autotest/testcase/commoncmd/nic_cfg.sh @@ -53,15 +53,21 @@ detect_backend() { fi } -# Resolve a connection's on-disk keyfile path. NM names it "-.nmconnection" -# (not just ".nmconnection") whenever a same-named file already exists, so resolve by -# UUID rather than assuming the plain name. -nm_keyfile() { - local conn=$1 uuid +# Resolve a connection's on-disk file, which NM reports. The keyfile plugin keeps it under +# /etc/NetworkManager/system-connections; the ifcfg-rh plugin, which owns xCAT connections on +# EL8, keeps it under /etc/sysconfig/network-scripts. +# +# Resolve by UUID: NM names a keyfile "-.nmconnection", not ".nmconnection", +# whenever a same-named file already exists. +nm_conn_file() { + local conn=$1 uuid file uuid=$(nmcli -g connection.uuid connection show "$conn" 2>/dev/null) - if [ -n "$uuid" ]; then - grep -l "uuid=$uuid" "$NMDIR"/*.nmconnection 2>/dev/null | head -1 + [ -n "$uuid" ] || return 0 + file=$(nmcli -t -f UUID,FILENAME connection show 2>/dev/null | sed -n "s|^${uuid}:||p" | head -1) + if [ -z "$file" ]; then + file=$(grep -l "uuid=$uuid" "$NMDIR"/*.nmconnection 2>/dev/null | head -1) fi + printf '%s\n' "$file" } # Resolve the NetworkManager connection name bound to a device. @@ -101,14 +107,15 @@ nm_show() { done IFS="$oldifs" mtu=$(nmcli -g 802-3-ethernet.mtu connection show "$conn" 2>/dev/null) - kf=$(nm_keyfile "$conn") + kf=$(nm_conn_file "$conn") + # A keyfile spells it "mtu=", an ifcfg file "MTU=". if { [ -z "$mtu" ] || [ "$mtu" = "auto" ]; } && [ -n "$kf" ] && [ -r "$kf" ]; then - mtu=$(awk -F= '/^[[:space:]]*mtu=/{print $2; exit}' "$kf") + mtu=$(awk -F= '/^[[:space:]]*[Mm][Tt][Uu]=/{print $2; exit}' "$kf") fi [ -n "$mtu" ] && [ "$mtu" != "auto" ] && echo "MTU=$mtu" - # Raw keyfile so anything not normalized above (extra params, slaves, vlan id, ...) + # Raw backend file so anything not normalized above (extra params, slaves, vlan id, ...) # is still visible and greppable by the case's check: lines. - if [ -r "$kf" ]; then echo "# --- $kf ---"; cat "$kf"; fi + if [ -n "$kf" ] && [ -r "$kf" ]; then echo "# --- $kf ---"; cat "$kf"; fi } file_show() { @@ -135,6 +142,11 @@ nm_del() { return 0 } +# Sourced by xCAT-test/bats/nic_cfg.bats: define the functions only. +if [ "${BASH_SOURCE[0]}" != "$0" ]; then + return 0 +fi + case "$1" in show) be=$(detect_backend); dev=$2 diff --git a/xCAT-test/bats/nic_cfg.bats b/xCAT-test/bats/nic_cfg.bats new file mode 100644 index 000000000..8d6b12dfe --- /dev/null +++ b/xCAT-test/bats/nic_cfg.bats @@ -0,0 +1,51 @@ +#!/usr/bin/env bats +# nic_cfg.sh show must print the profile NM reports, whichever store holds it, so a case can +# match a key NM does not model. nmcli is a shell function here. + +load 'helpers/shell_source' + +UUID='c9b049e1-abe6-4df3-9d76-d156d8c764c3' + +setup() +{ + source "$(require_repo_file 'xCAT-test/autotest/testcase/commoncmd/nic_cfg.sh')" +} + +# stub_nmcli -- NM binds xcat-ens4 to ens4 and reports for it. +stub_nmcli() +{ + NM_FILE="$1" + nmcli() + { + case "$*" in + "-t -f NAME,DEVICE connection show --active") printf '%s\n' 'xcat-ens4:ens4' ;; + "-g ipv4.method connection show xcat-ens4") printf '%s\n' manual ;; + "-g ipv4.addresses connection show xcat-ens4") printf '%s\n' '100.0.0.9/16' ;; + "-g 802-3-ethernet.mtu connection show xcat-ens4") printf '%s\n' auto ;; + "-g connection.uuid connection show xcat-ens4") printf '%s\n' "$UUID" ;; + "-t -f UUID,FILENAME connection show") printf '%s:%s\n' "$UUID" "$NM_FILE" ;; + *) return 10 ;; + esac + } +} + +@test "show prints an ifcfg profile that NM reports" { + f="${BATS_TEST_TMPDIR}/ifcfg-xcat-ens4-1" + printf 'TYPE=Ethernet\nCONNECTED_MODE=yes\n' >"$f" + stub_nmcli "$f" + run nm_show ens4 + [ "$status" -eq 0 ] + [[ "$output" == *"# --- $f ---"* ]] + [[ "$output" == *$'\nCONNECTED_MODE=yes'* ]] + [[ "$output" == *$'\nIPADDR=100.0.0.9'* ]] +} + +@test "show prints a keyfile that NM reports" { + f="${BATS_TEST_TMPDIR}/xcat-ens4-${UUID}.nmconnection" + printf '[connection]\nid=xcat-ens4\n[user]\nxcat.CONNECTED_MODE=yes\n' >"$f" + stub_nmcli "$f" + run nm_show ens4 + [ "$status" -eq 0 ] + [[ "$output" == *"# --- $f ---"* ]] + [[ "$output" == *$'\nxcat.CONNECTED_MODE=yes'* ]] +} diff --git a/xCAT-test/bats/xcatlib_nic_extra_param_persist.bats b/xCAT-test/bats/xcatlib_nic_extra_param_persist.bats new file mode 100644 index 000000000..d571a1ebc --- /dev/null +++ b/xCAT-test/bats/xcatlib_nic_extra_param_persist.bats @@ -0,0 +1,145 @@ +#!/usr/bin/env bats +# A nicextraparams key NM does not model survives only if configeth writes it into the file +# NM reports. nmcli is a shell function here, so nothing on the host is read. + +load 'helpers/shell_source' + +UUID='c9b049e1-abe6-4df3-9d76-d156d8c764c3' + +setup() +{ + source "$(require_repo_file 'xCAT/postscripts/xcatlib.sh')" +} + +# stub_nmcli -- NM knows and reports for it. +stub_nmcli() +{ + NM_CONN="$1" + NM_FILE="$2" + nmcli() + { + case "$*" in + "-g connection.uuid connection show $NM_CONN") printf '%s\n' "$UUID" ;; + "-t -f UUID,FILENAME connection show") + printf '%s\n' '0f9a1c55-0000-4000-8000-000000000001:/etc/sysconfig/network-scripts/ifcfg-eth0' + printf '%s:%s\n' "$UUID" "$NM_FILE" + ;; + *) return 10 ;; + esac + } +} + +@test "the connection file comes from NetworkManager, not from the connection name" { + # NM suffixes the file when one of the plain name already exists. + stub_nmcli xcat-ens4 /etc/sysconfig/network-scripts/ifcfg-xcat-ens4-1 + run xcat_nm_conn_file xcat-ens4 + [ "$status" -eq 0 ] + [ "$output" = "/etc/sysconfig/network-scripts/ifcfg-xcat-ens4-1" ] +} + +@test "a connection NetworkManager does not know has no file" { + stub_nmcli xcat-other /etc/sysconfig/network-scripts/ifcfg-xcat-other + run xcat_nm_conn_file xcat-ens4 + [ "$status" -ne 0 ] + [ -z "$output" ] +} + +@test "openEuler uses the keyfile store only when NM reports a keyfile" { + networkmanager_active=1 + stub_nmcli xcat-ens4 /etc/NetworkManager/system-connections/xcat-ens4.nmconnection + xcat_uses_nm_keyfile openeuler22.03 xcat-ens4 + stub_nmcli xcat-ens4 /etc/sysconfig/network-scripts/ifcfg-xcat-ens4 + run xcat_uses_nm_keyfile openeuler22.03 xcat-ens4 + [ "$status" -ne 0 ] +} + +@test "configeth's repair writes the key into an ifcfg profile NM reports" { + f="${BATS_TEST_TMPDIR}/ifcfg-xcat-ens4-1" + printf 'TYPE=Ethernet\nNAME=xcat-ens4\n' >"$f" + stub_nmcli xcat-ens4 "$f" + OSVER=openeuler20.03 networkmanager_active=1 + run xcat_nm_persist_nic_extra_params xcat-ens4 'CONNECTED_MODE=yes MTU=1500' default + [ "$status" -eq 0 ] + grep -qx 'CONNECTED_MODE=yes' "$f" + grep -qx 'MTU=1500' "$f" + refute_grep -q '\[user\]' "$f" + refute_grep -q 'xcat\.' "$f" +} + +@test "configeth's repair writes the key into a keyfile NM reports" { + f="${BATS_TEST_TMPDIR}/xcat-ens4-${UUID}.nmconnection" + printf '[connection]\nid=xcat-ens4\n' >"$f" + stub_nmcli xcat-ens4 "$f" + OSVER=openeuler24.03 networkmanager_active=1 + run xcat_nm_persist_nic_extra_params xcat-ens4 'CONNECTED_MODE=yes' + [ "$status" -eq 0 ] + [ "$(sed -n '/^\[user\]$/,$p' "$f" | grep -cx 'xcat.CONNECTED_MODE=yes')" -eq 1 ] + refute_grep -qx 'CONNECTED_MODE=yes' "$f" +} + +@test "configeth's repair names the connection when NM reports no file" { + stub_nmcli xcat-other /etc/sysconfig/network-scripts/ifcfg-xcat-other + run xcat_nm_persist_nic_extra_params xcat-ens4 'CONNECTED_MODE=yes' + [ "$status" -eq 1 ] + [[ "$output" == *"NetworkManager reports no file for 'xcat-ens4'"* ]] +} + +@test "configeth's repair does nothing when no IP carries extra params" { + nmcli() { echo "nmcli must not run" >&2; return 10; } + run xcat_nm_persist_nic_extra_params xcat-ens4 default '' + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "an ifcfg value is replaced, not duplicated, and a similar key is left alone" { + f="${BATS_TEST_TMPDIR}/ifcfg-xcat-ens4" + printf 'TYPE=Ethernet\nXCONNECTED_MODE=keep\nCONNECTED_MODE=no\nCONNECTED_MODE_X=keep\n' >"$f" + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + [ "$(grep -c '^CONNECTED_MODE=' "$f")" -eq 1 ] + grep -qx 'CONNECTED_MODE=yes' "$f" + grep -qx 'XCONNECTED_MODE=keep' "$f" + grep -qx 'CONNECTED_MODE_X=keep' "$f" +} + +@test "a keyfile value is replaced in [user], not duplicated, and a similar key is left alone" { + f="${BATS_TEST_TMPDIR}/xcat-ens4.nmconnection" + printf '[connection]\nid=xcat-ens4\n\n[user]\nxcat.CONNECTED_MODE=no\nxcat.CONNECTED_MODE_X=keep\n\n[ipv4]\nmethod=manual\n' >"$f" + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + [ "$(grep -c '^\[user\]' "$f")" -eq 1 ] + [ "$(grep -c '^xcat\.CONNECTED_MODE=' "$f")" -eq 1 ] + [ "$(sed -n '/^\[user\]$/,/^\[ipv4\]$/p' "$f" | grep -cx 'xcat.CONNECTED_MODE=yes')" -eq 1 ] + grep -qx 'xcat.CONNECTED_MODE_X=keep' "$f" +} + +@test "a keyfile with a [user] section elsewhere gets the key inside that section" { + f="${BATS_TEST_TMPDIR}/xcat-ens4.nmconnection" + printf '[connection]\nid=xcat-ens4\n[user]\nxcat.other=1\n[ipv4]\nmethod=manual\n' >"$f" + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + [ "$(sed -n '/^\[user\]$/,/^\[ipv4\]$/p' "$f" | grep -cx 'xcat.CONNECTED_MODE=yes')" -eq 1 ] + [ "$(stat -c %a "$f")" = 600 ] +} + +@test "an ifcfg profile without a final newline keeps its last line" { + f="${BATS_TEST_TMPDIR}/ifcfg-xcat-ens4" + printf 'TYPE=Ethernet\nNAME=xcat-ens4' >"$f" + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + grep -qx 'NAME=xcat-ens4' "$f" + grep -qx 'CONNECTED_MODE=yes' "$f" +} + +@test "a keyfile ending in [user] without a final newline keeps the section" { + f="${BATS_TEST_TMPDIR}/xcat-ens4.nmconnection" + printf '[connection]\nid=xcat-ens4\n[user]' >"$f" + xcat_persist_nic_extra_param "$f" CONNECTED_MODE yes + [ "$(grep -cx '\[user\]' "$f")" -eq 1 ] + grep -qx 'xcat.CONNECTED_MODE=yes' "$f" +} + +@test "a file that does not exist is refused" { + run xcat_persist_nic_extra_param "${BATS_TEST_TMPDIR}/absent" CONNECTED_MODE yes + [ "$status" -eq 1 ] + [[ "$output" == *"no profile file '${BATS_TEST_TMPDIR}/absent'"* ]] + [ ! -e "${BATS_TEST_TMPDIR}/absent" ] +} diff --git a/xCAT/postscripts/configeth b/xCAT/postscripts/configeth index 319b7c5bd..c556e64a2 100755 --- a/xCAT/postscripts/configeth +++ b/xCAT/postscripts/configeth @@ -1726,34 +1726,8 @@ if [ $networkmanager_active -eq 1 ] && [ -n "$tmp_con_name" ]; then nmcli con delete $tmp_con_name fi fi -# Persist nicextraparams that NetworkManager has no native setting for (e.g. CONNECTED_MODE) -# into the connection keyfile's [user] section. Done here as the very last step: the -# nmcli con reload/up in the restart phase above re-serializes the keyfile from NM's -# in-memory model and drops anything NM does not model, so an earlier write would be lost. -# On EL10 (keyfile-only) there is no ifcfg file to hold these. We do NOT reload after, so -# the file keeps the section; NM tolerates an unknown [user] section in keyfile mode. -if [ "$str_os_type" = "redhat" ] && [ "$networkmanager_active" = "1" ] && xcat_uses_nm_keyfile "$OSVER" "$con_name"; then - # Resolve the connection's keyfile by UUID -- NM may name it "-.nmconnection" - # (not the plain ".nmconnection") when a same-named file already exists. - ep_con="xcat-${str_nic_name}" - ep_uuid=$(nmcli -g connection.uuid connection show "$ep_con" 2>/dev/null) - ep_kf="" - [ -n "$ep_uuid" ] && ep_kf=$(grep -l "uuid=$ep_uuid" /etc/NetworkManager/system-connections/*.nmconnection 2>/dev/null | head -1) - if [ -n "$ep_kf" ] && [ -f "$ep_kf" ]; then - for ep in "${array_nic_params[@]}"; do - [ -z "$ep" ] && continue - [ "$ep" = "$str_default_token" ] && continue - parse_nic_extra_params "$ep" - j=0 - while [ $j -lt ${#array_extra_param_names[@]} ]; do - nm="${array_extra_param_names[$j]}" - vl="${array_extra_param_values[$j]}" - grep -q '^\[user\]' "$ep_kf" || printf '\n[user]\n' >> "$ep_kf" - grep -q "^xcat\.${nm}=" "$ep_kf" || echo "xcat.${nm}=${vl}" >> "$ep_kf" - j=$((j+1)) - done - done - chmod 600 "$ep_kf" - fi +# The last nmcli con up above dropped the keys NM does not model. Do not reload after this. +if [ "$str_os_type" = "redhat" ] && [ "$networkmanager_active" = "1" ]; then + xcat_nm_persist_nic_extra_params "xcat-${str_nic_name}" "${array_nic_params[@]}" fi exit $error_code diff --git a/xCAT/postscripts/xcatlib.sh b/xCAT/postscripts/xcatlib.sh index a4acb5085..f4a2f2417 100755 --- a/xCAT/postscripts/xcatlib.sh +++ b/xCAT/postscripts/xcatlib.sh @@ -32,16 +32,113 @@ xcat_is_openeuler() grep -Eq '^ID="?openEuler"?$' /etc/os-release 2>/dev/null } +# xcat_nm_conn_file -- the file NetworkManager reports, or non-zero if none. +# NM renames it when a file of the plain name exists, so resolve rather than compose. +xcat_nm_conn_file() +{ + local uuid + local file + uuid=$(nmcli -g connection.uuid connection show "$1" 2>/dev/null) + [ -n "$uuid" ] || return 1 + file=$(nmcli -t -f UUID,FILENAME connection show 2>/dev/null | sed -n "s|^${uuid}:||p" | head -1) + [ -n "$file" ] || return 1 + echo "$file" +} + +# xcat_persist_nic_extra_param -- a keyfile takes the key under [user] +# with an "xcat." prefix, an ifcfg file takes it plain. An existing line of that key is replaced. +xcat_persist_nic_extra_param() +{ + local file="$1" + local name="$2" + local value="$3" + local key="$name" + local section="" + local tmp + if [ -z "$file" ] || [ ! -f "$file" ]; then + echo "xcat_persist_nic_extra_param: no profile file '$file'" >&2 + return 1 + fi + if [ -z "$name" ]; then + echo "xcat_persist_nic_extra_param: no key name for '$file'" >&2 + return 1 + fi + case "$file" in + *.nmconnection) + key="xcat.${name}" + section="user" + ;; + esac + tmp=$(mktemp) || return 1 + # awk ends every line it prints with a newline, so a profile without a final newline + # does not join the new key to its last line. + XCAT_KEY="$key" XCAT_VALUE="$value" XCAT_SECTION="$section" awk ' + BEGIN { + key = ENVIRON["XCAT_KEY"]; line = key "=" ENVIRON["XCAT_VALUE"] + sect = ENVIRON["XCAT_SECTION"]; insect = (sect == ""); seen = 0; done = 0 + } + /^\[.*\]$/ { + if (insect && !done) { print line; done = 1 } + insect = ($0 == "[" sect "]"); if (insect) seen = 1 + print; next + } + insect && index($0, key "=") == 1 { + if (!done) { print line; done = 1 } + next + } + { print } + END { + if (!done) { + if (sect != "" && !seen) print "[" sect "]" + print line + } + } + ' "$file" >"$tmp" && cat "$tmp" >"$file" + local rc=$? + rm -f "$tmp" + [ "$rc" -eq 0 ] || return "$rc" + [ -z "$section" ] || chmod 600 "$file" +} + +# xcat_nm_persist_nic_extra_params ... -- write each nicextraparams +# entry ("KEY=value KEY2=value", or "default") into the file NM reports for . +# nmcli con up drops keys NM does not model, so call this after the last one, with no reload. +xcat_nm_persist_nic_extra_params() +{ + local conn="$1" + local file + local ep + local j + local wanted="" + shift + for ep in "$@"; do + [ -n "$ep" ] && [ "$ep" != "default" ] && wanted=1 + done + [ -n "$wanted" ] || return 0 + if ! file=$(xcat_nm_conn_file "$conn") || [ ! -f "$file" ]; then + echo "xcat_nm_persist_nic_extra_params: NetworkManager reports no file for '$conn'" >&2 + return 1 + fi + for ep in "$@"; do + [ -z "$ep" ] && continue + [ "$ep" = "default" ] && continue + parse_nic_extra_params "$ep" + j=0 + while [ $j -lt ${#array_extra_param_names[@]} ]; do + xcat_persist_nic_extra_param "$file" \ + "${array_extra_param_names[$j]}" "${array_extra_param_values[$j]}" || return 1 + j=$((j+1)) + done + done +} + xcat_uses_nm_keyfile() { xcat_is_el9_or_later "$1" && return 0 [ "$networkmanager_active" = "1" ] && xcat_is_openeuler "$1" || return 1 - local uuid - local filename - uuid=$(nmcli -g connection.uuid connection show "$2" 2>/dev/null) - [ -n "$uuid" ] || return 1 - filename=$(nmcli -t -f UUID,FILENAME connection show 2>/dev/null | sed -n "s/^$uuid://p") - case "$filename" in + local file + file=$(xcat_nm_conn_file "$2") || return 1 + case "$file" in /etc/NetworkManager/system-connections/*|/run/NetworkManager/system-connections/*|/var/run/NetworkManager/system-connections/*) return 0 ;; *) return 1 ;;