diff --git a/xCAT-test/unit/servicenode_builder_repo.t b/xCAT-test/unit/servicenode_builder_repo.t index 154a6abdc..7116eb291 100644 --- a/xCAT-test/unit/servicenode_builder_repo.t +++ b/xCAT-test/unit/servicenode_builder_repo.t @@ -6,46 +6,60 @@ use File::Spec; use FindBin; use Test::More; -# xCAT-server requires perl modules that EL keeps in the distribution builder repository: -# perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). That repository is -# DISABLED on a fresh EL install, so dnf install xCATsn on a service node does not resolve and -# the node ends with no xcatd. The servicenode postscript enabled EPEL and nothing else. +# xCAT-server requires perl modules EL keeps in the distribution builder repository: +# perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). That repository is disabled +# on a fresh EL install, so dnf install xCATsn on a service node does not resolve and the node +# ends with no xcatd. The servicenode postscript enabled EPEL and nothing else. # -# The repository has a different id on every EL vendor and release, so the decision is a sub and -# this test drives it. Enabling the repository is a side effect and stays in the postscript. +# The repository is not called crb on EL8, and each vendor names it differently, so the decision +# is a module and this is its table. Enabling the repository is a side effect and stays in the +# postscript. -my $script = File::Spec->catfile($FindBin::Bin, '..', '..', 'xCAT', 'postscripts', 'servicenode'); -plan skip_all => "servicenode not found" unless -f $script; -# The postscript guards its own body with caller(), so requiring it compiles the subs and runs -# nothing. A postscript that loses that guard executes here, which is the loud failure to have. -eval { require $script; 1 } or plan skip_all => "could not load servicenode: $@"; -can_ok('main', 'builder_repo_ids') or done_testing() && exit; +use lib File::Spec->catdir($FindBin::Bin, '..', '..', 'xCAT', 'postscripts'); +use_ok('ELBuilderRepo') or done_testing() && exit; -sub ids { return [ main::builder_repo_ids(@_) ] } +my @table = ( + # vendor major arch expected ids + [ 'almalinux', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], + [ 'almalinux', 9, 'x86_64', ['crb'] ], + [ 'almalinux', 10, 'x86_64', ['crb'] ], + [ 'rocky', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], + [ 'rocky', 9, 'ppc64le', ['crb'] ], + [ 'rocky', 10, 'x86_64', ['crb'] ], + [ 'centos', 8, 'x86_64', [ 'powertools', 'PowerTools' ] ], + [ 'centos', 9, 'x86_64', ['crb'] ], + [ 'centos', 10, 'x86_64', ['crb'] ], + [ 'rhel', 8, 'ppc64le', ['codeready-builder-for-rhel-8-ppc64le-rpms'] ], + [ 'rhel', 9, 'x86_64', ['codeready-builder-for-rhel-9-x86_64-rpms'] ], + [ 'rhel', 10, 'x86_64', ['codeready-builder-for-rhel-10-x86_64-rpms'] ], + [ 'ol', 8, 'x86_64', ['ol8_codeready_builder'] ], + [ 'ol', 9, 'x86_64', ['ol9_codeready_builder'] ], + [ 'ol', 10, 'x86_64', ['ol10_codeready_builder'] ], +); -is_deeply(ids('almalinux', '9.8', 'x86_64'), ['crb'], - 'AlmaLinux 9 names crb'); -is_deeply(ids('rocky', '10.0', 'x86_64'), ['crb'], - 'Rocky 10 names crb'); -is_deeply(ids('centos', '9', 'ppc64le'), ['crb'], - 'CentOS Stream 9 names crb'); -is_deeply(ids('almalinux', '8.10', 'x86_64'), [ 'powertools', 'PowerTools' ], - 'AlmaLinux 8 names powertools, and the CentOS 8 spelling after it'); -is_deeply(ids('rhel', '9.4', 'x86_64'), ['codeready-builder-for-rhel-9-x86_64-rpms'], - 'RHEL 9 names its arch-qualified codeready-builder repository'); -is_deeply(ids('rhel', '8.9', 'ppc64le'), ['codeready-builder-for-rhel-8-ppc64le-rpms'], - 'RHEL 8 keeps the arch in the repository id'); -is_deeply(ids('ol', '9.3', 'x86_64'), ['ol9_codeready_builder'], - 'Oracle Linux 9 names its own codeready builder repository'); +for my $row (@table) { + my ($vendor, $major, $arch, $want) = @$row; + my @got = ELBuilderRepo::builder_repo_ids($vendor, $major, $arch); + is_deeply(\@got, $want, "$vendor EL$major names @$want"); +} -# A caller with nothing to go on must get nothing to enable, not a guess. -is_deeply(ids(undef, '9', 'x86_64'), [], 'no distribution id yields no repository'); -is_deeply(ids('almalinux', undef, 'x86_64'), [], 'no version yields no repository'); -is_deeply(ids('almalinux', 'rawhide', 'x86_64'), [], 'a version with no major number yields no repository'); +# The EL8 name is the one that is easy to get wrong: the display name reads +# "AlmaLinux 8 - PowerTools" while the id is lowercase, and crb does not exist there at all. +for my $vendor (qw(almalinux rocky centos)) { + my @got = ELBuilderRepo::builder_repo_ids($vendor, 8, 'x86_64'); + ok(!grep({ $_ eq 'crb' } @got), "$vendor EL8 does not name crb"); + is($got[0], 'powertools', "$vendor EL8 tries the lowercase id first"); +} -# The arch only reaches the RHEL id. A missing arch must not build a repository id with a hole -# in it, which dnf would accept as an unknown repository and silently skip. -is_deeply(ids('rhel', '9.4', undef), ['codeready-builder-for-rhel-9-x86_64-rpms'], - 'a missing arch falls back to x86_64 rather than an empty field'); +# Nothing to go on must yield nothing to enable, not a guess. +is_deeply([ ELBuilderRepo::builder_repo_ids(undef, 9, 'x86_64') ], [], 'no vendor yields no repository'); +is_deeply([ ELBuilderRepo::builder_repo_ids('almalinux', undef, 'x86_64') ], [], 'no major version yields no repository'); +is_deeply([ ELBuilderRepo::builder_repo_ids('almalinux', '9.8', 'x86_64') ], [], + 'a version that is not a bare major yields no repository, so the caller must parse it'); + +# The arch reaches the RHEL id alone. A missing arch must not leave a hole in it, which dnf +# accepts as an unknown repository and skips without a word. +is_deeply([ ELBuilderRepo::builder_repo_ids('rhel', 9, undef) ], + ['codeready-builder-for-rhel-9-x86_64-rpms'], 'a missing arch falls back to x86_64'); done_testing(); diff --git a/xCAT/postscripts/ELBuilderRepo.pm b/xCAT/postscripts/ELBuilderRepo.pm new file mode 100644 index 000000000..2346c7613 --- /dev/null +++ b/xCAT/postscripts/ELBuilderRepo.pm @@ -0,0 +1,51 @@ +package ELBuilderRepo; + +# The repository ids that carry the EL builder packages. +# +# xCAT-server requires perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). EL +# keeps them in the distribution builder repository, which is disabled on a fresh install, so +# dnf install xCATsn on a service node does not resolve. Every vendor names that repository +# differently and the name changed at EL9: it is not crb on EL8. +# +# This module ships in /install/postscripts and is copied to /xcatpost with the postscripts, so +# it loads on a node that has no xCAT packages yet. + +use strict; +use warnings; + +#----------------------------------------------------------------------------- + +=head3 builder_repo_ids + + Arguments: + $vendor the ID field of /etc/os-release, for example almalinux, rocky, centos, rhel, ol + $major the EL major version, 8, 9 or 10 + $arch the machine architecture. Only the RHEL id carries one + + Returns: + the repository ids to try, most likely first, or an empty list when the arguments name + no EL release. An empty list is the answer for "do not guess". + + Covered: AlmaLinux, Rocky and CentOS Stream (crb on 9 and later, powertools on 8, with the + capitalised PowerTools that Rocky 8.4 and earlier shipped after it), RHEL + (codeready-builder-for-rhel---rpms) and Oracle Linux + (ol_codeready_builder). Another vendor takes the community spellings, which is a + guess. + +=cut + +#----------------------------------------------------------------------------- +sub builder_repo_ids { + my ($vendor, $major, $arch) = @_; + + return () unless defined $vendor && length $vendor; + return () unless defined $major && $major =~ /^\d+$/; + $arch = 'x86_64' unless defined $arch && length $arch; + + return ("codeready-builder-for-rhel-$major-$arch-rpms") if $vendor eq 'rhel'; + return ("ol${major}_codeready_builder") if $vendor eq 'ol'; + return ('powertools', 'PowerTools') if $major == 8; + return ('crb'); +} + +1; diff --git a/xCAT/postscripts/servicenode b/xCAT/postscripts/servicenode index 8ffcf2db5..276b2b6e9 100755 --- a/xCAT/postscripts/servicenode +++ b/xCAT/postscripts/servicenode @@ -44,6 +44,10 @@ if ($^O =~ /^aix/i) { } use lib "$::XCATROOT/lib/perl"; +# ELBuilderRepo ships beside this script and reaches the node in /xcatpost with it. +use File::Basename qw(dirname); +use Cwd qw(abs_path); +use lib dirname(abs_path($0)); use strict; # MAIN @@ -177,52 +181,14 @@ exit $rc; #----------------------------------------------------------------------------- -=head3 builder_repo_ids - - The repository ids that carry the EL builder packages, most likely first. - - xCAT-server requires perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and perl(Expect). - EL keeps them in the builder repository, which every vendor names differently and which is - disabled on a fresh install. The caller enables the first id dnf knows. - - Covered: AlmaLinux, Rocky and CentOS Stream (crb on 9 and 10, powertools on 8), RHEL - (codeready-builder-for-rhel---rpms) and Oracle Linux - (ol_codeready_builder). Not covered: any other vendor, which gets crb on 9 and later - and powertools on 8 because those are the community spellings. - - Arguments: - $id the ID field of /etc/os-release - $version the VERSION_ID field of /etc/os-release - $arch the machine architecture, for the RHEL id only - Returns: - the repository ids to try, in order, or an empty list when the arguments name no EL - release. An empty list is the answer for "do not guess". - -=cut - -#----------------------------------------------------------------------------- -sub builder_repo_ids { - my ($id, $version, $arch) = @_; - - return () unless defined $id && length $id; - return () unless defined $version; - my ($major) = $version =~ /^(\d+)/; - return () unless defined $major; - $arch = 'x86_64' unless defined $arch && length $arch; - - return ("codeready-builder-for-rhel-$major-$arch-rpms") if $id eq 'rhel'; - return ("ol${major}_codeready_builder") if $id eq 'ol'; - return ('powertools', 'PowerTools') if $major == 8; - return ('crb'); -} - -#----------------------------------------------------------------------------- - =head3 enable_el_dependency_repos - Enable EPEL and the builder repository on an EL service node, and say so when either is - still absent. Neither is fatal: a site can run without EPEL, and xcat-dep carries the same - perl closure. What must not happen is silence. + Enable EPEL and the distribution builder repository on an EL service node, and say so when + either is still absent. Neither is fatal: a site can run without EPEL, and xcat-dep carries + the same perl closure. What must not happen is silence. + + ELBuilderRepo names the builder repository. It ships beside this script and reaches the node + in /xcatpost with it, so it loads where no xCAT package is installed yet. =cut @@ -231,41 +197,56 @@ sub enable_el_dependency_repos { my ($log_label) = @_; my $osrel = `cat /etc/os-release 2>/dev/null` || ''; - my ($id) = $osrel =~ /^ID="?([^"\n]+)"?/m; + my ($vendor) = $osrel =~ /^ID="?([^"\n]+)"?/m; my ($version) = $osrel =~ /^VERSION_ID="?([^"\n]+)"?/m; + my ($major) = defined $version ? $version =~ /^(\d+)/ : (); my $arch = `uname -m 2>/dev/null`; chomp $arch if defined $arch; `logger -t $log_label -p local4.info servicenode: enabling EPEL and the builder repository for xCAT-server dependencies`; &runcmd("dnf -y install epel-release"); unless (`dnf repolist --enabled 2>/dev/null` =~ /^epel\b/m) { - my $m = "servicenode: EPEL is not enabled on $::hname. xCAT-server perl dependencies must" - . " then come from xcat-dep, or xCATsn will not install."; - print "$m\n"; - `logger -t $log_label -p local4.warning "$m"`; + &report_missing_repo($log_label, "EPEL", ""); } - my @ids = &builder_repo_ids($id, $version, $arch); - my $known = `dnf repolist --all 2>/dev/null` || ''; - my $enabled = ''; - foreach my $repo (@ids) { - next unless $known =~ /^\Q$repo\E\s/m; - &runcmd("dnf -y install dnf-plugins-core") unless -x "/usr/bin/dnf-3"; - if (&runcmd("dnf config-manager --set-enabled $repo") == 0) { - $enabled = $repo; - last; - } - } - if ($enabled) { - `logger -t $log_label -p local4.info "servicenode: enabled the builder repository $enabled"`; + my @ids; + if (eval { require ELBuilderRepo; 1 }) { + @ids = ELBuilderRepo::builder_repo_ids($vendor, $major, $arch); } else { - my $m = "servicenode: no builder repository could be enabled on $::hname (tried: " - . join(', ', @ids) . "). perl-IO-Tty, perl-Crypt-CBC, perl-Crypt-Rijndael and" - . " perl(Expect) must then come from xcat-dep, or xCATsn will not install."; - print "$m\n"; - `logger -t $log_label -p local4.warning "$m"`; + `logger -t $log_label -p local4.warning "servicenode: ELBuilderRepo did not load: $@"`; } + + my $known = `dnf repolist --all 2>/dev/null` || ''; + foreach my $repo (@ids) { + next unless $known =~ /^\Q$repo\E\s/m; + &runcmd("dnf -y install dnf-plugins-core"); + next unless &runcmd("dnf config-manager --set-enabled $repo") == 0; + `logger -t $log_label -p local4.info "servicenode: enabled the builder repository $repo"`; + return 0; + } + &report_missing_repo($log_label, "no builder repository", " (tried: " . join(', ', @ids) . ")"); + return 0; +} + +#----------------------------------------------------------------------------- + +=head3 report_missing_repo + + Print and log that a repository could not be enabled. Printing is what makes it visible: the + node copies this script's output into /var/log/xcat/xcat.log, while the EPEL step reported + only to syslog, where nothing read it. + +=cut + +#----------------------------------------------------------------------------- +sub report_missing_repo { + my ($log_label, $what, $detail) = @_; + my $m = "servicenode: $what could be enabled on $::hname$detail. perl-IO-Tty, perl-Crypt-CBC," + . " perl-Crypt-Rijndael and perl(Expect) must then come from xcat-dep, or xCATsn will" + . " not install."; + print "$m\n"; + `logger -t $log_label -p local4.warning "$m"`; return 0; }