diff --git a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm index 9cc0b35c7b..8f0d0ec7e0 100644 --- a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm +++ b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm @@ -189,6 +189,53 @@ sub mark_uftp_receiver { return !$error; } +# The initial ramdisk configures $prinic, and bringing it up again breaks an NFS-based boot. +# EL10 reads no ifcfg files, so its images get keyfiles. +sub write_nic_profiles { + my ($rootimg_dir, $osver, $prinic, $othernics) = @_; + my @othernics = grep { length } split(/,/, $othernics // ''); + my $scripts = "$rootimg_dir/etc/sysconfig/network-scripts"; + my $keyfiles = "$rootimg_dir/etc/NetworkManager/system-connections"; + my $major = el_major_version($osver); + my %profiles; + if (defined($major) && $major >= 10) { + if (-d $keyfiles) { + # The keyfiles of an earlier build can name interfaces that this build no longer requests. + opendir(my $dh, $keyfiles) or die("Error: cannot read $keyfiles: $!\n"); + foreach my $old (grep { /^xcat-genimage-.*\.nmconnection$/ } readdir($dh)) { + unlink("$keyfiles/$old") or die("Error: cannot remove $keyfiles/$old: $!\n"); + } + closedir($dh); + $profiles{"$keyfiles/xcat-genimage-$_.nmconnection"} = + "[connection]\nid=$_\ntype=ethernet\ninterface-name=$_\n\n[ipv4]\nmethod=auto\n" for @othernics; + } + } elsif (-d $scripts) { + $profiles{"$scripts/ifcfg-$prinic"} = "ONBOOT=no\nBOOTPROTO=dhcp\nDEVICE=$prinic\n" if $prinic; + $profiles{"$scripts/ifcfg-$_"} = "ONBOOT=yes\nBOOTPROTO=dhcp\nDEVICE=$_\n" for @othernics; + } + foreach my $file (sort keys %profiles) { + # NetworkManager ignores a keyfile that other users can read. + my $mode = $file =~ /\.nmconnection$/ ? oct('600') : oct('644'); + my $fh; + open($fh, '>', $file) && chmod($mode, $file) && print($fh $profiles{$file}) && close($fh) + or die("Error: cannot write $file: $!\n"); + } + return; +} + +# NetworkManager otherwise makes a DHCP profile for every Ethernet port without one. +sub disable_auto_profiles { + my ($rootimg_dir, $osver) = @_; + my $major = el_major_version($osver); + my $confdir = "$rootimg_dir/etc/NetworkManager/conf.d"; + return unless defined($major) && $major >= 10 && -d $confdir; + my $file = "$confdir/xcat.conf"; + my $fh; + open($fh, '>', $file) && print($fh "[main]\nno-auto-default=*\n") && close($fh) + or die("Error: cannot write $file: $!\n"); + return; +} + sub varsubinline{ my $line=shift; my $refvardict=shift; diff --git a/xCAT-server/share/xcat/netboot/rh/genimage b/xCAT-server/share/xcat/netboot/rh/genimage index 5808728a3a..26826cb2fb 100755 --- a/xCAT-server/share/xcat/netboot/rh/genimage +++ b/xCAT-server/share/xcat/netboot/rh/genimage @@ -2166,19 +2166,8 @@ sub generic_post { #This function is meant to leave the image in a state approxi print $cfgfile "#Dummy resolv.conf to make boot cleaner"; close($cfgfile); - # Create the ifcfg-x file for diskless node. But keep the ONBOOT=no - # to skip the break of nfs-based boot - if ($prinic) { - open($cfgfile, ">", "$rootimg_dir/etc/sysconfig/network-scripts/ifcfg-$prinic"); - print $cfgfile "ONBOOT=no\nBOOTPROTO=dhcp\nDEVICE=$prinic\n"; - close($cfgfile); - } - foreach (split /,/, $othernics) { - if (/^$/) { next; } - open($cfgfile, ">", "$rootimg_dir/etc/sysconfig/network-scripts/ifcfg-$_"); - print $cfgfile "ONBOOT=yes\nBOOTPROTO=dhcp\nDEVICE=$_\n"; - close($cfgfile); - } + imgutils::write_nic_profiles($rootimg_dir, $osver, $prinic, $othernics); + imgutils::disable_auto_profiles($rootimg_dir, $osver); # securetty not needed on s390x if ($arch ne "s390x") { diff --git a/xCAT-test/unit/imgutils_nic_profiles.t b/xCAT-test/unit/imgutils_nic_profiles.t new file mode 100644 index 0000000000..49a271027c --- /dev/null +++ b/xCAT-test/unit/imgutils_nic_profiles.t @@ -0,0 +1,87 @@ +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT", "$FindBin::Bin/../../xCAT-server/lib/perl"; +use Fcntl qw(S_IMODE); +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); +use File::Temp qw(tempdir); +use Test::More; +use XCAT::Test::File qw(repo_path); + +$ENV{XCATROOT} = repo_path('xCAT-server'); +require(repo_path('xCAT-server/share/xcat/netboot/imgutils/imgutils.pm')); + +my @both = ('etc/sysconfig/network-scripts', 'etc/NetworkManager/system-connections'); + +sub image { + my $root = tempdir(CLEANUP => 1); + make_path(map { "$root/$_" } @_); + return $root; +} + +sub mode { + my ($file) = @_; + return -f $file ? sprintf('%04o', S_IMODE((stat $file)[2])) : 'missing'; +} + +sub profiles { + my ($root) = @_; + return [map { substr($_, length($root) + 1) } sort glob("$root/{$both[0],$both[1]}/*")]; +} + +for my $case (['rhels10.1', 'etc/NetworkManager/system-connections'], ['alma10.1', @both]) { + my ($os, @dirs) = @$case; + my $root = image(@dirs); + imgutils::write_nic_profiles($root, $os, 'eth0', 'eth1,,eth2'); + is_deeply(profiles($root), [map { "$both[1]/xcat-genimage-$_.nmconnection" } qw(eth1 eth2)], + "a $os image with " . join(' and ', @dirs) . ' gets keyfiles for the requested interfaces only'); + for my $nic (qw(eth1 eth2)) { + my $file = "$root/$both[1]/xcat-genimage-$nic.nmconnection"; + is(-f $file ? read_text($file) : '', + "[connection]\nid=$nic\ntype=ethernet\ninterface-name=$nic\n\n[ipv4]\nmethod=auto\n", + "the $os keyfile for $nic asks for DHCP"); + is(mode($file), '0600', "only root can read the $os keyfile for $nic"); + } +} + +my $rebuilt = image($both[1]); +write_text("$rebuilt/$both[1]/site.nmconnection", "site profile\n"); +imgutils::write_nic_profiles($rebuilt, 'rhels10.1', 'eth0', 'eth1,eth2'); +imgutils::write_nic_profiles($rebuilt, 'rhels10.1', 'eth0', 'eth2'); +is_deeply(profiles($rebuilt), ["$both[1]/site.nmconnection", "$both[1]/xcat-genimage-eth2.nmconnection"], + 'a rebuilt EL10 image drops the keyfile of an interface that is no longer requested and keeps other profiles'); + +my $el9 = image(@both); +imgutils::write_nic_profiles($el9, 'rhels9.6', 'eth0', 'eth1'); +is_deeply(profiles($el9), ["$both[0]/ifcfg-eth0", "$both[0]/ifcfg-eth1"], 'an EL9 image gets ifcfg files only'); +is(read_text("$el9/$both[0]/ifcfg-eth0"), "ONBOOT=no\nBOOTPROTO=dhcp\nDEVICE=eth0\n", + 'the EL9 interface of the initial ramdisk stays off at boot'); +is(read_text("$el9/$both[0]/ifcfg-eth1"), "ONBOOT=yes\nBOOTPROTO=dhcp\nDEVICE=eth1\n", + 'the EL9 interface eth1 gets DHCP'); +is(mode("$el9/$both[0]/ifcfg-eth1"), '0644', 'the EL9 ifcfg file keeps its permissions'); + +for my $os (qw(rhels10.1 rhels9.6)) { + my $bare = image('etc'); + imgutils::write_nic_profiles($bare, $os, 'eth0', 'eth1'); + ok(!-e "$bare/etc/sysconfig" && !-e "$bare/etc/NetworkManager", "a $os image without either directory gets no profile"); +} + +for my $case (['rhels10.1', 1], ['rocky10.2', 1], ['rhels9.6', 0]) { + my ($os, $disabled) = @$case; + my $root = image('etc/NetworkManager/conf.d'); + imgutils::disable_auto_profiles($root, $os); + my $conf = "$root/etc/NetworkManager/conf.d/xcat.conf"; + if ($disabled) { + is(-f $conf ? read_text($conf) : '', "[main]\nno-auto-default=*\n", "a $os image turns off the automatic DHCP profiles"); + } else { + ok(!-e $conf, "a $os image keeps the NetworkManager defaults"); + } +} +my $without = image('etc'); +imgutils::disable_auto_profiles($without, 'rhels10.1'); +ok(!-e "$without/etc/NetworkManager", 'an image without NetworkManager gets no NetworkManager configuration'); + +done_testing();