From 76a44422d5e91aaa5f934e6b90b257931165ca81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:30:31 -0300 Subject: [PATCH 01/54] feat(perl-xCAT): recognize riscv64 as a node and osimage architecture riscv64 nodes boot through UEFI and grub2 only, like ppc64le PowerKVM guests and aarch64. Teach the three places that declare which noderes.netboot methods an architecture accepts about it: - xCAT::Utils::lookupNetboot returns grub2,grub2-tftp,grub2-http for riscv64 so nodeset and rinstall accept those methods. - the profiled-node rule table in xCAT::ProfiledNodeUtils defaults riscv64 nodes to grub2. - the nodetype.arch, osimage.osarch and noderes.netboot schema descriptions document riscv64. The arch token is riscv64 everywhere (rpm, dpkg and uname -m agree), so no alias handling is added. --- perl-xCAT/xCAT/ProfiledNodeUtils.pm | 42 ++++++++++++++++------------- perl-xCAT/xCAT/Schema.pm | 5 ++-- perl-xCAT/xCAT/Utils.pm | 2 ++ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/perl-xCAT/xCAT/ProfiledNodeUtils.pm b/perl-xCAT/xCAT/ProfiledNodeUtils.pm index b901d2413..4c634072d 100644 --- a/perl-xCAT/xCAT/ProfiledNodeUtils.pm +++ b/perl-xCAT/xCAT/ProfiledNodeUtils.pm @@ -1429,6 +1429,27 @@ sub get_all_vmhosts =cut #------------------------------------------------------------------------------- +our %NETBOOT_RULES = ( + 'x86_64' => 'xnba', + 'riscv64' => 'grub2', + 'ppc64' => { + 'rhels' => { + '7' => 'grub2', + '*' => 'yaboot', + }, + 'pkvm' => 'petitboot', + '*' => 'yaboot', + }, + 'ppc64le' => { + '*' => { + '*' => { + '*' => 'grub2', + 'ipmi' => 'petitboot', + }, + }, + }, +); + sub get_netboot_attr { my $class = shift; my $imageprofile = shift; @@ -1502,27 +1523,10 @@ sub get_netboot_attr { # 3 | | * | * | * | yaboot | # 4 | ppc64le/el | * | * | * | grub2 # 4 | ppc64le/el | * | * | ipmi | petitboot + # 5 | riscv64 | * | * | * | grub2 | # arch osname version hardware netboot - my %netboot_dict = ('x86_64' => 'xnba', - 'ppc64' => { - 'rhels' => { - '7' => 'grub2', - '*' => 'yaboot', - }, - 'pkvm' => 'petitboot', - '*' => 'yaboot', - }, - 'ppc64le' => { - '*' => { - '*' => { - '*' => 'grub2', - 'ipmi' => 'petitboot', - }, - }, - }, - ); my $condition_array_ref = [ $os_arch, $os_name, $os_major_version, $mgt ]; - $netboot = cal_netboot(\%netboot_dict, $condition_array_ref); + $netboot = cal_netboot(\%NETBOOT_RULES, $condition_array_ref); if ($netboot eq '0') { return 0, "Can not get the netboot attribute"; diff --git a/perl-xCAT/xCAT/Schema.pm b/perl-xCAT/xCAT/Schema.pm index 796201ecd..4fcc49c6d 100644 --- a/perl-xCAT/xCAT/Schema.pm +++ b/perl-xCAT/xCAT/Schema.pm @@ -676,6 +676,7 @@ passed as argument rather than by table value', ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 + riscv64 >=el10 grub2,grub2-http,grub2-tftp ', tftpserver => 'The TFTP server for this node (as known by this node). If not set, it defaults to networks.tftpserver.', @@ -750,7 +751,7 @@ passed as argument rather than by table value', descriptions => { node => 'The node name or group name.', os => 'The operating system deployed on this node. Valid values: AIX, rhels*,rhelc*, rhas*,centos*, alma*, rocky*,SL*, fedora*, sles* (where * is the version #). As a special case, if this is set to "boottarget", then it will use the initrd/kernel/parameters specified in the row in the boottarget table in which boottarget.bprofile equals nodetype.profile.', - arch => 'The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64.', + arch => 'The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64.', profile => 'The string to use to locate a kickstart or autoyast template to use for OS deployment of this node. If the provmethod attribute is set to an osimage name, that takes precedence, and profile need not be defined. Otherwise, the os, profile, and arch are used to search for the files in /install/custom first, and then in /opt/xcat/share/xcat.', provmethod => 'The provisioning method for node deployment. The valid values are install, netboot, statelite or an os image name from the osimage table. If an image name is specified, the osimage definition stored in the osimage table and the linuximage table (for Linux) or nimimage table (for AIX) are used to locate the files for templates, pkglists, syncfiles, etc. On Linux, if install, netboot or statelite is specified, the os, profile, and arch are used to search for the files in /install/custom first, and then in /opt/xcat/share/xcat.', supportedarchs => 'Comma delimited list of architectures this node can execute.', @@ -794,7 +795,7 @@ passed as argument rather than by table value', profile => 'The node usage category. For example compute, service.', osname => 'Operating system name- AIX or Linux.', osvers => 'The Linux operating system deployed on this node. Valid values: rhels*,rhelc*, rhas*,centos*,alma*, rocky*,SL*, fedora*, sles* (where * is the version #).', - osarch => 'The hardware architecture of this node. For netboot/statelite images, QEMU emulation for non-native architectures is used if qemu-user-static is installed and configured via systemd-binfmt. Valid values: x86_64, ppc64, x86, ia64, aarch64.', + osarch => 'The hardware architecture of this node. For netboot/statelite images, QEMU emulation for non-native architectures is used if qemu-user-static is installed and configured via systemd-binfmt. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64.', synclists => 'The fully qualified name of a file containing a list of files to synchronize on the nodes. Can be a comma separated list of multiple synclist files. The synclist generated by PCM named /install/osimages//synclist.cfm is reserved for use only by PCM and should not be edited by the admin.', postscripts => 'Comma separated list of scripts that should be run on this image after diskful installation or diskless boot. For installation of RedHat, CentOS, Fedora, the scripts will be run before the reboot. For installation of SLES, the scripts will be run after the reboot but before the init.d process. For diskless deployment, the scripts will be run at the init.d time, and xCAT will automatically add the list of scripts from the postbootscripts attribute to run after postscripts list. For installation of AIX, the scripts will run after the reboot and acts the same as the postbootscripts attribute. For AIX, use the postbootscripts attribute. See the site table runbootscripts attribute.', postbootscripts => 'Comma separated list of scripts that should be run on this after diskful installation or diskless boot. On AIX these scripts are run during the processing of /etc/inittab. On Linux they are run at the init.d time. xCAT automatically adds the scripts in the xcatdefaults.postbootscripts attribute to run first in the list. See the site table runbootscripts attribute.', diff --git a/perl-xCAT/xCAT/Utils.pm b/perl-xCAT/xCAT/Utils.pm index 65c253a7d..81256ab93 100644 --- a/perl-xCAT/xCAT/Utils.pm +++ b/perl-xCAT/xCAT/Utils.pm @@ -4916,6 +4916,8 @@ sub lookupNetboot { $ret = "petitboot,grub2,grub2-tftp,grub2-http"; } elsif ($osarch =~ /^aarch64$/i) { $ret = "grub2"; + } elsif ($osarch =~ /^riscv64$/i) { + $ret = "grub2,grub2-tftp,grub2-http"; } } return $ret; From be44015c3fb3964c7f9494e2cba0e9a2d9025f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:30:31 -0300 Subject: [PATCH 02/54] test(xCAT-test): cover riscv64 netboot method lookup Pin lookupNetboot, the profiled-node netboot rule table and the schema descriptions for riscv64 without loading the database-backed modules: the shipped subroutines are extracted from the source and evaluated directly, so the test runs without DBI. --- xCAT-test/unit/lookup_netboot_arch.t | 115 +++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 xCAT-test/unit/lookup_netboot_arch.t diff --git a/xCAT-test/unit/lookup_netboot_arch.t b/xCAT-test/unit/lookup_netboot_arch.t new file mode 100644 index 000000000..15676ce8a --- /dev/null +++ b/xCAT-test/unit/lookup_netboot_arch.t @@ -0,0 +1,115 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Spec; +use Test::More; + +# riscv64 nodes boot through UEFI and grub2 only. This test pins the three +# places that declare which noderes.netboot values an architecture accepts: +# xCAT::Utils::lookupNetboot (used by nodeset/rinstall validation), the +# profiled-node netboot rule table in xCAT::ProfiledNodeUtils, and the +# schema descriptions that document the valid values. +# +# xCAT::Utils and xCAT::ProfiledNodeUtils pull in the database layer at load +# time, so the shipped subroutines are extracted from the source and evaluated +# directly instead of loading the whole modules. + +my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); +my $utils_pm = File::Spec->catfile( $repo_root, 'perl-xCAT', 'xCAT', 'Utils.pm' ); +my $pnu_pm = File::Spec->catfile( $repo_root, 'perl-xCAT', 'xCAT', 'ProfiledNodeUtils.pm' ); + +plan skip_all => "$utils_pm not found" unless -r $utils_pm; +plan skip_all => "$pnu_pm not found" unless -r $pnu_pm; + +sub slurp { + my ($path) = @_; + open( my $fh, '<', $path ) or die "Unable to read $path: $!"; + my $source = do { local $/; <$fh> }; + close($fh); + return $source; +} + +# --------------------------------------------------------------------------- +# xCAT::Utils::lookupNetboot +# --------------------------------------------------------------------------- +my $utils_source = slurp($utils_pm); +my ($lookup_sub) = $utils_source =~ m{^(sub lookupNetboot \{.*?^\})}ms; +ok( $lookup_sub, 'lookupNetboot was located in xCAT::Utils' ) + or BAIL_OUT('Utils.pm no longer matches the expected lookupNetboot shape'); + +{ + package Test::LookupNetboot; + my $code = $lookup_sub; + eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval) + die "Unable to evaluate lookupNetboot: $@" if $@; +} + +my @lookup_cases = ( + [ 'rocky10.2', 'riscv64', 'Linux', 'grub2,grub2-tftp,grub2-http', 'riscv64 EL10 boots with grub2 over tftp or http' ], + [ 'rhels10.2', 'riscv64', 'Linux', 'grub2,grub2-tftp,grub2-http', 'riscv64 RHEL 10 uses the same grub2 methods' ], + [ 'ubuntu24.04.4', 'riscv64', 'Linux', 'grub2,grub2-tftp,grub2-http', 'riscv64 is arch-driven, not distro-driven' ], + [ 'rhels10.2', 'RISCV64', 'Linux', 'grub2,grub2-tftp,grub2-http', 'the arch match is case-insensitive like the other arches' ], + [ 'rhels9.4', 'aarch64', 'Linux', 'grub2', 'aarch64 keeps its single grub2 method' ], + [ 'rhels9.4', 'x86_64', 'Linux', 'xnba,pxe,grub2', 'x86_64 methods are unchanged' ], + [ 'rhels9.4', 'ppc64le', 'Linux', 'petitboot,grub2,grub2-tftp,grub2-http', 'ppc64le methods are unchanged' ], + [ 'rhels10.2', 'riscv64', 'NIM', 'nimol', 'NIM images are not affected by the arch' ], + [ 'rhels10.2', 'riscv32', 'Linux', '', 'unknown architectures still resolve to no netboot method' ], +); + +for my $case (@lookup_cases) { + my ( $osvers, $osarch, $imgtype, $expected, $label ) = @$case; + is( Test::LookupNetboot::lookupNetboot( $osvers, $osarch, $imgtype ), $expected, $label ); +} + +is( Test::LookupNetboot::lookupNetboot( 'xCAT::Utils', 'rocky10.2', 'riscv64', 'Linux' ), + 'grub2,grub2-tftp,grub2-http', 'the class-method calling convention works for riscv64' ); + +# --------------------------------------------------------------------------- +# xCAT::ProfiledNodeUtils netboot rule table + cal_netboot +# --------------------------------------------------------------------------- +my $pnu_source = slurp($pnu_pm); +my ($netboot_dict) = $pnu_source =~ m{^(\s*my %netboot_dict = \(.*?^\s*\);)}ms; +ok( $netboot_dict, 'the profiled-node netboot rule table was located' ) + or BAIL_OUT('ProfiledNodeUtils.pm no longer matches the expected %netboot_dict shape'); +my ($cal_netboot) = $pnu_source =~ m{^(sub cal_netboot \{.*?^\})}ms; +ok( $cal_netboot, 'cal_netboot was located in xCAT::ProfiledNodeUtils' ) + or BAIL_OUT('ProfiledNodeUtils.pm no longer matches the expected cal_netboot shape'); + +my $rule_table; +{ + package Test::ProfiledNetboot; + my $code = "$cal_netboot\n sub rule_table { $netboot_dict return \\%netboot_dict; }"; + eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval) + die "Unable to evaluate the profiled-node netboot rules: $@" if $@; + $rule_table = rule_table(); +} + +is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'riscv64', 'rhels', '10', '*' ] ), + 'grub2', 'profiled riscv64 nodes default to grub2' ); +is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'riscv64', 'rhels', '10', 'ipmi' ] ), + 'grub2', 'riscv64 grub2 does not depend on the management method' ); +is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'x86_64', 'rhels', '10', '*' ] ), + 'xnba', 'x86_64 profiled nodes still default to xnba' ); +is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'ppc64le', 'rhels', '9', 'ipmi' ] ), + 'petitboot', 'ppc64le ipmi profiled nodes still default to petitboot' ); +is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'aarch64', 'rhels', '9', '*' ] ), + '0', 'aarch64 profiled nodes are still undefined in the rule table' ); + +# --------------------------------------------------------------------------- +# Schema descriptions +# --------------------------------------------------------------------------- +SKIP: { + skip 'xCAT::Schema is not loadable here', 3 + unless eval { require lib; lib->import( File::Spec->catdir( $repo_root, 'perl-xCAT' ) ); require xCAT::Schema; 1 }; + + like( $xCAT::Schema::tabspec{nodetype}{descriptions}{arch}, qr/\briscv64\b/, + 'nodetype.arch documents riscv64 as a valid value' ); + like( $xCAT::Schema::tabspec{osimage}{descriptions}{osarch}, qr/\briscv64\b/, + 'osimage.osarch documents riscv64 as a valid value' ); + like( $xCAT::Schema::tabspec{noderes}{descriptions}{netboot}, qr/riscv64\s+>=el10\s+grub2,grub2-http,grub2-tftp/, + 'noderes.netboot documents the riscv64 grub2 methods' ); +} + +done_testing(); From 9549e660f95c20707a202e11992865f07c7cc3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:32:27 -0300 Subject: [PATCH 03/54] feat(dhcp): boot RISC-V 64-bit UEFI clients with grub2 RISC-V 64-bit UEFI firmware identifies itself with DHCP option 93 client-system-architecture 27 (0x001b, IANA processor architecture types). Neither DHCP backend knew the value: Kea handed such clients no boot file and ISC dhcpd fell through to the /yaboot catch-all. Add an xcat-riscv64 Kea client class and an ISC subnet branch that send them boot/grub2/grub2.riscv64, the same shape as the aarch64 entries. The UEFI HTTP boot id (0x001c) is left alone: it needs a URL boot file and the HTTPClient vendor class, which is a separate change. --- perl-xCAT/xCAT/DHCP/BootPolicy.pm | 5 +++++ xCAT-server/lib/xcat/plugins/dhcp.pm | 3 +++ 2 files changed, 8 insertions(+) diff --git a/perl-xCAT/xCAT/DHCP/BootPolicy.pm b/perl-xCAT/xCAT/DHCP/BootPolicy.pm index c24992d8c..98f1f0b62 100644 --- a/perl-xCAT/xCAT/DHCP/BootPolicy.pm +++ b/perl-xCAT/xCAT/DHCP/BootPolicy.pm @@ -36,6 +36,11 @@ sub kea_client_classes { test => 'option[93].hex == 0x000b', 'boot-file-name' => 'boot/grub2/grub2.aarch64', }, + { + name => 'xcat-riscv64', + test => 'option[93].hex == 0x001b', + 'boot-file-name' => 'boot/grub2/grub2.riscv64', + }, { name => 'xcat-ppc64', test => 'option[93].hex == 0x000c', diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index c6afb2adb..1505f4610 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -4467,6 +4467,9 @@ sub addnet push @netent, " } else if option client-architecture = 00:0b { #aaarch64\n "; push @netent, " filename \"boot/grub2/grub2.aarch64\";\n"; + push @netent, + " } else if option client-architecture = 00:1b { #riscv64 uefi\n "; + push @netent, " filename \"boot/grub2/grub2.riscv64\";\n"; push @netent, " } else if option client-architecture = 00:0e { #OPAL-v3\n "; push @netent, " option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/" . $net . "_" . $maskbits . "\";\n"; From d64366a30a2d301a592745106a8dab5ab991aa9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:32:27 -0300 Subject: [PATCH 04/54] test(xCAT-test): cover the riscv64 DHCP boot policy Assert the Kea xcat-riscv64 class (architecture 0x001b only, grub2 boot file, present with and without xNBA loaders) and pin the ISC subnet block so the riscv64 branch stays ahead of the /yaboot fallback that would otherwise shadow it. --- xCAT-test/unit/dhcp_boot_policy.t | 8 +++-- xCAT-test/unit/dhcp_isc_client_arch.t | 48 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 xCAT-test/unit/dhcp_isc_client_arch.t diff --git a/xCAT-test/unit/dhcp_boot_policy.t b/xCAT-test/unit/dhcp_boot_policy.t index b5369b066..d7021bc21 100644 --- a/xCAT-test/unit/dhcp_boot_policy.t +++ b/xCAT-test/unit/dhcp_boot_policy.t @@ -9,13 +9,13 @@ use Test::More; use xCAT::DHCP::BootPolicy; my $fallback_classes = xCAT::DHCP::BootPolicy->kea_client_classes(); -is( scalar @$fallback_classes, 4, 'Kea boot policy omits xNBA classes when xNBA loaders are unavailable' ); +is( scalar @$fallback_classes, 5, 'Kea boot policy omits xNBA classes when xNBA loaders are unavailable' ); my %fallback_by_name = map { $_->{name} => $_ } @$fallback_classes; is( $fallback_by_name{'xcat-bios'}{'boot-file-name'}, 'pxelinux.0', 'BIOS clients fall back to pxelinux.0 without xNBA loaders' ); ok( !exists $fallback_by_name{'xcat-xnba-bios'}, 'xNBA user-class is not advertised without xNBA kpxe' ); my $classes = xCAT::DHCP::BootPolicy->kea_client_classes(xnba_kpxe => 1, xnba_efi => 1); -is( scalar @$classes, 5, 'Kea boot policy renders expected xNBA client classes' ); +is( scalar @$classes, 6, 'Kea boot policy renders expected xNBA client classes' ); my %by_name = map { $_->{name} => $_ } @$classes; is( $by_name{'xcat-bios'}{'boot-file-name'}, 'xcat/xnba.kpxe', 'BIOS clients receive xNBA kpxe' ); @@ -27,6 +27,10 @@ like( $by_name{'xcat-uefi-x64'}{test}, qr/not \(\(option\[77\]\.exists/, 'generi is( $by_name{'xcat-aarch64'}{'boot-file-name'}, 'boot/grub2/grub2.aarch64', 'AArch64 clients receive grub2 boot file' ); is( $by_name{'xcat-ppc64'}{'boot-file-name'}, '/boot/grub2/grub2.ppc', 'POWER clients receive grub2 Open Firmware boot file' ); is( $by_name{'xcat-ppc64'}{test}, 'option[93].hex == 0x000c', 'POWER class keeps existing POWER architecture id' ); +is( $by_name{'xcat-riscv64'}{'boot-file-name'}, 'boot/grub2/grub2.riscv64', 'RISC-V 64-bit UEFI clients receive the riscv64 grub2 boot file' ); +is( $by_name{'xcat-riscv64'}{test}, 'option[93].hex == 0x001b', 'RISC-V 64-bit UEFI class matches IANA client architecture 27 only' ); +is( $fallback_by_name{'xcat-riscv64'}{'boot-file-name'}, 'boot/grub2/grub2.riscv64', 'riscv64 clients get grub2 even without xNBA loaders' ); +unlike( join( ' ', map { $_->{test} } @$classes ), qr/0x001[9ade]/, 'no class claims the RISC-V 32-bit or 128-bit architecture ids' ); my $xnba_classes = xCAT::DHCP::BootPolicy->kea_xnba_node_classes( xnba_efi => 1, diff --git a/xCAT-test/unit/dhcp_isc_client_arch.t b/xCAT-test/unit/dhcp_isc_client_arch.t new file mode 100644 index 000000000..d65e33a94 --- /dev/null +++ b/xCAT-test/unit/dhcp_isc_client_arch.t @@ -0,0 +1,48 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Spec; +use Test::More; + +# The ISC dhcpd subnet block in dhcp.pm maps DHCP option 93 (client system +# architecture) to a boot file. The block is rendered inside a large +# database-backed subroutine, so this pins the shipped source text: every +# architecture branch must appear before the catch-all that hands unknown +# clients /yaboot, otherwise the branch is unreachable. + +my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); +my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'dhcp.pm' ); + +plan skip_all => "$plugin not found" unless -r $plugin; + +open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; +my $source = do { local $/; <$fh> }; +close($fh); + +my %branch = ( + aarch64 => qr/client-architecture = 00:0b \{[^\n]*\n\s*push \@netent, "\s*filename \\"boot\/grub2\/grub2\.aarch64\\";/, + riscv64 => qr/client-architecture = 00:1b \{[^\n]*\n\s*push \@netent, "\s*filename \\"boot\/grub2\/grub2\.riscv64\\";/, + opal => qr/client-architecture = 00:0e \{/, + yaboot => qr/substring\(filename,0,1\) = null \{[^\n]*\n\s*push \@netent, "\s*filename \\"\/yaboot\\";/, +); + +my %pos; +for my $name ( sort keys %branch ) { + ok( $source =~ $branch{$name}, "the ISC subnet block renders the $name branch" ) + or next; + $pos{$name} = $-[0]; +} + +SKIP: { + skip 'not every branch was found', 3 unless 4 == scalar keys %pos; + cmp_ok( $pos{aarch64}, '<', $pos{riscv64}, 'riscv64 follows the aarch64 branch' ); + cmp_ok( $pos{riscv64}, '<', $pos{opal}, 'riscv64 is rendered before the POWER OPAL branch' ); + cmp_ok( $pos{riscv64}, '<', $pos{yaboot}, 'riscv64 is rendered before the /yaboot fallback, so it is reachable' ); +} + +my @riscv_ids = $source =~ /client-architecture = (00:1[9a-e])/g; +is_deeply( \@riscv_ids, ['00:1b'], 'only the RISC-V 64-bit UEFI architecture id (27) is mapped' ); + +done_testing(); From bd87aa9ed9884ae9d69054769d7414377ae02df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:34:16 -0300 Subject: [PATCH 05/54] feat(nodediscover): default discovered riscv64 nodes to grub2 Discovery left noderes.netboot untouched for any architecture outside x86, ppc and armv7l, so a discovered riscv64 node had no boot method and nodeset failed to find a plugin for it. Move the default-netboot ladder into _default_netboot(), which returns the method to set or undef, and teach it that riscv64 nodes boot through UEFI and grub2. The existing x86, PowerNV, ppc and onie rules are unchanged; aarch64 is deliberately left as it was. The platform of the discovery request is only read when the request carries it, so a node that reports none does not gain the key, which would end up stored as discovery data. --- xCAT-server/lib/xcat/plugins/nodediscover.pm | 40 +++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/nodediscover.pm b/xCAT-server/lib/xcat/plugins/nodediscover.pm index d7f4fef1e..8e9b77fb0 100644 --- a/xCAT-server/lib/xcat/plugins/nodediscover.pm +++ b/xCAT-server/lib/xcat/plugins/nodediscover.pm @@ -137,6 +137,31 @@ sub handled_commands { }; } +# Pick the noderes.netboot method a freshly discovered node should use when +# the admin has not already chosen one that fits its architecture. Returns the +# method to set, or undef to leave noderes.netboot alone. +sub _default_netboot { + my ($arch, $platform, $currboot) = @_; + $arch = '' unless defined $arch; + $platform = '' unless defined $platform; + $currboot = '' unless defined $currboot; + + if ($arch =~ /x86/ and $currboot !~ /pxe/ and $currboot !~ /xnba/) { + return 'xnba'; + } elsif ($arch =~ /ppc/ and $platform =~ /PowerNV/) { + return 'petitboot'; + } elsif ($arch =~ /ppc/ and $currboot !~ /yaboot/) { + return 'yaboot'; + } elsif ($arch =~ /armv7l/ and $currboot !~ /onie/) { + #for onie switch, the netboot should be "onie" + return 'onie'; + } elsif ($arch =~ /^riscv64$/ and $currboot !~ /grub2/) { + #riscv64 nodes boot through UEFI and grub2 only + return 'grub2'; + } + return; +} + sub process_request { my $request = shift; my $callback = shift; @@ -230,15 +255,12 @@ sub process_request { $currboot = $rent->{'netboot'}; } - if ($request->{arch}->[0] =~ /x86/ and $currboot !~ /pxe/ and $currboot !~ /xnba/) { - $nrtab->setNodeAttribs($node, { netboot => 'xnba' }); - } elsif ($request->{arch}->[0] =~ /ppc/ and $request->{platform}->[0] =~ /PowerNV/) { - $nrtab->setNodeAttribs($node, { netboot => 'petitboot' }); - } elsif ($request->{arch}->[0] =~ /ppc/ and $currboot !~ /yaboot/) { - $nrtab->setNodeAttribs($node, { netboot => 'yaboot' }); - } elsif($request->{arch}->[0] =~ /armv7l/ and $currboot !~ /onie/) { - #for onie switch, the netboot should be "onie" - $nrtab->setNodeAttribs($node, { netboot => 'onie' }); + # do not dereference platform unless the request carries it: the key would be + # autovivified into the request hash and later stored as discovery data + my $platform = exists $request->{platform} ? $request->{platform}->[0] : undef; + my $netboot = _default_netboot($request->{arch}->[0], $platform, $currboot); + if (defined $netboot) { + $nrtab->setNodeAttribs($node, { netboot => $netboot }); } } From 360beccb73f168e63fb1ae7ef84a0551227274d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:34:16 -0300 Subject: [PATCH 06/54] test(xCAT-test): cover the nodediscover default netboot ladder Exercise _default_netboot() with the database-backed modules stubbed: riscv64 defaults to grub2 and keeps any grub2 variant, the x86, PowerNV, ppc, onie and aarch64 outcomes are unchanged, and undefined inputs do not warn. --- xCAT-test/unit/nodediscover_default_netboot.t | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 xCAT-test/unit/nodediscover_default_netboot.t diff --git a/xCAT-test/unit/nodediscover_default_netboot.t b/xCAT-test/unit/nodediscover_default_netboot.t new file mode 100644 index 000000000..39641171d --- /dev/null +++ b/xCAT-test/unit/nodediscover_default_netboot.t @@ -0,0 +1,86 @@ +#!/usr/bin/env perl +use strict; +use warnings; +## no critic (Modules::RequireFilenameMatchesPackage) + +use FindBin; +use File::Spec; +use Test::More; + +# nodediscover sets noderes.netboot for a freshly discovered node when the +# admin did not pick a method that fits the reported architecture. The ladder +# lives in _default_netboot() so it can be checked without a database: stub +# the modules the plugin loads at compile time and call the helper directly. + +BEGIN { + package xCAT::Table; + $INC{'xCAT/Table.pm'} = __FILE__; + + package xCAT::Utils; + $INC{'xCAT/Utils.pm'} = __FILE__; + + package XML::Simple; + sub import { } + $INC{'XML/Simple.pm'} = __FILE__; + + package xCAT::data::switchinfo; + our %global_mac_identity; + $INC{'xCAT/data/switchinfo.pm'} = __FILE__; + + package xCAT::DiscoveryUtils; + $INC{'xCAT/DiscoveryUtils.pm'} = __FILE__; +} + +my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); +my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'nodediscover.pm' ); +plan skip_all => "$plugin not found" unless -r $plugin; + +require $plugin; + +my $warnings = 0; +local $SIG{__WARN__} = sub { $warnings++; diag(@_) }; + +sub default_netboot { + return xCAT_plugin::nodediscover::_default_netboot(@_); +} + +# riscv64: UEFI + grub2 only +is( default_netboot( 'riscv64', '', '' ), 'grub2', 'riscv64 nodes default to grub2' ); +is( default_netboot( 'riscv64', undef, undef ), 'grub2', 'riscv64 defaults to grub2 when no platform or netboot was reported' ); +is( default_netboot( 'riscv64', '', 'xnba' ), 'grub2', 'a riscv64 node with an x86 method is corrected to grub2' ); +is( default_netboot( 'riscv64', '', 'grub2' ), undef, 'riscv64 keeps an explicit grub2 method' ); +is( default_netboot( 'riscv64', '', 'grub2-http' ), undef, 'riscv64 keeps grub2-http' ); +is( default_netboot( 'riscv64', '', 'grub2-tftp' ), undef, 'riscv64 keeps grub2-tftp' ); +is( default_netboot( 'riscv32', '', '' ), undef, 'riscv32 is not treated as riscv64' ); + +# existing architectures keep their behavior +is( default_netboot( 'x86_64', '', '' ), 'xnba', 'x86_64 nodes default to xnba' ); +is( default_netboot( 'x86_64', '', 'pxe' ), undef, 'x86_64 keeps pxe' ); +is( default_netboot( 'x86_64', '', 'xnba' ), undef, 'x86_64 keeps xnba' ); +is( default_netboot( 'x86', '', 'grub2' ), 'xnba', 'x86 with a non-x86 method is corrected to xnba' ); +is( default_netboot( 'ppc64le', 'PowerNV', '' ), 'petitboot', 'PowerNV nodes default to petitboot' ); +is( default_netboot( 'ppc64le', 'PowerNV', 'petitboot' ), 'petitboot', 'PowerNV nodes are always set to petitboot' ); +is( default_netboot( 'ppc64', 'pSeries', '' ), 'yaboot', 'non-PowerNV ppc nodes default to yaboot' ); +is( default_netboot( 'ppc64', 'pSeries', 'yaboot' ), undef, 'ppc nodes keep yaboot' ); +is( default_netboot( 'armv7l', '', '' ), 'onie', 'armv7l switches default to onie' ); +is( default_netboot( 'armv7l', '', 'onie' ), undef, 'armv7l keeps onie' ); +is( default_netboot( 'aarch64', '', '' ), undef, 'aarch64 is left untouched, as before' ); +is( default_netboot( undef, undef, undef ), undef, 'a missing arch sets nothing' ); + +is( $warnings, 0, 'no warnings were emitted for undefined inputs' ); + +# The discovery request is stored as discovery data key by key, so reading the platform +# of a node that never reported one must not add the key to the request. +my $source = do { open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; local $/; <$fh> }; +like( + $source, + qr/my \$platform = exists \$request->\{platform\} \? \$request->\{platform\}->\[0\] : undef;/, + 'the netboot ladder only reads the platform of a request that carries one', +); +unlike( + $source, + qr/_default_netboot\(\s*\$request->\{arch\}->\[0\],\s*\$request->\{platform\}->\[0\]/, + 'the netboot ladder does not autovivify the platform key of the discovery request', +); + +done_testing(); From 033b16d522e215678ebdbff70730bfa27a7d45bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:39:34 -0300 Subject: [PATCH 07/54] feat(mknb): write grub2 discovery configs for riscv64 mknb only knew how to publish a discovery boot configuration for x86 (PXELINUX and xNBA) and POWER (petitboot). Any other architecture got a Genesis kernel and initramfs under /tftpboot/xcat and nothing that would make a firmware boot them, so riscv64 discovery could not start. riscv64 nodes boot through UEFI and grub2. Write one grub2 configuration per network, /tftpboot/boot/grub2/grub.cfg-, using the same network keys as the PXELINUX files. A net-booted grub2.riscv64 searches grub.cfg-01-, grub.cfg-<8 hex ip> and then shorter prefixes of the ip, so the per-node files that nodeset writes keep priority and the network file is only reached by clients without a node configuration. The file is regenerated from the published Genesis artifacts (lzma preferred over gzip), guarded by $grub_cpu so other grub2 architectures can share it later, carries the xcatd endpoint, the serial console and BOOTIF=$net_default_mac, and is dropped for networks served by a :noboot interface. It is written by name rather than into an existing file, because on a /32 network nodeset's hard link for the node carries the same name. Publishing a Genesis image now also drops the other compression variant of that architecture, so a leftover genesis.fs..lzma can no longer be paired with a freshly published kernel by this configuration or by --configfileonly. And since these configurations are only reachable through grub2., which xCAT does not build, a missing boot loader is reported instead of leaving the nodes to time out in firmware. xcatconfig now also runs mknb riscv64 when xCAT-genesis-base-riscv64 is installed, and the usage text lists the architecture. --- xCAT-server/lib/xcat/plugins/mknb.pm | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index a3a2a5b91..94702fe66 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -18,6 +18,14 @@ sub _canonical_genesis_arch { return $arch eq 'ppc64el' ? 'ppc64le' : $arch; } +# Architectures whose discovery boot goes through UEFI firmware and grub2. +# mknb writes one grub2 configuration per network for them. The value is the +# $grub_cpu string reported by that architecture's GRUB build, so one file can +# carry a menu entry per architecture that shares a network. +my %GRUB2_DISCOVERY_ARCHES = ( + riscv64 => 'riscv64', +); + sub handled_commands { return { mknb => 'mknb', @@ -703,6 +711,9 @@ sub process_request { } } elsif ($arch =~ /ppc/) { mkpath("$tftpdir/pxelinux.cfg/p/"); + } elsif (exists $GRUB2_DISCOVERY_ARCHES{$arch}) { + mkpath("$tftpdir/boot/grub2"); + chmod(0755, "$tftpdir/boot/grub2"); } my $dopxe = 0; foreach (keys %{$normnets}) { @@ -816,11 +827,97 @@ sub process_request { print $cfgfile " initrd http://" . $xcatd_address . "$portsuffix/$initrd_file\n"; print $cfgfile ' append "xcatd=' . $xcatd_address . ":$xcatdport $consolecmdline\"\n"; close($cfgfile); + } elsif (exists $GRUB2_DISCOVERY_ARCHES{$arch}) { + # Unlike the PXELINUX files, which only look at the legacy address of the network, + # the discovery configuration is also dropped when the xcatd address chosen for the + # network sits on a :noboot interface: it would send Genesis to an interface that + # must not serve booting. + if (defined($nobootnicips{ $hexnets->{$_} }) or defined($nobootnicips{$xcatd_address})) { + unlink("$tftpdir/boot/grub2/grub.cfg-" . uc($_)); + next; + } + _write_grub2_discovery_config( + tftpdir => $tftpdir, + hexnet => $_, + xcatd_address => $xcatd_address, + xcatdport => $xcatdport, + consolecmdline => $consolecmdline, + ); } } + if (exists $GRUB2_DISCOVERY_ARCHES{$arch} and !-e "$tftpdir/boot/grub2/grub2.$arch") { + # The discovery configurations are only reached through this boot loader, which + # xCAT does not build: say so instead of letting the nodes time out in firmware. + $callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (it is installed by grub2-xcat, or copied from the EL $arch installation media)"] }); + } if ($configfileonly) { $callback->({ data => ["Write netboot config file done"] }); } } +# Return the grub2-class architectures whose Genesis kernel and initrd are +# published under $tftpdir/xcat, as [arch, grub_cpu, kernel, initrd] with the +# file names relative to the TFTP root. + +sub _grub2_discovery_arches { + my ($tftpdir) = @_; + my @present; + foreach my $arch (sort keys %GRUB2_DISCOVERY_ARCHES) { + next unless -e "$tftpdir/xcat/genesis.kernel.$arch"; + my ($initrd) = grep { -e "$tftpdir/$_" } + map { "xcat/genesis.fs.$arch.$_" } qw(lzma gz); + next unless $initrd; + push @present, + [ $arch, $GRUB2_DISCOVERY_ARCHES{$arch}, "xcat/genesis.kernel.$arch", $initrd ]; + } + return @present; +} + +# Write the grub2 discovery configuration for one network. +# +# grub2., net booted from $tftpdir/boot/grub2, looks for grub.cfg-01-, +# then grub.cfg-<8 hex digit ip>, then ever shorter prefixes of that ip, and +# finally grub.cfg. nodeset writes the per-node files (full ip and mac), so a +# per-network prefix is only reached by clients without a node configuration: +# discovery. The file is rebuilt from the Genesis artifacts present under +# $tftpdir/xcat and removed when none are left. Returns the path written. +sub _write_grub2_discovery_config { + my (%args) = @_; + my $tftpdir = $args{tftpdir}; + my $hexnet = uc($args{hexnet}); + my $cfgpath = "$tftpdir/boot/grub2/grub.cfg-$hexnet"; + my @arches = _grub2_discovery_arches($tftpdir); + unless (@arches) { + unlink($cfgpath); + return; + } + my $cmdline = "xcatd=$args{xcatd_address}:$args{xcatdport}"; + if (defined($args{consolecmdline}) and $args{consolecmdline} ne '') { + $cmdline .= " $args{consolecmdline}"; + } + my $content = "# xCAT Genesis discovery for network $hexnet - generated by mknb, do not edit\n"; + $content .= "set default=0\n"; + $content .= "set timeout=5\n"; + my $keyword = 'if'; + foreach my $entry (@arches) { + my ($arch, $grub_cpu, $kernel, $initrd) = @{$entry}; + $content .= "$keyword [ \"\$grub_cpu\" = \"$grub_cpu\" ]; then\n"; + $content .= "menuentry \"xCAT Genesis $arch\" {\n"; + $content .= " linux /$kernel $cmdline BOOTIF=\$net_default_mac\n"; + $content .= " initrd /$initrd\n"; + $content .= "}\n"; + $keyword = 'elif'; + } + $content .= "fi\n"; + # nodeset hard-links grub.cfg-<8 hex digit ip> to the node file; on a /32 network that is + # this file's name, so replace the name instead of truncating a shared inode. + unlink($cfgpath); + my $cfg; + open($cfg, ">", $cfgpath) or return; + print $cfg $content; + close($cfg); + chmod(0644, $cfgpath); + return $cfgpath; +} + 1; From ee3df5c3e616dcec77703332634325ce465c10e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:39:34 -0300 Subject: [PATCH 08/54] test(xCAT-test): cover the riscv64 mknb discovery configuration Drive mknb riscv64 --configfileonly through the existing stubs and assert the grub2 network configuration: name, cpu guard, kernel, initrd, xcatd endpoint, serial console, lzma preference, no PXELINUX/xNBA/ petitboot output, removal without artifacts and for :noboot interfaces. Also install a riscv64 Genesis export and reject it for another architecture. --- xCAT-test/unit/mknb_xcatd_address.t | 147 +++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t index 56339f98e..b20e335fd 100644 --- a/xCAT-test/unit/mknb_xcatd_address.t +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -13,10 +13,11 @@ BEGIN { $INC{'xCAT/Utils.pm'} = __FILE__; package xCAT::TableUtils; - our ($tftpdir, $site_master, $site_httpport); + our ($tftpdir, $site_master, $site_httpport, %site_extra); sub getTftpDir { return $tftpdir; } sub get_site_attribute { my $attribute = $_[-1]; + return ($site_extra{$attribute}) if exists $site_extra{$attribute}; return ($site_master) if $attribute eq 'master'; return ($site_httpport) if $attribute eq 'httpport' and defined $site_httpport; return; @@ -36,6 +37,8 @@ BEGIN { return $hexnet_addresses; } sub getipaddr { return @master_addresses; } + our $nic_ips; + sub get_nic_ip { return $nic_ips || {}; } $INC{'xCAT/NetworkUtils.pm'} = __FILE__; package xCAT::NodeRange; @@ -334,4 +337,146 @@ is( 'legacy PXE output is byte-identical without a site.master preference', ); +# riscv64 discovery boots through UEFI firmware and grub2: mknb writes one +# grub2 configuration per network, named by the network's hex prefix the same +# way PXELINUX files are, instead of PXELINUX/xNBA or petitboot files. +make_path("$::XCATROOT/share/xcat/netboot/genesis/riscv64"); +use_reporter_address_maps(); +prepare_tftpdir($tmpdir, 'tftpboot-riscv64', 'riscv64'); +$responses = run_mknb('riscv64'); +generation_succeeded($responses, 'riscv64 configuration generation succeeds'); + +my $grub_cfg_path = "$xCAT::TableUtils::tftpdir/boot/grub2/grub.cfg-C0A89"; +ok(-f $grub_cfg_path, 'riscv64 writes a grub2 configuration named by the network hex prefix'); +my $grub_cfg = read_config($grub_cfg_path); +like( + $grub_cfg, + qr/^# xCAT Genesis discovery for network C0A89 - generated by mknb, do not edit$/m, + 'the grub2 discovery configuration names its network and generator', +); +like($grub_cfg, qr/^set default=0$/m, 'the grub2 discovery configuration selects the first entry'); +like($grub_cfg, qr/^set timeout=5$/m, 'the grub2 discovery configuration boots after a short timeout'); +like( + $grub_cfg, + qr/^if \[ "\$grub_cpu" = "riscv64" \]; then$/m, + 'the riscv64 menu entry is guarded by the GRUB cpu', +); +like($grub_cfg, qr/^menuentry "xCAT Genesis riscv64" \{$/m, 'the menu entry names the architecture'); +like( + $grub_cfg, + qr/^ linux \/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3001 BOOTIF=\$net_default_mac$/m, + 'the kernel line loads the Genesis kernel with the xcatd endpoint and the booting MAC', +); +like($grub_cfg, qr/^ initrd \/xcat\/genesis\.fs\.riscv64\.gz$/m, 'the initrd line loads the Genesis initramfs'); +like($grub_cfg, qr/^\}\nfi\n\z/m, 'the configuration closes the menu entry and the cpu guard'); +unlike( + $grub_cfg, + qr/\Q$xCAT::TableUtils::tftpdir\E/, + 'the grub2 configuration does not expose the configured TFTP root', +); +unlike( + $grub_cfg, + qr/xcatd=192\.168\.149\.100:3001/, + 'the grub2 configuration does not use the later floating address as the xcatd endpoint', +); +ok(!-e "$xCAT::TableUtils::tftpdir/pxelinux.cfg/C0A89", 'riscv64 writes no PXELINUX configuration'); +ok(!-e "$xCAT::TableUtils::tftpdir/xcat/xnba", 'riscv64 writes no xNBA configuration'); +ok(!-e "$xCAT::TableUtils::tftpdir/etc/c0a89", 'riscv64 writes no petitboot configuration'); +ok(!-e "$xCAT::TableUtils::tftpdir/pxelinux.cfg/p", 'riscv64 writes no POWER network configuration'); +is_deeply( + [ xCAT_plugin::mknb::_grub2_discovery_arches($xCAT::TableUtils::tftpdir) ], + [ [ 'riscv64', 'riscv64', 'xcat/genesis.kernel.riscv64', 'xcat/genesis.fs.riscv64.gz' ] ], + 'the published riscv64 Genesis artifacts are enumerated relative to the TFTP root', +); + +# serial console settings reach the Genesis kernel line +%xCAT::TableUtils::site_extra = ( + defserialport => '0', + defserialspeed => '115200', + defserialflow => 'hard', + xcatdport => '3002', +); +$responses = run_mknb('riscv64'); +generation_succeeded($responses, 'riscv64 configuration generation succeeds with a serial console'); +like( + read_config($grub_cfg_path), + qr/^ linux \/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3002 console=tty0 console=ttyS0,115200n8r BOOTIF=\$net_default_mac$/m, + 'the serial console and a non-default xcatd port are carried into the kernel line', +); +%xCAT::TableUtils::site_extra = (); + +# an lzma initramfs is preferred when mknb produced one +open(my $lzma_fh, '>', "$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma") or die "Unable to create lzma: $!"; +close($lzma_fh); +$responses = run_mknb('riscv64'); +generation_succeeded($responses, 'riscv64 configuration generation succeeds with an lzma initramfs'); +like( + read_config($grub_cfg_path), + qr/^ initrd \/xcat\/genesis\.fs\.riscv64\.lzma$/m, + 'the lzma Genesis initramfs is preferred over the gzip one', +); +unlink("$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma"); + +# the discovery configurations are only reachable through grub2., which mknb does not +# build: a missing boot loader is reported instead of failing silently in firmware +{ + my $loader = "$xCAT::TableUtils::tftpdir/boot/grub2/grub2.riscv64"; + my $missing = run_mknb('riscv64'); + generation_succeeded($missing, 'riscv64 configuration generation succeeds without the grub2 boot loader'); + ok( + scalar(grep { ref($_) eq 'HASH' && $_->{data} && "@{$_->{data}}" =~ m{\Qboot/grub2/grub2.riscv64\E is missing} } @{$missing}), + 'a missing grub2.riscv64 boot loader is reported', + ); + make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); + open(my $loader_fh, '>', $loader) or die "Unable to create $loader: $!"; + close($loader_fh); + my $present = run_mknb('riscv64'); + ok( + !grep({ ref($_) eq 'HASH' && $_->{data} && "@{$_->{data}}" =~ /is missing/ } @{$present}), + 'nothing is reported once the boot loader is in place', + ); + unlink($loader); +} + + +# the configuration is rebuilt from what is published: without artifacts it goes away +unlink("$xCAT::TableUtils::tftpdir/xcat/genesis.kernel.riscv64"); +is( + xCAT_plugin::mknb::_write_grub2_discovery_config( + tftpdir => $xCAT::TableUtils::tftpdir, + hexnet => 'c0a89', + xcatd_address => '192.168.148.10', + xcatdport => 3001, + ), + undef, + 'no grub2 discovery configuration is written without a published Genesis kernel', +); +ok(!-e $grub_cfg_path, 'a stale grub2 discovery configuration is removed with its artifacts'); + +# --configfileonly without published artifacts fails like the other architectures +$xCAT::TableUtils::tftpdir = "$tmpdir/tftpboot-riscv64-empty"; +make_path("$xCAT::TableUtils::tftpdir/xcat"); +$responses = run_mknb('riscv64'); +ok( + scalar(grep { ref($_) eq 'HASH' && $_->{error} && "@{$_->{error}}" =~ /No kernel file found/ } @{$responses}), + 'riscv64 --configfileonly without a kernel reports the missing kernel', +); + +# a :noboot interface keeps no discovery configuration for its network +%xCAT::TableUtils::site_extra = ( dhcpinterfaces => 'eth0,eth1:noboot' ); +$xCAT::NetworkUtils::nic_ips = { eth0 => '10.0.0.1', eth1 => '192.168.148.10' }; +use_reporter_address_maps(); +prepare_tftpdir($tmpdir, 'tftpboot-riscv64-noboot', 'riscv64'); +make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); +open(my $stale_fh, '>', "$xCAT::TableUtils::tftpdir/boot/grub2/grub.cfg-C0A89") or die "Unable to create stale config: $!"; +close($stale_fh); +$responses = run_mknb('riscv64'); +generation_succeeded($responses, 'riscv64 configuration generation succeeds with a :noboot interface'); +ok( + !-e "$xCAT::TableUtils::tftpdir/boot/grub2/grub.cfg-C0A89", + 'a network served by a :noboot interface gets no grub2 discovery configuration', +); +%xCAT::TableUtils::site_extra = (); +$xCAT::NetworkUtils::nic_ips = undef; + done_testing(); From 2ef44d5d7f7553b7c0a7bf3d93da27e1a1a0180a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:42:19 -0300 Subject: [PATCH 09/54] feat(xCAT-server): install and netboot EL10 riscv64 nodes EL riscv64 media lay out the installer kernel and initrd under images/pxeboot exactly like x86 and aarch64 media, but anaconda.pm only looked there for those two families and geninitrd.pm refused riscv64 outright ("unknow arch"). Treat riscv64 like x86/aarch64 in both places and recognise riscv64 kernels when a driver disk updates the installer kernel. There is no riscv64 SUSE media, so geninitrd keeps the unsupported-architecture error for sles/suse rather than reading the x86 SUSE layout. Diskless images get a riscv64 default network driver list (virtio, Intel, Realtek, Broadcom and Mellanox) and take the resolver libraries from lib64, which is where riscv64 EL puts them. --- xCAT-server/lib/xcat/plugins/anaconda.pm | 4 ++-- xCAT-server/lib/xcat/plugins/geninitrd.pm | 4 +++- xCAT-server/share/xcat/netboot/imgutils/imgutils.pm | 1 + xCAT-server/share/xcat/netboot/rh/genimage | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/anaconda.pm b/xCAT-server/lib/xcat/plugins/anaconda.pm index 9107f26ed..334f17108 100644 --- a/xCAT-server/lib/xcat/plugins/anaconda.pm +++ b/xCAT-server/lib/xcat/plugins/anaconda.pm @@ -1342,7 +1342,7 @@ sub mkinstall if ( ( - ( $arch =~ /x86/ or $arch =~ /aarch64/ ) and + ( $arch =~ /x86/ or $arch =~ /aarch64/ or $arch =~ /riscv64/ ) and ( -r "$pkgdir/images/pxeboot/vmlinuz" and $kernpath = "$pkgdir/images/pxeboot/vmlinuz" @@ -2798,7 +2798,7 @@ sub insert_dd { # and copy it to the /tftpboot my @new_kernels = <$dd_dir/rpm/boot/vmlinuz*>; foreach my $new_kernel (@new_kernels) { - if (-r $new_kernel && $new_kernel =~ /\/vmlinuz-(.*(x86_64|ppc64|el\d+|ppc64le|aarch64))$/) { + if (-r $new_kernel && $new_kernel =~ /\/vmlinuz-(.*(x86_64|ppc64|el\d+|ppc64le|aarch64|riscv64))$/) { $new_kernel_ver = $1; $cmd = "/bin/mv -f $new_kernel $kernelpath"; xCAT::Utils->runcmd($cmd, -1); diff --git a/xCAT-server/lib/xcat/plugins/geninitrd.pm b/xCAT-server/lib/xcat/plugins/geninitrd.pm index 157280ee7..47b07e883 100644 --- a/xCAT-server/lib/xcat/plugins/geninitrd.pm +++ b/xCAT-server/lib/xcat/plugins/geninitrd.pm @@ -189,7 +189,9 @@ sub geninitrd { unless (-d $tftppath) { mkpath $tftppath; } - if ($arch =~ /x86/) { + # riscv64 installation media exist for the EL family only; leave any other + # distribution to the "unknow arch" error below instead of reading it as x86 + if ($arch =~ /x86/ or ($arch =~ /riscv64/ and $osvers !~ /sles|suse/)) { if ($osvers =~ /(^ol[0-9].*)|(centos.*)|(alma.*)|(rocky.*)|(rh.*)|(fedora.*)|(SL.*)/) { $kernelpath = "$tftppath/vmlinuz"; copy("$pkgdir/images/pxeboot/vmlinuz", $kernelpath); diff --git a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm index ea01a75ff..a5881c984 100644 --- a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm +++ b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm @@ -256,6 +256,7 @@ sub default_net_drivers { x86 => [qw(tg3 bnx2 bnx2x e1000 e1000e igb mlx_en mlx5_core virtio_net be2net)], x86_64 => [qw(tg3 bnx2 bnx2x e1000 e1000e igb mlx_en mlx5_core virtio_net be2net)], aarch64 => [qw(tg3 bnx2 bnx2x e1000e igb mlx_en mlx5_core virtio_net)], + riscv64 => [qw(e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net)], ppc64 => [qw(e1000 e1000e igb ibmveth ehea)], s390x => [qw(qdio ccwgroup)], }, diff --git a/xCAT-server/share/xcat/netboot/rh/genimage b/xCAT-server/share/xcat/netboot/rh/genimage index 20424cd82..3dc1c392d 100755 --- a/xCAT-server/share/xcat/netboot/rh/genimage +++ b/xCAT-server/share/xcat/netboot/rh/genimage @@ -1924,7 +1924,7 @@ EOMS } } - if ($arch =~ /x86_64/ or $arch =~ /aarch64/) { + if ($arch =~ /x86_64/ or $arch =~ /aarch64/ or $arch =~ /riscv64/) { push @filestoadd, "lib64/libnss_dns.so.2"; push @filestoadd, "lib64/libresolv.so.2"; } else { From 16eea29daf0c315579c0d8d8f99db44aa7d7265f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:42:19 -0300 Subject: [PATCH 10/54] test(xCAT-test): cover EL riscv64 install media and diskless defaults Pin the riscv64 installer kernel lookup in anaconda.pm and geninitrd.pm, and evaluate the rh/genimage default driver and resolver library blocks for riscv64 and the existing architectures. Add an autotest nodeset case with bogus riscv64 grub2 nodes and a rhels10.99 riscv64 osimage, checking the per-node grub2 files, the grub2.riscv64 loader link and the hex-ip/mac links. --- xCAT-test/autotest/testcase/nodeset/cases1 | 7 ++ .../autotest/testcase/nodeset/functions.sh | 28 +++++- .../nodeset/nodeset_osimage_grub2_riscv64 | 73 +++++++++++++++ xCAT-test/unit/el_riscv64_install_paths.t | 88 +++++++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100755 xCAT-test/autotest/testcase/nodeset/nodeset_osimage_grub2_riscv64 create mode 100644 xCAT-test/unit/el_riscv64_install_paths.t diff --git a/xCAT-test/autotest/testcase/nodeset/cases1 b/xCAT-test/autotest/testcase/nodeset/cases1 index 8667e37ae..b3196702d 100644 --- a/xCAT-test/autotest/testcase/nodeset/cases1 +++ b/xCAT-test/autotest/testcase/nodeset/cases1 @@ -26,6 +26,13 @@ cmd:/opt/xcat/share/xcat/tools/autotest/testcase/nodeset/nodeset_osimage_grub2 check:rc==0 end +start:nodeset_osimage_grub2_riscv64 +label:others +description: Verify if `nodeset osimage` perform well for riscv64 nodes booted by grub2 over UEFI when part of nodes in a node range have problem +cmd:/opt/xcat/share/xcat/tools/autotest/testcase/nodeset/nodeset_osimage_grub2_riscv64 +check:rc==0 +end + start:nodeset_osimage_petitboot label:others description: Verify if `nodeset osimage` perform well when part of nodes in a node range have problem. Use petitboot OS loader diff --git a/xCAT-test/autotest/testcase/nodeset/functions.sh b/xCAT-test/autotest/testcase/nodeset/functions.sh index 5476a9411..fbd73fef0 100644 --- a/xCAT-test/autotest/testcase/nodeset/functions.sh +++ b/xCAT-test/autotest/testcase/nodeset/functions.sh @@ -118,6 +118,21 @@ function make_bogus_petitboot_nodes() done } +function make_bogus_riscv64_grub2_nodes() +{ + local i + # riscv64 boots through UEFI and grub2 + for i in {001..005} + do + mkdef -t node -o tz${i} \ + arch=riscv64 cons=ipmi groups=riscv64 mgt=ipmi \ + netboot=grub2 \ + ip=10.99.1.$((10#${i})) \ + mac=e6:d4:d2:3a:ad:0$((10#${i})) \ + profile=compute os=rhels10.99 + done +} + function make_bogus_xnba_nodes() { local i @@ -181,6 +196,16 @@ function make_bogus_x64_osimage() echo blah >/install/rhels6.99/x86_64/images/pxeboot/initrd.img } +function make_bogus_riscv64_osimage() +{ + mkdef "rhels10.99-riscv64-install-compute" \ + -u profile=compute provmethod=install \ + osvers=rhels10.99 osarch=riscv64 + mkdir -p /install/rhels10.99/riscv64/images/pxeboot + echo blah >/install/rhels10.99/riscv64/images/pxeboot/vmlinuz + echo blah >/install/rhels10.99/riscv64/images/pxeboot/initrd.img +} + function destory_bogus_osimages() { local o @@ -188,7 +213,8 @@ function destory_bogus_osimages() rhels7.99-ppc64le-install-compute \ rhels7.99-ppc64-install-compute \ rhels6.99-ppc64-install-compute \ - rhels6.99-x86_64-install-compute + rhels6.99-x86_64-install-compute \ + rhels10.99-riscv64-install-compute do rmdef -t osimage ${o} done diff --git a/xCAT-test/autotest/testcase/nodeset/nodeset_osimage_grub2_riscv64 b/xCAT-test/autotest/testcase/nodeset/nodeset_osimage_grub2_riscv64 new file mode 100755 index 000000000..2dc293615 --- /dev/null +++ b/xCAT-test/autotest/testcase/nodeset/nodeset_osimage_grub2_riscv64 @@ -0,0 +1,73 @@ +#!/bin/bash + +BASE_DIR="${0%/*}" + +! source "${BASE_DIR}/functions.sh" >/dev/null 2>&1 && + echo "File \"${BASE_DIR}/functions.sh\" not found" >&2 && exit 1 + +make_bogus_riscv64_osimage +make_bogus_riscv64_grub2_nodes + +# riscv64 nodes are booted by the grub2.riscv64 UEFI image; provide a bogus +# one when the management node does not ship it so nodeset can link to it +GRUB2_RISCV64="/tftpboot/boot/grub2/grub2.riscv64" +BOGUS_GRUB2_RISCV64="" +if [ ! -e "${GRUB2_RISCV64}" ] +then + mkdir -p "${GRUB2_RISCV64%/*}" + echo blah >"${GRUB2_RISCV64}" + BOGUS_GRUB2_RISCV64="${GRUB2_RISCV64}" +fi + +function custom_cleanup() +{ + destory_bogus_nodes + destory_bogus_osimages + [ -n "${BOGUS_GRUB2_RISCV64}" ] && rm -f "${BOGUS_GRUB2_RISCV64}" +} + +chdef tz002 mac= +chdef tz003 arch= + +NODESET_STDOUT="${TMP_DIR}/nodeset.out" +NODESET_STDERR="${TMP_DIR}/nodeset.err" + +nodeset tz001+4 osimage=rhels10.99-riscv64-install-compute \ + > >(tee "${NODESET_STDOUT}") 2> >(tee "${NODESET_STDERR}") +# Check $? != 0 +[ "$?" -ne "0" ] +exit_if_bad "$?" "nodeset command should exit with non-zero" + +grep 'tz002: .*o MAC address' "${NODESET_STDERR}" +exit_if_bad "$?" "nodeset command error message checking failed" + +# Make sure all other nodes are good +for node in tz001 tz004 tz005 +do + [ -f "/tftpboot/boot/grub2/${node}" ] + exit_if_bad "$?" "file not found /tftpboot/boot/grub2/${node}" + + # the per-node loader link points at the riscv64 grub2 image + [ "$(readlink "/tftpboot/boot/grub2/grub2-${node}")" = "grub2.riscv64" ] + exit_if_bad "$?" "grub2-${node} does not link to grub2.riscv64" + + # the installer kernel and initrd come from images/pxeboot + grep 'vmlinuz' "/tftpboot/boot/grub2/${node}" + exit_if_bad "$?" "installer kernel not found in /tftpboot/boot/grub2/${node}" + grep 'initrd.img' "/tftpboot/boot/grub2/${node}" + exit_if_bad "$?" "installer initrd not found in /tftpboot/boot/grub2/${node}" + + # the riscv64 grub2 image has no linuxefi command + ! grep 'linuxefi' "/tftpboot/boot/grub2/${node}" + exit_if_bad "$?" "linuxefi must not be used for riscv64 in /tftpboot/boot/grub2/${node}" +done + +# the hex ip and mac links nodeset writes shadow the discovery network files +[ -f "/tftpboot/boot/grub2/grub.cfg-0A630101" ] +exit_if_bad "$?" "file not found /tftpboot/boot/grub2/grub.cfg-0A630101" +[ -f "/tftpboot/boot/grub2/grub.cfg-01-e6-d4-d2-3a-ad-01" ] +exit_if_bad "$?" "file not found /tftpboot/boot/grub2/grub.cfg-01-e6-d4-d2-3a-ad-01" + +nodeset tz001+4 offline + +exit 0 diff --git a/xCAT-test/unit/el_riscv64_install_paths.t b/xCAT-test/unit/el_riscv64_install_paths.t new file mode 100644 index 000000000..c204ac238 --- /dev/null +++ b/xCAT-test/unit/el_riscv64_install_paths.t @@ -0,0 +1,88 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use File::Spec; +use Test::More; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils"; +use imgutils; + +# EL riscv64 media and diskless images: the installer kernel/initrd live under +# images/pxeboot like x86 and aarch64 media, and riscv64 diskless images need +# their own default network drivers and lib64 resolver libraries. The +# behavior is pinned without running the database-backed plugins: the arch +# conditions are asserted in the shipped source and the resolver block is +# extracted and evaluated directly. + +my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); + +sub slurp { + my ($relative) = @_; + my $path = File::Spec->catfile( $repo_root, split( m{/}, $relative ) ); + plan skip_all => "$path not found" unless -r $path; + open( my $fh, '<', $path ) or die "Unable to read $path: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +my $anaconda = slurp('xCAT-server/lib/xcat/plugins/anaconda.pm'); +my $geninitrd = slurp('xCAT-server/lib/xcat/plugins/geninitrd.pm'); +my $genimage = slurp('xCAT-server/share/xcat/netboot/rh/genimage'); + +# anaconda.pm: stateful install kernel/initrd discovery +like( + $anaconda, + qr/\( \$arch =~ \/x86\/ or \$arch =~ \/aarch64\/ or \$arch =~ \/riscv64\/ \) and\n\s*\(\n\s*-r "\$pkgdir\/images\/pxeboot\/vmlinuz"/, + 'anaconda looks for riscv64 installer kernels under images/pxeboot like x86 and aarch64', +); +like( + $anaconda, + qr/\/\\\/vmlinuz-\(\.\*\(x86_64\|ppc64\|el\\d\+\|ppc64le\|aarch64\|riscv64\)\)\$\//, + 'driver-disk kernel updates recognise riscv64 kernels', +); + +# geninitrd.pm: diskless installer initrd source +like( + $geninitrd, + qr/if \(\$arch =~ \/x86\/ or \(\$arch =~ \/riscv64\/ and \$osvers !~ \/sles\|suse\/\)\) \{\n\s*if \(\$osvers =~ \/\(\^ol\[0-9\]\.\*\)\|\(centos\.\*\)\|\(alma\.\*\)\|\(rocky\.\*\)\|\(rh\.\*\)\|\(fedora\.\*\)\|\(SL\.\*\)\/\) \{\n\s*\$kernelpath = "\$tftppath\/vmlinuz";\n\s*copy\("\$pkgdir\/images\/pxeboot\/vmlinuz", \$kernelpath\);/, + 'geninitrd copies riscv64 EL kernels from images/pxeboot', +); +like( + $geninitrd, + qr/\$arch =~ \/riscv64\/ and \$osvers !~ \/sles\|suse\//, + 'a SUSE osimage on riscv64 keeps the unsupported-architecture error instead of reading SUSE installer media paths', +); +unlike( + $geninitrd, + qr/\} elsif \(\$arch =~ \/riscv64\/\)/, + 'geninitrd does not need a separate riscv64 branch', +); + +is_deeply( + [ imgutils::default_net_drivers( 'rh', 'riscv64' ) ], + [qw/e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net/], + 'riscv64 diskless images default to virtio, Intel, Realtek, Broadcom and Mellanox drivers', +); + +# rh/genimage: resolver libraries for the boot image +my ($lib_block) = $genimage =~ m{^(\s*if \(\$arch =~ /x86_64/ or \$arch =~ /aarch64/ or \$arch =~ /riscv64/\) \{\n\s*push \@filestoadd, "lib64/libnss_dns\.so\.2";\n.*?^\s*\}\n)}ms; +ok( $lib_block, 'the resolver library block was located in rh/genimage' ) + or BAIL_OUT('rh/genimage no longer matches the expected resolver library block'); + +sub resolver_libs { + my ($arch) = @_; + my $code = "sub { my \$arch = shift; my \@filestoadd;\n$lib_block\n return \@filestoadd; }"; + my $sub = eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval) + die "Unable to evaluate the resolver library block: $@" if $@; + return [ $sub->($arch) ]; +} + +is_deeply( resolver_libs('riscv64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], 'riscv64 images take the resolver libraries from lib64' ); +is_deeply( resolver_libs('x86_64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], 'x86_64 images still use lib64' ); +is_deeply( resolver_libs('ppc64'), [ 'lib/libnss_dns.so.2', 'lib/libresolv.so.2' ], 'ppc64 images still use lib' ); + +done_testing(); From 3917fd83453c7c207bdf60d000b98c09bc53c385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:45:21 -0300 Subject: [PATCH 11/54] feat(xCAT-server): add EL10 riscv64 osimage profiles Provide the compute and service profiles for rocky10 and rhels10 on riscv64: netboot pkglist/exlist/postinstall files and the service otherpkgs lists for netboot and install. They mirror the x86_64 profiles (every package resolves in the Rocky 10 riscv64 BaseOS/AppStream/CRB repositories and the minimal-environment group exists there), with the kbd keymap exclude spelled correctly, the duplicate man exclude dropped, and goconserver pulled from the rh10/riscv64 dependency repository. --- .../service.rhels10.riscv64.otherpkgs.pkglist | 2 + .../service.rocky10.riscv64.otherpkgs.pkglist | 2 + .../netboot/rh/compute.rhels10.riscv64.exlist | 33 ++++++++++ .../rh/compute.rhels10.riscv64.pkglist | 30 +++++++++ .../rh/compute.rhels10.riscv64.postinstall | 62 +++++++++++++++++++ .../netboot/rh/service.rhels10.riscv64.exlist | 33 ++++++++++ .../service.rhels10.riscv64.otherpkgs.pkglist | 2 + .../rh/service.rhels10.riscv64.pkglist | 30 +++++++++ .../rh/service.rhels10.riscv64.postinstall | 62 +++++++++++++++++++ .../rocky/compute.rocky10.riscv64.exlist | 33 ++++++++++ .../rocky/compute.rocky10.riscv64.pkglist | 30 +++++++++ .../rocky/compute.rocky10.riscv64.postinstall | 62 +++++++++++++++++++ .../rocky/service.rocky10.riscv64.exlist | 33 ++++++++++ .../service.rocky10.riscv64.otherpkgs.pkglist | 2 + .../rocky/service.rocky10.riscv64.pkglist | 30 +++++++++ .../rocky/service.rocky10.riscv64.postinstall | 62 +++++++++++++++++++ 16 files changed, 508 insertions(+) create mode 100644 xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.otherpkgs.pkglist create mode 100644 xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.otherpkgs.pkglist create mode 100644 xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.exlist create mode 100644 xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.pkglist create mode 100755 xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.postinstall create mode 100644 xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.exlist create mode 100644 xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.otherpkgs.pkglist create mode 100644 xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.pkglist create mode 100755 xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.postinstall create mode 100644 xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.exlist create mode 100644 xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.pkglist create mode 100755 xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.postinstall create mode 100644 xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.exlist create mode 100644 xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.otherpkgs.pkglist create mode 100644 xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.pkglist create mode 100755 xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.postinstall diff --git a/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.otherpkgs.pkglist b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.otherpkgs.pkglist new file mode 100644 index 000000000..cfcb8349f --- /dev/null +++ b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.otherpkgs.pkglist @@ -0,0 +1,2 @@ +xcat/xcat-core/xCATsn +xcat/xcat-dep/rh10/riscv64/goconserver diff --git a/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.otherpkgs.pkglist b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.otherpkgs.pkglist new file mode 100644 index 000000000..cfcb8349f --- /dev/null +++ b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.otherpkgs.pkglist @@ -0,0 +1,2 @@ +xcat/xcat-core/xCATsn +xcat/xcat-dep/rh10/riscv64/goconserver diff --git a/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.exlist b/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.exlist new file mode 100644 index 000000000..07436e39f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.exlist @@ -0,0 +1,33 @@ +./boot* +./usr/include* +./usr/lib/locale* +./usr/lib64/perl5/Encode/CN* +./usr/lib64/perl5/Encode/JP* +./usr/lib64/perl5/Encode/TW* +./usr/lib64/perl5/Encode/KR* +./lib/kbd/keymaps/i386* +./lib/kbd/keymaps/mac* +./lib/kbd/keymaps/include* +./usr/local/include* +./usr/local/share/man* +./usr/share/man* +./usr/share/cracklib* +./usr/share/doc* +./usr/share/gnome* +./usr/share/i18n* ++./usr/share/i18n/en_US* +./usr/share/info* +./usr/share/locale/* ++./usr/share/locale/en_US* ++./usr/share/locale/C* ++./usr/share/locale/locale.alias ++./usr/lib/locale/locale-archive ++./usr/lib/locale/en* ++./usr/lib/locale/C* +./usr/share/omf* +./usr/share/vim/site/doc* +./usr/share/vim/vim74/doc* +./usr/share/zoneinfo* +./var/cache/man* +./var/lib/yum* +./tmp* diff --git a/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.pkglist b/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.pkglist new file mode 100644 index 000000000..ccc8bfd79 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.pkglist @@ -0,0 +1,30 @@ +@minimal-environment +initscripts +chrony +kernel +net-tools +nfs-utils +openssh-server +rsync +tar +util-linux +wget +python3 +tar +bzip2 +bc +dracut +dracut-network +NetworkManager +rsyslog +hostname +e2fsprogs +ethtool +parted +openssl +openssh-clients +bash +vim-minimal +rpm +iputils +perl-interpreter diff --git a/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.postinstall b/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.postinstall new file mode 100755 index 000000000..015cfec0f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/compute.rhels10.riscv64.postinstall @@ -0,0 +1,62 @@ +#!/bin/sh +#-- Do not remove following line if you want to make use of CVS version tracking +#-- $Id: compute.postinstall,v 1.21 2008/09/04 12:05:45 sikorsky Exp $ +#-- jurij.sikorsky@t-systems.cz +#-- +#-- this script is run after all packages from $profile.pkglist are installed +#-- +#-- it gets these arguments: +#-- +#-- $1 = install root (chroot directory for profile) +#-- $2 = OS version +#-- $3 = architecture +#-- $4 = profile name +#-- $5 = work dir (where genimage is located) +#-- +#-- +installroot=$1 +osver=$2 +arch=$3 +profile=$4 +workdir=$5 + +#-- Example how /etc/fstab can be automatically generated during image generation: +cat <$installroot/etc/fstab +proc /proc proc rw 0 0 +sysfs /sys sysfs rw 0 0 +devpts /dev/pts devpts rw,gid=5,mode=620 0 0 +END + +#-- Uncomment the line contains "cons" in /etc/inittab +#cons:12345:respawn:/sbin/smart_agetty -L 38400 console +#echo "co:2345:respawn:/sbin/agetty -L 38400 console" >> $installroot/etc/inittab + + +#-- Disable SELinux in the rootimg +#-- Redhat 7.3 will install selinux-policy and selinux is enabled by default +#-- Need to disable selinux, otherwise, the booting will hang on "Loading selinux policy" +if [ -f "$installroot/etc/selinux/config" ] +then + sed -i 's/SELINUX=enforcing\|permissive/SELINUX=disabled/' $installroot/etc/selinux/config +fi +#--for redhat 8 and 8.1 +#-- Need to disable firewalld, otherwise, the remoteshell script will not able to get all the SSH keys +FIREWALLD="$installroot/etc/systemd/system/multi-user.target.wants/firewalld.service" +if [[ -f "$FIREWALLD" || -L "$FIREWALLD" ]] +then + rm -f $FIREWALLD +fi +FIREWALLD1="$installroot/etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service" +if [[ -f "$FIREWALLD1" || -L "$FIREWALLD1" ]] +then + rm -f $FIREWALLD1 +fi + + +#-- Example of booted image versioning +#-- We want to know, with what configuration (version of the image) each node was booted. +#-- Hence, we keep image definition files and postscripts in CVS. During image generation we create file /etc/IMGVERSION and fill it with CVS "$Id$" of files with image definition (.pkglist, .exlist, .repolist, .postinstall). Then, during boot, each "CVS enabled" postscript (see /install/postscripts/cvs_template.sh and /install/postscripts/cvs_template.pl) adds one line to /etc/IMGVERSION. Then you can determine in any time what image you are running and what postscipts in which versions were run. +#cat /dev/null > $installroot/etc/IMGVERSION +#for ext in pkglist exlist postinstall repolist; do +# [ -r $workdir/$profile.$ext ] && cat $workdir/$profile.$ext | grep -E '^[[:space:]]*#.*[[:space:]]\$Id' >> $installroot/etc/IMGVERSION +#done diff --git a/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.exlist b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.exlist new file mode 100644 index 000000000..07436e39f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.exlist @@ -0,0 +1,33 @@ +./boot* +./usr/include* +./usr/lib/locale* +./usr/lib64/perl5/Encode/CN* +./usr/lib64/perl5/Encode/JP* +./usr/lib64/perl5/Encode/TW* +./usr/lib64/perl5/Encode/KR* +./lib/kbd/keymaps/i386* +./lib/kbd/keymaps/mac* +./lib/kbd/keymaps/include* +./usr/local/include* +./usr/local/share/man* +./usr/share/man* +./usr/share/cracklib* +./usr/share/doc* +./usr/share/gnome* +./usr/share/i18n* ++./usr/share/i18n/en_US* +./usr/share/info* +./usr/share/locale/* ++./usr/share/locale/en_US* ++./usr/share/locale/C* ++./usr/share/locale/locale.alias ++./usr/lib/locale/locale-archive ++./usr/lib/locale/en* ++./usr/lib/locale/C* +./usr/share/omf* +./usr/share/vim/site/doc* +./usr/share/vim/vim74/doc* +./usr/share/zoneinfo* +./var/cache/man* +./var/lib/yum* +./tmp* diff --git a/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.otherpkgs.pkglist b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.otherpkgs.pkglist new file mode 100644 index 000000000..cfcb8349f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.otherpkgs.pkglist @@ -0,0 +1,2 @@ +xcat/xcat-core/xCATsn +xcat/xcat-dep/rh10/riscv64/goconserver diff --git a/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.pkglist b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.pkglist new file mode 100644 index 000000000..ac608b148 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.pkglist @@ -0,0 +1,30 @@ +@minimal-environment +chrony +kernel +net-tools +nfs-utils +openssh-server +rsync +tar +util-linux +wget +perl-DBD-MySQL +perl-DBD-Pg +perl-lib +python3 +tar +bzip2 +bc +dracut +dracut-network +rsyslog +hostname +e2fsprogs +ethtool +parted +openssl +openssh-clients +bash +vim-minimal +rpm +iputils diff --git a/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.postinstall b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.postinstall new file mode 100755 index 000000000..015cfec0f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rh/service.rhels10.riscv64.postinstall @@ -0,0 +1,62 @@ +#!/bin/sh +#-- Do not remove following line if you want to make use of CVS version tracking +#-- $Id: compute.postinstall,v 1.21 2008/09/04 12:05:45 sikorsky Exp $ +#-- jurij.sikorsky@t-systems.cz +#-- +#-- this script is run after all packages from $profile.pkglist are installed +#-- +#-- it gets these arguments: +#-- +#-- $1 = install root (chroot directory for profile) +#-- $2 = OS version +#-- $3 = architecture +#-- $4 = profile name +#-- $5 = work dir (where genimage is located) +#-- +#-- +installroot=$1 +osver=$2 +arch=$3 +profile=$4 +workdir=$5 + +#-- Example how /etc/fstab can be automatically generated during image generation: +cat <$installroot/etc/fstab +proc /proc proc rw 0 0 +sysfs /sys sysfs rw 0 0 +devpts /dev/pts devpts rw,gid=5,mode=620 0 0 +END + +#-- Uncomment the line contains "cons" in /etc/inittab +#cons:12345:respawn:/sbin/smart_agetty -L 38400 console +#echo "co:2345:respawn:/sbin/agetty -L 38400 console" >> $installroot/etc/inittab + + +#-- Disable SELinux in the rootimg +#-- Redhat 7.3 will install selinux-policy and selinux is enabled by default +#-- Need to disable selinux, otherwise, the booting will hang on "Loading selinux policy" +if [ -f "$installroot/etc/selinux/config" ] +then + sed -i 's/SELINUX=enforcing\|permissive/SELINUX=disabled/' $installroot/etc/selinux/config +fi +#--for redhat 8 and 8.1 +#-- Need to disable firewalld, otherwise, the remoteshell script will not able to get all the SSH keys +FIREWALLD="$installroot/etc/systemd/system/multi-user.target.wants/firewalld.service" +if [[ -f "$FIREWALLD" || -L "$FIREWALLD" ]] +then + rm -f $FIREWALLD +fi +FIREWALLD1="$installroot/etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service" +if [[ -f "$FIREWALLD1" || -L "$FIREWALLD1" ]] +then + rm -f $FIREWALLD1 +fi + + +#-- Example of booted image versioning +#-- We want to know, with what configuration (version of the image) each node was booted. +#-- Hence, we keep image definition files and postscripts in CVS. During image generation we create file /etc/IMGVERSION and fill it with CVS "$Id$" of files with image definition (.pkglist, .exlist, .repolist, .postinstall). Then, during boot, each "CVS enabled" postscript (see /install/postscripts/cvs_template.sh and /install/postscripts/cvs_template.pl) adds one line to /etc/IMGVERSION. Then you can determine in any time what image you are running and what postscipts in which versions were run. +#cat /dev/null > $installroot/etc/IMGVERSION +#for ext in pkglist exlist postinstall repolist; do +# [ -r $workdir/$profile.$ext ] && cat $workdir/$profile.$ext | grep -E '^[[:space:]]*#.*[[:space:]]\$Id' >> $installroot/etc/IMGVERSION +#done diff --git a/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.exlist b/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.exlist new file mode 100644 index 000000000..07436e39f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.exlist @@ -0,0 +1,33 @@ +./boot* +./usr/include* +./usr/lib/locale* +./usr/lib64/perl5/Encode/CN* +./usr/lib64/perl5/Encode/JP* +./usr/lib64/perl5/Encode/TW* +./usr/lib64/perl5/Encode/KR* +./lib/kbd/keymaps/i386* +./lib/kbd/keymaps/mac* +./lib/kbd/keymaps/include* +./usr/local/include* +./usr/local/share/man* +./usr/share/man* +./usr/share/cracklib* +./usr/share/doc* +./usr/share/gnome* +./usr/share/i18n* ++./usr/share/i18n/en_US* +./usr/share/info* +./usr/share/locale/* ++./usr/share/locale/en_US* ++./usr/share/locale/C* ++./usr/share/locale/locale.alias ++./usr/lib/locale/locale-archive ++./usr/lib/locale/en* ++./usr/lib/locale/C* +./usr/share/omf* +./usr/share/vim/site/doc* +./usr/share/vim/vim74/doc* +./usr/share/zoneinfo* +./var/cache/man* +./var/lib/yum* +./tmp* diff --git a/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.pkglist b/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.pkglist new file mode 100644 index 000000000..ccc8bfd79 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.pkglist @@ -0,0 +1,30 @@ +@minimal-environment +initscripts +chrony +kernel +net-tools +nfs-utils +openssh-server +rsync +tar +util-linux +wget +python3 +tar +bzip2 +bc +dracut +dracut-network +NetworkManager +rsyslog +hostname +e2fsprogs +ethtool +parted +openssl +openssh-clients +bash +vim-minimal +rpm +iputils +perl-interpreter diff --git a/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.postinstall b/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.postinstall new file mode 100755 index 000000000..015cfec0f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/compute.rocky10.riscv64.postinstall @@ -0,0 +1,62 @@ +#!/bin/sh +#-- Do not remove following line if you want to make use of CVS version tracking +#-- $Id: compute.postinstall,v 1.21 2008/09/04 12:05:45 sikorsky Exp $ +#-- jurij.sikorsky@t-systems.cz +#-- +#-- this script is run after all packages from $profile.pkglist are installed +#-- +#-- it gets these arguments: +#-- +#-- $1 = install root (chroot directory for profile) +#-- $2 = OS version +#-- $3 = architecture +#-- $4 = profile name +#-- $5 = work dir (where genimage is located) +#-- +#-- +installroot=$1 +osver=$2 +arch=$3 +profile=$4 +workdir=$5 + +#-- Example how /etc/fstab can be automatically generated during image generation: +cat <$installroot/etc/fstab +proc /proc proc rw 0 0 +sysfs /sys sysfs rw 0 0 +devpts /dev/pts devpts rw,gid=5,mode=620 0 0 +END + +#-- Uncomment the line contains "cons" in /etc/inittab +#cons:12345:respawn:/sbin/smart_agetty -L 38400 console +#echo "co:2345:respawn:/sbin/agetty -L 38400 console" >> $installroot/etc/inittab + + +#-- Disable SELinux in the rootimg +#-- Redhat 7.3 will install selinux-policy and selinux is enabled by default +#-- Need to disable selinux, otherwise, the booting will hang on "Loading selinux policy" +if [ -f "$installroot/etc/selinux/config" ] +then + sed -i 's/SELINUX=enforcing\|permissive/SELINUX=disabled/' $installroot/etc/selinux/config +fi +#--for redhat 8 and 8.1 +#-- Need to disable firewalld, otherwise, the remoteshell script will not able to get all the SSH keys +FIREWALLD="$installroot/etc/systemd/system/multi-user.target.wants/firewalld.service" +if [[ -f "$FIREWALLD" || -L "$FIREWALLD" ]] +then + rm -f $FIREWALLD +fi +FIREWALLD1="$installroot/etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service" +if [[ -f "$FIREWALLD1" || -L "$FIREWALLD1" ]] +then + rm -f $FIREWALLD1 +fi + + +#-- Example of booted image versioning +#-- We want to know, with what configuration (version of the image) each node was booted. +#-- Hence, we keep image definition files and postscripts in CVS. During image generation we create file /etc/IMGVERSION and fill it with CVS "$Id$" of files with image definition (.pkglist, .exlist, .repolist, .postinstall). Then, during boot, each "CVS enabled" postscript (see /install/postscripts/cvs_template.sh and /install/postscripts/cvs_template.pl) adds one line to /etc/IMGVERSION. Then you can determine in any time what image you are running and what postscipts in which versions were run. +#cat /dev/null > $installroot/etc/IMGVERSION +#for ext in pkglist exlist postinstall repolist; do +# [ -r $workdir/$profile.$ext ] && cat $workdir/$profile.$ext | grep -E '^[[:space:]]*#.*[[:space:]]\$Id' >> $installroot/etc/IMGVERSION +#done diff --git a/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.exlist b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.exlist new file mode 100644 index 000000000..07436e39f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.exlist @@ -0,0 +1,33 @@ +./boot* +./usr/include* +./usr/lib/locale* +./usr/lib64/perl5/Encode/CN* +./usr/lib64/perl5/Encode/JP* +./usr/lib64/perl5/Encode/TW* +./usr/lib64/perl5/Encode/KR* +./lib/kbd/keymaps/i386* +./lib/kbd/keymaps/mac* +./lib/kbd/keymaps/include* +./usr/local/include* +./usr/local/share/man* +./usr/share/man* +./usr/share/cracklib* +./usr/share/doc* +./usr/share/gnome* +./usr/share/i18n* ++./usr/share/i18n/en_US* +./usr/share/info* +./usr/share/locale/* ++./usr/share/locale/en_US* ++./usr/share/locale/C* ++./usr/share/locale/locale.alias ++./usr/lib/locale/locale-archive ++./usr/lib/locale/en* ++./usr/lib/locale/C* +./usr/share/omf* +./usr/share/vim/site/doc* +./usr/share/vim/vim74/doc* +./usr/share/zoneinfo* +./var/cache/man* +./var/lib/yum* +./tmp* diff --git a/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.otherpkgs.pkglist b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.otherpkgs.pkglist new file mode 100644 index 000000000..cfcb8349f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.otherpkgs.pkglist @@ -0,0 +1,2 @@ +xcat/xcat-core/xCATsn +xcat/xcat-dep/rh10/riscv64/goconserver diff --git a/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.pkglist b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.pkglist new file mode 100644 index 000000000..ac608b148 --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.pkglist @@ -0,0 +1,30 @@ +@minimal-environment +chrony +kernel +net-tools +nfs-utils +openssh-server +rsync +tar +util-linux +wget +perl-DBD-MySQL +perl-DBD-Pg +perl-lib +python3 +tar +bzip2 +bc +dracut +dracut-network +rsyslog +hostname +e2fsprogs +ethtool +parted +openssl +openssh-clients +bash +vim-minimal +rpm +iputils diff --git a/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.postinstall b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.postinstall new file mode 100755 index 000000000..015cfec0f --- /dev/null +++ b/xCAT-server/share/xcat/netboot/rocky/service.rocky10.riscv64.postinstall @@ -0,0 +1,62 @@ +#!/bin/sh +#-- Do not remove following line if you want to make use of CVS version tracking +#-- $Id: compute.postinstall,v 1.21 2008/09/04 12:05:45 sikorsky Exp $ +#-- jurij.sikorsky@t-systems.cz +#-- +#-- this script is run after all packages from $profile.pkglist are installed +#-- +#-- it gets these arguments: +#-- +#-- $1 = install root (chroot directory for profile) +#-- $2 = OS version +#-- $3 = architecture +#-- $4 = profile name +#-- $5 = work dir (where genimage is located) +#-- +#-- +installroot=$1 +osver=$2 +arch=$3 +profile=$4 +workdir=$5 + +#-- Example how /etc/fstab can be automatically generated during image generation: +cat <$installroot/etc/fstab +proc /proc proc rw 0 0 +sysfs /sys sysfs rw 0 0 +devpts /dev/pts devpts rw,gid=5,mode=620 0 0 +END + +#-- Uncomment the line contains "cons" in /etc/inittab +#cons:12345:respawn:/sbin/smart_agetty -L 38400 console +#echo "co:2345:respawn:/sbin/agetty -L 38400 console" >> $installroot/etc/inittab + + +#-- Disable SELinux in the rootimg +#-- Redhat 7.3 will install selinux-policy and selinux is enabled by default +#-- Need to disable selinux, otherwise, the booting will hang on "Loading selinux policy" +if [ -f "$installroot/etc/selinux/config" ] +then + sed -i 's/SELINUX=enforcing\|permissive/SELINUX=disabled/' $installroot/etc/selinux/config +fi +#--for redhat 8 and 8.1 +#-- Need to disable firewalld, otherwise, the remoteshell script will not able to get all the SSH keys +FIREWALLD="$installroot/etc/systemd/system/multi-user.target.wants/firewalld.service" +if [[ -f "$FIREWALLD" || -L "$FIREWALLD" ]] +then + rm -f $FIREWALLD +fi +FIREWALLD1="$installroot/etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service" +if [[ -f "$FIREWALLD1" || -L "$FIREWALLD1" ]] +then + rm -f $FIREWALLD1 +fi + + +#-- Example of booted image versioning +#-- We want to know, with what configuration (version of the image) each node was booted. +#-- Hence, we keep image definition files and postscripts in CVS. During image generation we create file /etc/IMGVERSION and fill it with CVS "$Id$" of files with image definition (.pkglist, .exlist, .repolist, .postinstall). Then, during boot, each "CVS enabled" postscript (see /install/postscripts/cvs_template.sh and /install/postscripts/cvs_template.pl) adds one line to /etc/IMGVERSION. Then you can determine in any time what image you are running and what postscipts in which versions were run. +#cat /dev/null > $installroot/etc/IMGVERSION +#for ext in pkglist exlist postinstall repolist; do +# [ -r $workdir/$profile.$ext ] && cat $workdir/$profile.$ext | grep -E '^[[:space:]]*#.*[[:space:]]\$Id' >> $installroot/etc/IMGVERSION +#done From e7168fc1373db724b0048b93c43a320dd8a60cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:45:21 -0300 Subject: [PATCH 12/54] test(xCAT-test): cover the EL10 riscv64 osimage profiles Resolve the rocky10/rhels10 point releases through imgutils::get_profile_def_filename with the OS search list emulated, and assert the riscv64 files exist, win the lookup, match the x86_64 package lists and point service images at the riscv64 dependency repository. --- xCAT-test/unit/netboot_profile_riscv64.t | 109 +++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 xCAT-test/unit/netboot_profile_riscv64.t diff --git a/xCAT-test/unit/netboot_profile_riscv64.t b/xCAT-test/unit/netboot_profile_riscv64.t new file mode 100644 index 000000000..56a8f23c2 --- /dev/null +++ b/xCAT-test/unit/netboot_profile_riscv64.t @@ -0,0 +1,109 @@ +#!/usr/bin/env perl +use strict; +use warnings; +## no critic (Modules::RequireFilenameMatchesPackage) + +use FindBin; +use File::Spec; +use Cwd qw(realpath); +use Test::More; + +# EL10 riscv64 diskless and service profiles are plain data files resolved by +# imgutils::get_profile_def_filename (osver.arch first, then osbase.arch, then +# the arch-less fallbacks). Pin that the riscv64 files exist, win the lookup +# for rocky10/rhels10 point releases, and carry the right content. + +BEGIN { + # imgutils pulls in xCAT::SvrUtils only for the OS search list; emulate + # the point-release walk (rocky10.2 -> rocky10.2, rocky10.1, ..., rocky10, + # rocky) without the database-backed module. + package xCAT::SvrUtils; + sub get_os_search_list { + my ($os) = @_; + my @word = split( /\./, $os ); + my @list; + while ( @word && $word[-1] =~ /^[0-9]+$/ ) { + my $last = pop @word; + while ( $last >= 0 ) { + push @list, join( '.', @word, $last ); + $last--; + } + } + push @list, join( '.', @word ); + return @list; + } + $INC{'xCAT/SvrUtils.pm'} = __FILE__; +} + +my $repo_root = realpath( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $share = File::Spec->catdir( $repo_root, 'xCAT-server', 'share', 'xcat' ); +my $imgutils = File::Spec->catfile( $share, 'netboot', 'imgutils', 'imgutils.pm' ); +plan skip_all => "$imgutils not found" unless -r $imgutils; +require $imgutils; + +sub slurp { + my ($path) = @_; + open( my $fh, '<', $path ) or die "Unable to read $path: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +my @families = ( + [ 'rocky', 'rocky10', 'rocky10.2' ], + [ 'rh', 'rhels10', 'rhels10.2' ], +); + +for my $family (@families) { + my ( $dir, $osbase, $osver ) = @$family; + my $base = File::Spec->catdir( $share, 'netboot', $dir ); + + for my $profile (qw(compute service)) { + for my $ext (qw(pkglist exlist postinstall)) { + my $expected = File::Spec->catfile( $base, "$profile.$osbase.riscv64.$ext" ); + ok( -r $expected, "$dir/$profile.$osbase.riscv64.$ext exists" ); + is( + imgutils::get_profile_def_filename( $osver, $profile, 'riscv64', $base, $ext ), + $expected, + "$osver riscv64 $profile $ext resolves to the riscv64 file", + ); + } + my $x86 = slurp( File::Spec->catfile( $base, "$profile.$osbase.x86_64.pkglist" ) ); + my $rv = slurp( File::Spec->catfile( $base, "$profile.$osbase.riscv64.pkglist" ) ); + s/\s+\z/\n/ for ( $x86, $rv ); + is( $rv, $x86, "$dir/$profile.$osbase riscv64 pkglist matches the x86_64 list (no arch-specific packages)" ); + unlike( $rv, qr/^(?:microcode_ctl|grub2-efi-x64|shim-x64|syslinux|xnba)/m, "$dir/$profile.$osbase riscv64 pkglist has no x86-only packages" ); + + my $exlist = slurp( File::Spec->catfile( $base, "$profile.$osbase.riscv64.exlist" ) ); + like( $exlist, qr{^\./lib/kbd/keymaps/include\*$}m, "$dir/$profile.$osbase riscv64 exlist excludes the kbd keymap includes" ); + unlike( $exlist, qr{^\./lib/kdb/}m, "$dir/$profile.$osbase riscv64 exlist has no kdb typo" ); + is( scalar( () = $exlist =~ m{^\./usr/share/man\*$}mg ), 1, "$dir/$profile.$osbase riscv64 exlist lists usr/share/man once" ); + + my $postinstall = slurp( File::Spec->catfile( $base, "$profile.$osbase.riscv64.postinstall" ) ); + like( $postinstall, qr/^#!\/bin\/sh/, "$dir/$profile.$osbase riscv64 postinstall is a shell script" ); + like( $postinstall, qr/SELINUX=disabled/, "$dir/$profile.$osbase riscv64 postinstall disables SELinux in the image" ); + } + + my $otherpkgs = File::Spec->catfile( $base, "service.$osbase.riscv64.otherpkgs.pkglist" ); + is( + imgutils::get_profile_def_filename( $osver, 'service', 'riscv64', $base, 'otherpkgs.pkglist' ), + $otherpkgs, + "$osver riscv64 service otherpkgs resolves to the riscv64 file", + ); + like( slurp($otherpkgs), qr{^xcat/xcat-dep/rh10/riscv64/goconserver$}m, "$dir netboot service otherpkgs pulls goconserver from the riscv64 EL10 dep repo" ); + + my $install_otherpkgs = File::Spec->catfile( $share, 'install', $dir, "service.$osbase.riscv64.otherpkgs.pkglist" ); + ok( -r $install_otherpkgs, "install/$dir/service.$osbase.riscv64.otherpkgs.pkglist exists" ); + like( slurp($install_otherpkgs), qr{^xcat/xcat-dep/rh10/riscv64/goconserver$}m, "$dir install service otherpkgs pulls goconserver from the riscv64 EL10 dep repo" ); + like( slurp($install_otherpkgs), qr{^xcat/xcat-core/xCATsn$}m, "$dir install service otherpkgs installs xCATsn" ); +} + +# an architecture without its own files still falls back to the arch-less ones +my $rocky_base = File::Spec->catdir( $share, 'netboot', 'rocky' ); +is( + imgutils::get_profile_def_filename( 'rocky10.2', 'compute', 'riscv32', $rocky_base, 'pkglist' ), + File::Spec->catfile( $rocky_base, 'compute.pkglist' ), + 'an unknown architecture falls back to the arch-less compute pkglist', +); + +done_testing(); From 6eba1277fe8e2c29baeaf85fd1adb24a8c7c530a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:51:22 -0300 Subject: [PATCH 13/54] docs(riscv64): document EL10 riscv64 compute node support Add the riscv64 page to the cluster management guide (UEFI + grub2 boot path, discovery through mknb's grub2 network configurations, stateful and stateless provisioning, the dependency picture for a management node on riscv64, limitations), list riscv64 in the node object attributes and in the support matrices, add the architecture to the cross-build page for stateless images, extend the grub2 install guide (and fix its swapped x86_64/aarch64 file names), the uninstall package lists, the DHCP backend validation matrix and the mknb/genimage man pages, and carry the riscv64 schema values into the generated nodetype, osimage, noderes, node and group references. --- .../building_stateless_images.rst | 11 +- .../advanced/mixed_cluster/support_matrix.rst | 75 ++++++------ docs/source/developers/guides/code/builds.rst | 6 + .../code/dhcp_backend_validation_matrix.rst | 16 +++ .../basic_concepts/xcat_object/node.rst | 6 +- .../admin-guides/manage_clusters/index.rst | 1 + .../manage_clusters/riscv64/index.rst | 108 ++++++++++++++++++ .../references/man1/genimage.1.rst | 2 +- .../references/man5/noderes.5.rst | 1 + .../references/man5/nodetype.5.rst | 2 +- .../references/man5/osimage.5.rst | 2 +- .../admin-guides/references/man7/group.7.rst | 3 +- .../admin-guides/references/man7/node.7.rst | 3 +- .../references/man7/osimage.7.rst | 2 +- .../admin-guides/references/man8/mknb.8.rst | 2 + .../guides/install-guides/yum/grub2.rst | 15 ++- docs/source/overview/differentiators.rst | 2 +- docs/source/overview/support_matrix.rst | 40 ++++--- xCAT-client/pods/man1/genimage.1.pod | 2 +- xCAT-client/pods/man8/mknb.8.pod | 2 + 20 files changed, 224 insertions(+), 77 deletions(-) create mode 100644 docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst diff --git a/docs/source/advanced/mixed_cluster/building_stateless_images.rst b/docs/source/advanced/mixed_cluster/building_stateless_images.rst index b08f855e0..c3a1ffa5a 100644 --- a/docs/source/advanced/mixed_cluster/building_stateless_images.rst +++ b/docs/source/advanced/mixed_cluster/building_stateless_images.rst @@ -9,15 +9,16 @@ In a homogeneous cluster, the management node is the same hardware architecture The issues arises in a heterogeneous cluster, where the management node is running a different level operating system *or* hardware architecture as the compute nodes in which to deploy the image. The ``genimage`` command that builds stateless images depends on various utilities provided by the base operating system and needs to be run on a node with the same hardware architecture and *major* Operating System release as the nodes that will be booted from the image. -When running xCAT >= 2.17 on EL >= 8 based management node with x86_64 architecture, qemu-user-static can be used to cross-build ppc64* and aarch64 osimages. Therefore, you don't need to build images on systems with the target architecture anymore. +When running xCAT >= 2.17 on EL >= 8 based management node with x86_64 architecture, qemu-user-static can be used to cross-build ppc64*, aarch64 and riscv64 osimages. Therefore, you don't need to build images on systems with the target architecture anymore. -Cross-build ppc64*/aarch64 stateless/statelite image on x86_64 management node ------------------------------------------------------------------------------- +Cross-build ppc64*/aarch64/riscv64 stateless/statelite image on x86_64 management node +-------------------------------------------------------------------------------------- -#. Download qemu-user-static binaries for ppc64le and/or aarch64: :: +#. Download qemu-user-static binaries for ppc64le, aarch64 and/or riscv64: :: wget https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-ppc64le-static -P /usr/bin wget https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-aarch64-static -P /usr/bin + wget https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-riscv64-static -P /usr/bin chmod 755 /usr/bin/qemu-*-static #. Configure systemd-binfmt accordingly: :: @@ -26,7 +27,7 @@ Cross-build ppc64*/aarch64 stateless/statelite image on x86_64 management node bash qemu-binfmt-conf.sh --systemd 'ALL' --qemu-path '/usr/bin' --qemu-suffix '-static' --persistent 'yes' systemctl restart systemd-binfmt.service -#. Make sure ``osimage.osarch`` of your image is set to your target architecture eg. ``ppc64le`` or ``aarch64``. +#. Make sure ``osimage.osarch`` of your image is set to your target architecture eg. ``ppc64le``, ``aarch64`` or ``riscv64``. Now, ``genimage`` will use systemd-binfmt and the qemu-user-static binary to build the image for the specified architecture. diff --git a/docs/source/advanced/mixed_cluster/support_matrix.rst b/docs/source/advanced/mixed_cluster/support_matrix.rst index 8fcbacfd0..8bf5e69e7 100644 --- a/docs/source/advanced/mixed_cluster/support_matrix.rst +++ b/docs/source/advanced/mixed_cluster/support_matrix.rst @@ -1,43 +1,43 @@ Support Matrix ============== -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| | RHEL | SLES | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | -| | ppc64 | ppc64 | x86_64 | x86_64 | x86_64 | ppc64le | ppc64le | ppc64el | aarch64 | aarch64 | aarch64 | -| | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | -+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+ -| RHEL | | | | | | | | | | | | -| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| SLES | | | | | | | | | | | | -| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| RHEL | | | | | | | | | | | | -| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | -| MN/SN | [4]_ | [4]_ | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| SLES | | | | | | | | | | | | -| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | -| MN/SN | [4]_ | [4]_ | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| Ubuntu | | | | | | | | | | | | -| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | -| MN/SN | [5]_ | [5]_ | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| RHEL | | | | | | | | | | | | -| ppc64le | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | [2]_ | [2]_ | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| SLES | | | | | | | | | | | | -| ppc64le | no | no | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | | | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ -| Ubuntu | | | | | | | | | | | | -| ppc64el | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | -| MN/SN | [3]_ | [3]_ | | | | | | | | | | -+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| | RHEL | SLES | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | SLES | Ubuntu | RHEL | +| | ppc64 | ppc64 | x86_64 | x86_64 | x86_64 | ppc64le | ppc64le | ppc64el | aarch64 | aarch64 | aarch64 | riscv64 | +| | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | CN | ++=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+=========+ +| RHEL | | | | | | | | | | | | | +| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | +| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| SLES | | | | | | | | | | | | | +| ppc64 | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | +| MN/SN | | | [1]_ | [1]_ | [1]_ | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| RHEL | | | | | | | | | | | | | +| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | yes | +| MN/SN | [4]_ | [4]_ | | | | | | | | | | [6]_ | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| SLES | | | | | | | | | | | | | +| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | +| MN/SN | [4]_ | [4]_ | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| Ubuntu | | | | | | | | | | | | | +| x86_64 | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | +| MN/SN | [5]_ | [5]_ | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| RHEL | | | | | | | | | | | | | +| ppc64le | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | +| MN/SN | [2]_ | [2]_ | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| SLES | | | | | | | | | | | | | +| ppc64le | no | no | yes | yes | yes | yes | yes | yes | no | no | no | no | +| MN/SN | | | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ +| Ubuntu | | | | | | | | | | | | | +| ppc64el | yes | yes | yes | yes | yes | yes | yes | yes | no | no | no | no | +| MN/SN | [3]_ | [3]_ | | | | | | | | | | | ++---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+ Notes: @@ -53,3 +53,4 @@ Notes: .. [3] If the compute nodes are DFM managed systems, will need xCAT 2.10 or high versions and the ppc64le DFM and ppc64le hardware server on the management node. .. [4] If the compute nodes are DFM managed systems, will need the ppc64le DFM and ppc64le hardware server on the management node. .. [5] Does not support DFM managed compute nodes, hardware control does not work. +.. [6] riscv64 compute nodes boot through UEFI firmware and grub2 only. The management node needs the riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) and ``/tftpboot/boot/grub2/grub2.riscv64`` (see :doc:`/guides/install-guides/yum/grub2`). EL10 compute nodes are supported, validated from an EL10 x86_64 management node; a riscv64 management node is documented in :doc:`/guides/admin-guides/manage_clusters/riscv64/index`. diff --git a/docs/source/developers/guides/code/builds.rst b/docs/source/developers/guides/code/builds.rst index 9f7f5cbe3..6e016881c 100644 --- a/docs/source/developers/guides/code/builds.rst +++ b/docs/source/developers/guides/code/builds.rst @@ -20,6 +20,12 @@ binary rpms for each architecture. ``rpmbuild`` does not need the packages named in ``BuildRequires`` to make a source rpm, so this also builds on a machine that cannot complete a full build. ``SRCONLY`` is not supported on AIX. +``buildcore.sh`` builds the architecture specific packages (``xCAT``, ``xCATsn``, +``xCAT-genesis-scripts``) for every supported architecture, riscv64 included, with +``rpmbuild --target``. The build host therefore needs an rpm that knows the riscv64 +architecture and the ``Recommends:`` tag (EL8 or later); an older host fails the riscv64 +builds and, as the script only publishes when every architecture built, produces no rpms. + xcat-deps --------- diff --git a/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst b/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst index 8bb769b15..5b3515d4d 100644 --- a/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst +++ b/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst @@ -226,6 +226,22 @@ paths. - package installation on ``ppc64el``; DHCP offer; boot file handoff; POWER boot-path correctness; Genesis shell when live validation is in scope + * - EL10 + - ``riscv64`` + - ``Kea`` + - ``UEFI grub2 handoff`` + - DHCP offer carries ``boot/grub2/grub2.riscv64`` for client + architecture 27 (``0x001b``); ``grub2.riscv64`` fetches + ``boot/grub2/grub.cfg-`` for discovery and the + per-node ``grub.cfg-`` after ``nodeset``; Genesis shell when + Genesis payload validation is in scope + * - EL10 + - ``riscv64`` + - ``ISC`` + - ``UEFI grub2 handoff`` + - same as the Kea row through the ISC subnet branch for client + architecture ``00:1b``; the riscv64 branch must answer before the + ``/yaboot`` fallback Current Lab Baseline -------------------- diff --git a/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst b/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst index 15f3d48d3..c72a78d42 100644 --- a/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst +++ b/docs/source/guides/admin-guides/basic_concepts/xcat_object/node.rst @@ -13,7 +13,7 @@ Key Attributes The operating system deployed on this node. Valid values: AIX, rhels*, rhelc*, rhas*, centos*, SL*, fedora*, sles* (where * is the version #) * arch: - The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64. + The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64. * groups: Usually, there are a set of nodes with some attributes in common, xCAT admin can define a node group containing these nodes, so that the management task can be issued against the group instead of individual nodes. A node can be a member of different groups, so the value of this attributes is a comma-delimited list of groups. At least one group is required to create a node. The new created group names should not be prefixed with "__" as this token has been preserved as the internal group name. @@ -44,7 +44,9 @@ Key Attributes | ppc64le PowerKVM Guest | ALL | grub2,grub2-http,grub2-tftp | +--------------------------+----------------------+-----------------------------------+ | aarch64 | >=el8 | grub2 | - +-------------------------------------------------+-----------------------------------+ + +--------------------------+----------------------+-----------------------------------+ + | riscv64 | >=el10 | grub2,grub2-http,grub2-tftp | + +--------------------------+----------------------+-----------------------------------+ * postscripts: Comma separated list of scripts, that should be run on this node after diskful installation or diskless boot, finish some system configuration and maintenance work. For installation of RedHat, CentOS, Fedora, the scripts will be run before the reboot. For installation of SLES, the scripts will be run after the reboot but before the init.d process. diff --git a/docs/source/guides/admin-guides/manage_clusters/index.rst b/docs/source/guides/admin-guides/manage_clusters/index.rst index d44e4a099..e9e8b0d11 100644 --- a/docs/source/guides/admin-guides/manage_clusters/index.rst +++ b/docs/source/guides/admin-guides/manage_clusters/index.rst @@ -11,3 +11,4 @@ The sections are organized based on hardware architecture. ppc64le/index.rst x86_64/index.rst + riscv64/index.rst diff --git a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst new file mode 100644 index 000000000..4279b4596 --- /dev/null +++ b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst @@ -0,0 +1,108 @@ +RISC-V 64-bit (riscv64) +======================= + +xCAT manages RISC-V 64-bit (``riscv64``) compute nodes running EL10. Rocky +Linux 10 is the reference distribution; the RHEL 10 RISC-V developer preview +uses the same media layout. The general cluster management documentation under +:doc:`/guides/admin-guides/manage_clusters/common/index` applies; this page +only covers what is specific to the architecture. + +What riscv64 nodes need +----------------------- + +* **UEFI firmware that can PXE boot.** riscv64 nodes boot through UEFI and + grub2 only: there is no BIOS/PXELINUX, petitboot or xNBA path. The firmware + sends DHCP option 93 (client system architecture) value 27 (``0x001b``) and + xCAT answers with the boot file ``boot/grub2/grub2.riscv64``, from both the + Kea and the ISC DHCP backends. +* ``noderes.netboot`` is one of ``grub2``, ``grub2-tftp`` or ``grub2-http``. + ``grub2-http`` is recommended for installers, whose initrd is large. +* ``nodetype.arch`` and ``osimage.osarch`` are ``riscv64``. No alias is + needed: ``uname -m``, rpm and dpkg all use the same token. +* ``/tftpboot/boot/grub2/grub2.riscv64``: the EL grub2 UEFI image for riscv64. + It is provided by the ``grub2-xcat`` package where available; otherwise copy + ``EFI/BOOT/grubriscv64.efi`` from the EL10 riscv64 BaseOS media as described + in :doc:`/guides/install-guides/yum/grub2`. +* The riscv64 Genesis image (``xCAT-genesis-base-riscv64`` and + ``xCAT-genesis-scripts-riscv64``) for discovery, BMC setup and flashing. Its + kernel must be loadable by grub2, that is, built with the EFI stub. + +The management node itself can be x86_64 or ppc64le; riscv64 is managed like +any other mixed-architecture cluster (see +:doc:`/advanced/mixed_cluster/support_matrix`). + +Node discovery +-------------- + +``mknb riscv64`` publishes the Genesis kernel and initramfs as +``/tftpboot/xcat/genesis.kernel.riscv64`` and +``/tftpboot/xcat/genesis.fs.riscv64.gz`` and writes one grub2 configuration per +network, ``/tftpboot/boot/grub2/grub.cfg-``. A net booted +``grub2.riscv64`` looks for ``grub.cfg-01-``, then ``grub.cfg-<8 hex digit +ip>``, then shorter prefixes of that ip; the per-node files written by +``nodeset`` therefore take priority and the network file is only used by nodes +that have no configuration yet, which is exactly the discovery case. +``mknb`` runs automatically when the riscv64 Genesis packages are installed or +updated; run ``mknb riscv64`` yourself after changing ``site.master``, +``site.dhcpinterfaces`` or the serial console settings. + +Discovered riscv64 nodes get ``nodetype.arch=riscv64`` and, when no compatible +method is set, ``noderes.netboot=grub2``. Use the discovery procedures documented +for the other architectures. + +Stateful (diskful) installation +-------------------------------- + +Import the Rocky Linux 10 riscv64 DVD with ``copycds``; it creates the +``rocky10.x-riscv64-install-compute`` osimage. The installer kernel and initrd +come from ``images/pxeboot`` on the media, like x86_64 and aarch64. The default +kickstart templates and package lists are shared with the other architectures; +``service.rocky10.riscv64.otherpkgs.pkglist`` pulls the service node packages +from the ``rh10/riscv64`` xcat-dep repository. + +The installer creates the UEFI boot entry for the installed system; after +``nodeset boot`` the firmware falls through to it because the per-node +``grub2-`` loader link is removed. + +Stateless (diskless) images +--------------------------- + +``genimage`` builds riscv64 images with the ``compute.rocky10.riscv64.*`` and +``compute.rhels10.riscv64.*`` profiles (package list, exclude list and +postinstall). On an x86_64 management node this needs ``qemu-user-static`` +registered for riscv64 through ``systemd-binfmt``; see +:doc:`/advanced/mixed_cluster/building_stateless_images`. EL10 has no +``qemu-user-static`` package of its own, so take the static riscv64 emulator +from a distribution that ships one. The default network driver list for +riscv64 images covers virtio, Intel, Realtek, Broadcom and Mellanox adapters; +add others through ``linuximage.netdrivers``. + +Management node on riscv64 +-------------------------- + +The EL10 riscv64 BaseOS, AppStream and CRB repositories provide every +dependency that xCAT takes from the distribution on x86_64, including ``kea``. +EPEL has no riscv64 build, so the packages xCAT otherwise takes from EPEL must +come from the riscv64 xcat-dep repository together with the usual xcat-dep +packages: + +* from EPEL on x86_64: ``perl-Digest-SHA1``, ``perl-Net-DNS``, + ``perl-Crypt-CBC``, ``perl-Crypt-Rijndael``, ``perl-DB_File``; optional + features also use ``perl-Expect``, ``perl-HTML-Form``, ``perl-Sys-Virt``, + ``perl-Mail-Sender``, ``perl-SOAP-Lite``, ``perl-Crypt-Blowfish``, + ``perl-Net-IP`` and ``conserver``; +* always from xcat-dep: ``perl-Net-Telnet``, ``perl-IO-Stty``, + ``perl-Net-HTTPS-NB``, ``perl-HTTP-Async``, ``perl-Crypt-SSLeay``, + ``goconserver``, ``ipmitool-xcat``, ``conserver-xcat`` and ``grub2-xcat``. + +xCAT does not install anything from CPAN; every dependency is an rpm. + +Limitations +----------- + +* UEFI HTTP boot (client architecture 28, ``0x001c``) is not configured yet; + use PXE (TFTP) to load ``grub2.riscv64`` and ``grub2-http`` for the payload. +* Ubuntu riscv64 is not supported yet. +* The serial console defaults to ``ttyS``; boards whose + firmware exposes the console on another device need + ``linuximage.addkcmdline`` or the serial settings adjusted. diff --git a/docs/source/guides/admin-guides/references/man1/genimage.1.rst b/docs/source/guides/admin-guides/references/man1/genimage.1.rst index 1d13e6ede..31022bf89 100644 --- a/docs/source/guides/admin-guides/references/man1/genimage.1.rst +++ b/docs/source/guides/admin-guides/references/man1/genimage.1.rst @@ -72,7 +72,7 @@ OPTIONS \ **-a**\ \ *arch*\ - The hardware architecture of this node: ppc64le, x86_64, ppc64, x86, ia64, aarch64 etc. If omitted, the current hardware architecture will be used. + The hardware architecture of this node: ppc64le, x86_64, ppc64, x86, ia64, aarch64, riscv64 etc. If omitted, the current hardware architecture will be used. diff --git a/docs/source/guides/admin-guides/references/man5/noderes.5.rst b/docs/source/guides/admin-guides/references/man5/noderes.5.rst index 67bb5fde7..733a5afab 100644 --- a/docs/source/guides/admin-guides/references/man5/noderes.5.rst +++ b/docs/source/guides/admin-guides/references/man5/noderes.5.rst @@ -62,6 +62,7 @@ noderes Attributes: ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 + riscv64 >=el10 grub2,grub2-http,grub2-tftp diff --git a/docs/source/guides/admin-guides/references/man5/nodetype.5.rst b/docs/source/guides/admin-guides/references/man5/nodetype.5.rst index 72e6d6f4b..bf10c8f87 100644 --- a/docs/source/guides/admin-guides/references/man5/nodetype.5.rst +++ b/docs/source/guides/admin-guides/references/man5/nodetype.5.rst @@ -50,7 +50,7 @@ nodetype Attributes: \ **arch**\ - The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64. + The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64. diff --git a/docs/source/guides/admin-guides/references/man5/osimage.5.rst b/docs/source/guides/admin-guides/references/man5/osimage.5.rst index 5ec6a150e..4586a84f2 100644 --- a/docs/source/guides/admin-guides/references/man5/osimage.5.rst +++ b/docs/source/guides/admin-guides/references/man5/osimage.5.rst @@ -115,7 +115,7 @@ osimage Attributes: \ **osarch**\ - The hardware architecture of this node. For netboot/statelite images, QEMU emulation for non-native architectures is used if qemu-user-static is installed and configured via systemd-binfmt. Valid values: x86_64, ppc64, x86, ia64, aarch64. + The hardware architecture of this node. For netboot/statelite images, QEMU emulation for non-native architectures is used if qemu-user-static is installed and configured via systemd-binfmt. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64. diff --git a/docs/source/guides/admin-guides/references/man7/group.7.rst b/docs/source/guides/admin-guides/references/man7/group.7.rst index 655dbfc8c..477bda943 100644 --- a/docs/source/guides/admin-guides/references/man7/group.7.rst +++ b/docs/source/guides/admin-guides/references/man7/group.7.rst @@ -47,7 +47,7 @@ group Attributes: \ **arch**\ (nodetype.arch) - The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64. + The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64. @@ -547,6 +547,7 @@ group Attributes: ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 + riscv64 >=el10 grub2,grub2-http,grub2-tftp diff --git a/docs/source/guides/admin-guides/references/man7/node.7.rst b/docs/source/guides/admin-guides/references/man7/node.7.rst index 14ca6699f..6c290d1d7 100644 --- a/docs/source/guides/admin-guides/references/man7/node.7.rst +++ b/docs/source/guides/admin-guides/references/man7/node.7.rst @@ -59,7 +59,7 @@ node Attributes: \ **arch**\ (nodetype.arch) - The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64. + The hardware architecture of this node. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64. @@ -547,6 +547,7 @@ node Attributes: ppc64le NonVirtualize ALL petitboot ppc64le PowerKVM Guest ALL grub2,grub2-http,grub2-tftp aarch64 >=el8 grub2 + riscv64 >=el10 grub2,grub2-http,grub2-tftp diff --git a/docs/source/guides/admin-guides/references/man7/osimage.7.rst b/docs/source/guides/admin-guides/references/man7/osimage.7.rst index cb31b767e..db91bb9d9 100644 --- a/docs/source/guides/admin-guides/references/man7/osimage.7.rst +++ b/docs/source/guides/admin-guides/references/man7/osimage.7.rst @@ -230,7 +230,7 @@ osimage Attributes: \ **osarch**\ (osimage.osarch) - The hardware architecture of this node. For netboot/statelite images, QEMU emulation for non-native architectures is used if qemu-user-static is installed and configured via systemd-binfmt. Valid values: x86_64, ppc64, x86, ia64, aarch64. + The hardware architecture of this node. For netboot/statelite images, QEMU emulation for non-native architectures is used if qemu-user-static is installed and configured via systemd-binfmt. Valid values: x86_64, ppc64, x86, ia64, aarch64, riscv64. diff --git a/docs/source/guides/admin-guides/references/man8/mknb.8.rst b/docs/source/guides/admin-guides/references/man8/mknb.8.rst index de6f39c38..acb753076 100644 --- a/docs/source/guides/admin-guides/references/man8/mknb.8.rst +++ b/docs/source/guides/admin-guides/references/man8/mknb.8.rst @@ -46,6 +46,8 @@ OpenEmbedded images use the exact architecture names \ ``x86``\ , \ ``x86_64``\ Canonical \ ``ppc64``\ images are big-endian. xCAT marks them so \ ``ppc64le``\ nodes do not use them as a legacy little-endian fallback. \ **mknb**\ also refuses to replace a marked \ ``ppc64``\ image with that fallback. +riscv64 nodes boot through UEFI firmware and grub2. For riscv64, \ **mknb**\ publishes the Genesis kernel and initramfs and writes one grub2 configuration per network under ``/tftpboot/boot/grub2``, named ``grub.cfg-`` followed by the network hex prefix, so that ``grub2.riscv64`` loaded by the firmware can start node discovery. The per-node files written by \ **nodeset**\ take priority over these network files. Networks served by a ``:noboot`` interface in ``site.dhcpinterfaces`` get no discovery configuration. + ******* OPTIONS diff --git a/docs/source/guides/install-guides/yum/grub2.rst b/docs/source/guides/install-guides/yum/grub2.rst index ff0b498a3..ff12e1539 100644 --- a/docs/source/guides/install-guides/yum/grub2.rst +++ b/docs/source/guides/install-guides/yum/grub2.rst @@ -1,17 +1,20 @@ -grub2 support for x86_64 and aarch64 -==================================== +grub2 support for x86_64, aarch64 and riscv64 +============================================= -xCAT 2.17 enables grub2 boot support for x86_64 and aarch64 but does not ship the necessary grub2 binaries for both architectures. +xCAT 2.17 enables grub2 boot support for x86_64 and aarch64 but does not ship the necessary grub2 binaries for those architectures. If you want to use grub2 for x86_64 or aarch64 you need to download the binaries from some EL OS repository. +riscv64 nodes can only boot through UEFI firmware and grub2, so they always need ``grub2.riscv64``. Recent ``grub2-xcat`` packages from xcat-dep install it; if the package on your management node does not (``ls /tftpboot/boot/grub2/grub2.riscv64``), copy the binary by hand as described below. + #. Download files from an BaseOS EL repository mirror - For EL these files are named ``grubx64.efi`` (x86_64) and ``grubaa64.efi`` (aarch64) and usually in ``BaseOS//os/EFI/BOOT``. + For EL these files are named ``grubx64.efi`` (x86_64), ``grubaa64.efi`` (aarch64) and ``grubriscv64.efi`` (riscv64) and usually in ``BaseOS//os/EFI/BOOT``. For very recent hardware you might need to use a newer grub version. Therefore, it's recommended to use a binary from the latest operationg system release available. You can use Red Hat Enterprise Linux, AlmaLinux, Rocky Linux or even Fedora repositories to download the grub files. #. Copy downloaded files to ``/tftpboot/boot/grub2``: - * x86_64: ``/tftpboot/boot/grub2/grub2.aarch64`` - * aarch64: ``/tftpboot/boot/grub2/grub2.x86_64`` + * x86_64: ``/tftpboot/boot/grub2/grub2.x86_64`` + * aarch64: ``/tftpboot/boot/grub2/grub2.aarch64`` + * riscv64: ``/tftpboot/boot/grub2/grub2.riscv64`` diff --git a/docs/source/overview/differentiators.rst b/docs/source/overview/differentiators.rst index 2f4fe5e49..7e9e68206 100644 --- a/docs/source/overview/differentiators.rst +++ b/docs/source/overview/differentiators.rst @@ -16,7 +16,7 @@ Differentiators * Support Multiple Hardware - IBM Power, IBM Power LE, x86_64, aarch64 (alpha support) + IBM Power, IBM Power LE, x86_64, aarch64 (alpha support), riscv64 (EL10, UEFI + grub2) * Support Multiple Virtualization Infrastructures diff --git a/docs/source/overview/support_matrix.rst b/docs/source/overview/support_matrix.rst index d458c6167..6d360f634 100644 --- a/docs/source/overview/support_matrix.rst +++ b/docs/source/overview/support_matrix.rst @@ -1,22 +1,24 @@ Operating System & Hardware Support Matrix ========================================== -+-------+-------+-------+-----+-------+--------+--------+--------+----------+ -| | Power | Power | zVM | Power | x86_64 | x86_64 | x86_64 | aarch64 | -| | | LE | | KVM | | KVM | Esxi | | -+=======+=======+=======+=====+=======+========+========+========+==========+ -|RHEL | yes | yes | yes | yes | yes | yes | yes | yes | -| | | | | | | | | | -+-------+-------+-------+-----+-------+--------+--------+--------+----------+ -|SLES | yes | yes | yes | yes | yes | yes | yes | no | -| | | | | | | | | | -+-------+-------+-------+-----+-------+--------+--------+--------+----------+ -|Ubuntu | no | yes | no | yes | yes | yes | yes | no | -| | | | | | | | | | -+-------+-------+-------+-----+-------+--------+--------+--------+----------+ -|CentOS | no | no | no | no | yes | yes | yes | no | -| | | | | | | | | | -+-------+-------+-------+-----+-------+--------+--------+--------+----------+ -|Windows| no | no | no | no | yes | yes | yes | no | -| | | | | | | | | | -+-------+-------+-------+-----+-------+--------+--------+--------+----------+ ++-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ +| | Power | Power | zVM | Power | x86_64 | x86_64 | x86_64 | aarch64 | riscv64 | +| | | LE | | KVM | | KVM | Esxi | | | ++=======+=======+=======+=====+=======+========+========+========+==========+==========+ +|RHEL | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| | | | | | | | | | | ++-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ +|SLES | yes | yes | yes | yes | yes | yes | yes | no | no | +| | | | | | | | | | | ++-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ +|Ubuntu | no | yes | no | yes | yes | yes | yes | no | no | +| | | | | | | | | | | ++-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ +|CentOS | no | no | no | no | yes | yes | yes | no | no | +| | | | | | | | | | | ++-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ +|Windows| no | no | no | no | yes | yes | yes | no | no | +| | | | | | | | | | | ++-------+-------+-------+-----+-------+--------+--------+--------+----------+----------+ + +riscv64 support covers EL10 compute nodes (Rocky Linux 10 and the RHEL 10 RISC-V developer preview) that boot through UEFI firmware and grub2. See :doc:`/guides/admin-guides/manage_clusters/riscv64/index` for details. diff --git a/xCAT-client/pods/man1/genimage.1.pod b/xCAT-client/pods/man1/genimage.1.pod index 60d9c78be..48633e597 100644 --- a/xCAT-client/pods/man1/genimage.1.pod +++ b/xCAT-client/pods/man1/genimage.1.pod @@ -50,7 +50,7 @@ I specifies the name of an os image definition to be used. The specif =item B<-a> I -The hardware architecture of this node: ppc64le, x86_64, ppc64, x86, ia64, aarch64 etc. If omitted, the current hardware architecture will be used. +The hardware architecture of this node: ppc64le, x86_64, ppc64, x86, ia64, aarch64, riscv64 etc. If omitted, the current hardware architecture will be used. =item B<-o> I diff --git a/xCAT-client/pods/man8/mknb.8.pod b/xCAT-client/pods/man8/mknb.8.pod index 1e377b0cc..6ffa74517 100644 --- a/xCAT-client/pods/man8/mknb.8.pod +++ b/xCAT-client/pods/man8/mknb.8.pod @@ -28,6 +28,8 @@ OpenEmbedded images use the exact architecture names C, C, C Canonical C images are big-endian. xCAT marks them so C nodes do not use them as a legacy little-endian fallback. B also refuses to replace a marked C image with that fallback. +riscv64 nodes boot through UEFI firmware and grub2. For riscv64, B publishes the Genesis kernel and initramfs and writes one grub2 configuration per network under C, named C followed by the network hex prefix, so that C loaded by the firmware can start node discovery. The per-node files written by B take priority over these network files. Networks served by a C<:noboot> interface in C get no discovery configuration. + =head1 OPTIONS =over 12 From 945f9fe0d19bc872afa239519fd76a2d04f3ecb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:54:22 -0300 Subject: [PATCH 14/54] build(rpm): build xCAT and xCATsn for riscv64 Add riscv64 to the xCAT and xCATsn build paths, keep the unavailable legacy Genesis dependency conditional, collect the architecture RPMs, and document the forcearch mock target. --- buildcore.sh | 2 +- buildlocal.sh | 6 +++++- buildrpms.pl | 8 +++++++- xCAT/xCAT.spec | 13 ++++++++++++- xCATsn/xCATsn.spec | 5 +++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/buildcore.sh b/buildcore.sh index 192ddf101..fbe85a7bf 100755 --- a/buildcore.sh +++ b/buildcore.sh @@ -392,7 +392,7 @@ for rpmname in xCAT xCATsn; do ./makerpm $rpmname "$EMBED" if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname"; fi else - for arch in $(xcat_rpm_build_arches x86_64 ppc64 ppc64le s390x aarch64); do + for arch in $(xcat_rpm_build_arches x86_64 ppc64 ppc64le s390x aarch64 riscv64); do if [ "$rpmname" = "xCAT-OpenStack" -a "$arch" != "x86_64" ] || [ "$rpmname" = "xCAT-OpenStack-baremetal" -a "$arch" != "x86_64" ] ; then continue; fi # only bld openstack for x86_64 for now ./makerpm $rpmname $arch "$EMBED" if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname-$arch"; fi diff --git a/buildlocal.sh b/buildlocal.sh index 853471758..c5cc13f78 100755 --- a/buildlocal.sh +++ b/buildlocal.sh @@ -90,10 +90,12 @@ echo "This is an $OSNAME system" rm -rf /usr/src/packages/RPMS/noarch/* rm -rf /usr/src/packages/RPMS/x86_64/* rm -rf /usr/src/packages/RPMS/ppc64/* + rm -rf /usr/src/packages/RPMS/riscv64/* else rm -rf /root/rpmbuild/RPMS/noarch/* rm -rf /root/rpmbuild/RPMS/x86_64/* rm -rf /root/rpmbuild/RPMS/ppc64/* + rm -rf /root/rpmbuild/RPMS/riscv64/* fi mkdir -p $CURDIR/build/ @@ -123,7 +125,7 @@ echo "This is an $OSNAME system" $CURDIR/makerpm $rpmname if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname"; fi else - for arch in x86_64 ppc64 s390x aarch64; do + for arch in x86_64 ppc64 s390x aarch64 riscv64; do $CURDIR/makerpm $rpmname $arch if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname-$arch"; fi done @@ -134,10 +136,12 @@ echo "This is an $OSNAME system" cp /usr/src/packages/RPMS/noarch/* $CURDIR/build/ cp /usr/src/packages/RPMS/x86_64/* $CURDIR/build/ cp /usr/src/packages/RPMS/ppc64/* $CURDIR/build/ + cp /usr/src/packages/RPMS/riscv64/* $CURDIR/build/ else cp /root/rpmbuild/RPMS/noarch/* $CURDIR/build/ cp /root/rpmbuild/RPMS/x86_64/* $CURDIR/build/ cp /root/rpmbuild/RPMS/ppc64/* $CURDIR/build/ + cp /root/rpmbuild/RPMS/riscv64/* $CURDIR/build/ fi #begin to create repo for redhat platform diff --git a/buildrpms.pl b/buildrpms.pl index fee5ada80..153be05c8 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -1106,6 +1106,12 @@ once per target). When omitted, the default is a single C<< +epel-10-. +A riscv64 build on an x86_64 builder needs a mock configuration that sets +C (with the riscv64 qemu-user-static +emulator registered through binfmt); the stock C only allows +riscv64 build hosts. Copy it under a new name in C, add the forcearch +option, and pass that name as C<--target>, e.g. C<--target=rocky-10-riscv64-xcat>. + =item B<--package>=I Build only selected package(s). Repeatable. @@ -1114,7 +1120,7 @@ Build only selected package(s). Repeatable. Build only the arch-native packages (C, C, C) -- the ones whose rpms carry the target arch. Everything else in the default set is C and -identical on every arch, so a secondary-arch builder (e.g. ppc64le) uses this to avoid +identical on every arch, so a secondary-arch builder (e.g. ppc64le or riscv64) uses this to avoid rebuilding the noarch packages that the x86_64 builder already produces. Ignored if C<--package> is given. diff --git a/xCAT/xCAT.spec b/xCAT/xCAT.spec index 34cbfdf04..f052ea36e 100644 --- a/xCAT/xCAT.spec +++ b/xCAT/xCAT.spec @@ -54,7 +54,10 @@ Requires: xCAT-server = 4:%{version}-%{release} %if %nots390x Requires: xCAT-probe = 4:%{version}-%{release} -Requires: xCAT-genesis-scripts-%{genesistarch} = 1:%{version}-%{release} +# Only where a legacy Genesis package exists for the build architecture. riscv64 has +# none: its image ships as xCAT-genesis-openembedded-riscv64, which mknb consumes +# directly, and an unset genesistarch would otherwise emit an unsatisfiable name. +%{?genesistarch:Requires: xCAT-genesis-scripts-%{genesistarch} = 1:%{version}-%{release}} # RPM 4.11 does not recognize weak dependency tags. %if 0%{?fedora} || 0%{?rhel} >= 8 || 0%{?suse_version} >= 1500 Recommends: xCAT-genesis-openembedded-x86_64 @@ -106,6 +109,14 @@ Requires: ipmitool-xcat >= 1.8.18-4 %endif %endif +%ifos linux +%ifarch riscv64 +# riscv64 management nodes manage BMC based nodes; the x86 PXE loaders are +# x86-only packages and riscv64 nodes boot through UEFI and grub2. +Requires: ipmitool-xcat >= 1.8.18-4 +%endif +%endif + %description xCAT is a server management package intended for at-scale management, including hardware management and software management. diff --git a/xCATsn/xCATsn.spec b/xCATsn/xCATsn.spec index 7540cd694..f048ef370 100644 --- a/xCATsn/xCATsn.spec +++ b/xCATsn/xCATsn.spec @@ -83,6 +83,11 @@ Requires: ipmitool-xcat >= 1.8.17-1 Requires: ipmitool-xcat >= 1.8.17-1 %endif %endif +%ifos linux +%ifarch riscv64 +Requires: ipmitool-xcat >= 1.8.17-1 +%endif +%endif %description xCAT supports management of very large sized cluster by creating a Hierarchical From aa208918b8c1e8381c2eccf53c842ca685744eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:54:23 -0300 Subject: [PATCH 15/54] test(xCAT-test): cover riscv64 rpm packaging and build scripts Pin the riscv64 %ifarch blocks in the xCAT, xCATsn and genesis specs (genesis token, ipmitool-xcat, no x86 PXE loaders) and the riscv64 entries in buildcore.sh, buildlocal.sh and the buildrpms.pl notes. --- xCAT-test/unit/riscv64_packaging.t | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 xCAT-test/unit/riscv64_packaging.t diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t new file mode 100644 index 000000000..aee37f5ec --- /dev/null +++ b/xCAT-test/unit/riscv64_packaging.t @@ -0,0 +1,55 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# riscv64 packaging: the arch-named packages (xCAT, xCATsn) must resolve their +# architecture token and dependencies for riscv64, and the build scripts must +# know the riscv64 package names. The Genesis image comes from the OpenEmbedded +# packages, so there is no arch-named Genesis package to resolve here. + +my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); + +sub read_file { + my ($relative) = @_; + my $path = File::Spec->catfile( $repo_root, split( m{/}, $relative ) ); + open( my $fh, '<', $path ) or die "Unable to read $path: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +my $xcat = read_file('xCAT/xCAT.spec'); +like( $xcat, qr/^%ifarch riscv64\n(?:#[^\n]*\n)*Requires: ipmitool-xcat >= 1\.8\.18-4\n%endif$/m, 'xCAT.spec requires ipmitool-xcat on riscv64' ); +my ($xcat_rv) = $xcat =~ /^%ifarch riscv64\n((?:#[^\n]*\n|Requires:[^\n]*\n)*)%endif$/m; +unlike( $xcat_rv || '', qr/xnba-undi|syslinux-xcat|elilo-xcat/, 'xCAT.spec does not require the x86 PXE loaders on riscv64' ); + +# riscv64 has no legacy Genesis package: the requirement must disappear rather than +# resolve to an unsatisfiable name, and the OpenEmbedded image is recommended instead. +like( $xcat, qr/^%\{\?genesistarch:Requires: xCAT-genesis-scripts-%\{genesistarch\} = 1:%\{version\}-%\{release\}\}$/m, 'xCAT.spec asks for the legacy Genesis package only where the architecture has one' ); + +SKIP: { + skip 'rpmspec is not installed', 2 unless `sh -c 'command -v rpmspec' 2>/dev/null`; + my $requires = `rpmspec -q --target riscv64 --requires xCAT/xCAT.spec 2>/dev/null`; + unlike( $requires, qr/genesis-scripts/, 'a riscv64 build requires no legacy Genesis package' ); + unlike( $requires, qr/\Q%{genesistarch}\E/, 'a riscv64 build leaves no unexpanded architecture macro' ); +} + +my $xcatsn = read_file('xCATsn/xCATsn.spec'); +like( $xcatsn, qr/^%ifarch riscv64\nRequires: ipmitool-xcat >= 1\.8\.17-1\n%endif$/m, 'xCATsn.spec requires ipmitool-xcat on riscv64' ); + + +my $buildcore = read_file('buildcore.sh'); +like( $buildcore, qr/^\s*for arch in x86_64 ppc64 ppc64le s390x aarch64 riscv64; do$/m, 'buildcore.sh builds xCAT and xCATsn for riscv64' ); + +my $buildlocal = read_file('buildlocal.sh'); +like( $buildlocal, qr/^\s*for arch in x86_64 ppc64 s390x aarch64 riscv64; do$/m, 'buildlocal.sh builds xCAT and xCATsn for riscv64' ); +like( $buildlocal, qr{^\s*cp /root/rpmbuild/RPMS/riscv64/\* \$CURDIR/build/$}m, 'buildlocal.sh collects riscv64 rpms' ); + +my $buildrpms = read_file('buildrpms.pl'); +like( $buildrpms, qr/forcearch/, 'buildrpms.pl documents the forcearch mock configuration for riscv64 builds' ); + +done_testing(); From 4f26b38d814d04d10a30b4dd11a720603f757796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:05:38 -0300 Subject: [PATCH 16/54] build(server): make perl-DB_File a weak dependency of xCAT-server perl-DB_File is only used by the Confluent client (lib/xcat/Confluent/Client.pm). EL10 dropped libdb, EPEL re-adds it only on its own architectures, and riscv64 has no perl-DB_File at all, so a hard Requires makes xCAT-server uninstallable on a riscv64 management node. Ask for it weakly: dnf installs it where it exists and skips it where it does not; the Confluent client stays optional. The dependency generator also turns Confluent/Client.pm's "use DB_File" into a hard perl(DB_File) requirement regardless of the Recommends, so that one generated requirement is excluded as well, appended to whatever filter the build root already set. Weak dependencies need rpm 4.12, so both only apply on a build host that has it (EL8 and later, or SUSE 15 and later); older build hosts keep the hard requirement they have today. --- xCAT-server/xCAT-server.spec | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/xCAT-server/xCAT-server.spec b/xCAT-server/xCAT-server.spec index 2c4f4b1a6..b964f90fd 100644 --- a/xCAT-server/xCAT-server.spec +++ b/xCAT-server/xCAT-server.spec @@ -51,7 +51,19 @@ Requires: perl-IO-Socket-SSL perl-XML-Simple perl-XML-Parser perl-Digest-SHA1 pe # therefore not auto-required, and (unlike SNMP, Expect, JSON, Net::Ping, # Time::HiRes and Text::Balanced) are not pulled in transitively by perl-xCAT # either. List them here or the affected subcommands die at load time. -Requires: perl-Net-Telnet perl-Net-DNS perl-Crypt-CBC perl-Crypt-Rijndael perl-DB_File +Requires: perl-Net-Telnet perl-Net-DNS perl-Crypt-CBC perl-Crypt-Rijndael +# DB_File is only used by the Confluent client (lib/xcat/Confluent/Client.pm). EL10 +# dropped libdb: EPEL still carries perl-DB_File for x86_64 and ppc64le, but riscv64 has +# no EPEL at all, so on EL10 ask for the module weakly -- it is installed where it exists +# and skipped where it cannot. The dependency generator still turns that "use DB_File" +# into a hard perl(DB_File) requirement; drop it there too, appending to any filter the +# build root already set. Every other platform keeps the hard requirement it has today. +%if 0%{?rhel} >= 10 +Recommends: perl-DB_File +%global __requires_exclude %{?__requires_exclude:%{__requires_exclude}|}^perl\\(DB_File\\)$ +%else +Requires: perl-DB_File +%endif %endif Obsoletes: atftp-xcat %endif From 428e5fa0f3e13f2f72881b0873ba15a01b732b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:05:38 -0300 Subject: [PATCH 17/54] test(xCAT-test): pin the weak perl-DB_File dependency --- xCAT-test/unit/riscv64_packaging.t | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t index aee37f5ec..8cf390e05 100644 --- a/xCAT-test/unit/riscv64_packaging.t +++ b/xCAT-test/unit/riscv64_packaging.t @@ -41,6 +41,25 @@ SKIP: { my $xcatsn = read_file('xCATsn/xCATsn.spec'); like( $xcatsn, qr/^%ifarch riscv64\nRequires: ipmitool-xcat >= 1\.8\.17-1\n%endif$/m, 'xCATsn.spec requires ipmitool-xcat on riscv64' ); +my $server = read_file('xCAT-server/xCAT-server.spec'); +like( $server, qr/^Recommends: perl-DB_File$/m, 'xCAT-server.spec recommends perl-DB_File on EL10 (riscv64 has no EPEL to provide it)' ); +like( + $server, + qr/^%if 0%\{\?rhel\} >= 10\nRecommends: perl-DB_File\n/m, + 'the weak perl-DB_File dependency is limited to EL10, where riscv64 has no EPEL', +); +like( + $server, + qr/^%else\nRequires: perl-DB_File\n%endif$/m, + 'build hosts without weak dependencies keep the hard perl-DB_File requirement', +); +like( + $server, + qr/^%global __requires_exclude %\{\?__requires_exclude:%\{__requires_exclude\}\|\}\^perl\\\\\(DB_File\\\\\)\$$/m, + 'xCAT-server.spec appends the generated perl(DB_File) requirement to the build root filter', +); +like( $server, qr/^Requires: perl-Net-Telnet perl-Net-DNS perl-Crypt-CBC perl-Crypt-Rijndael$/m, 'xCAT-server.spec keeps the other EL perl requires' ); + my $buildcore = read_file('buildcore.sh'); like( $buildcore, qr/^\s*for arch in x86_64 ppc64 ppc64le s390x aarch64 riscv64; do$/m, 'buildcore.sh builds xCAT and xCATsn for riscv64' ); From e7633d0dbadd864ebd9e77dc255156222a620f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:53:54 -0300 Subject: [PATCH 18/54] build(buildrpms): derive the rpm arch from suffixed mock targets targetarch_from_target took the last dash-separated part of the mock target name as the architecture. The riscv64 forcearch configuration is named rocky-10-riscv64-xcat, so riscv64 builds looked for xCAT-genesis-scripts-xcat srpms and failed. Take the last part that names an architecture and only fall back to the last part when none does; stock targets are unaffected. --- buildrpms.pl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/buildrpms.pl b/buildrpms.pl index 153be05c8..8029c8def 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -297,10 +297,15 @@ sub genesis_tarch_from_targetarch { sub targetarch_from_target { my ($target) = @_; return $ARCH unless defined $target && length $target; - my @parts = split /-/, $target; - my $arch = $parts[-1]; - $arch =~ s/^\s+|\s+$//g; - return lc $arch; + # mock configs are named --, but a site config may carry a + # suffix (e.g. rocky-10-riscv64-xcat, the forcearch config for riscv64 builds on an + # x86_64 host), so take the last part that names an architecture and only fall back + # to the last part when none does. + my @parts = map { my $p = lc $_; $p =~ s/^\s+|\s+$//g; $p } split /-/, $target; + for my $part (reverse @parts) { + return $part if $part =~ /^(?:x86_64|i[3-6]86|ppc64le|ppc64|aarch64|riscv64|s390x|armv7hl)$/; + } + return $parts[-1]; } # product(\@A, \@B) returns the catersian product of \@A and \@B From df712ee1538b01e2bd0c2e74ef239e8272da520e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:53:55 -0300 Subject: [PATCH 19/54] test(xCAT-test): cover suffixed mock targets in buildrpms.pl --- xCAT-test/unit/riscv64_packaging.t | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t index 8cf390e05..99c5fd482 100644 --- a/xCAT-test/unit/riscv64_packaging.t +++ b/xCAT-test/unit/riscv64_packaging.t @@ -71,4 +71,23 @@ like( $buildlocal, qr{^\s*cp /root/rpmbuild/RPMS/riscv64/\* \$CURDIR/build/$}m, my $buildrpms = read_file('buildrpms.pl'); like( $buildrpms, qr/forcearch/, 'buildrpms.pl documents the forcearch mock configuration for riscv64 builds' ); +# buildrpms.pl derives the rpm architecture from the mock target name; a suffixed +# site config (the riscv64 forcearch one) must still resolve to riscv64. +my ($targetarch_sub) = $buildrpms =~ m{^(sub targetarch_from_target \{.*?^\})}ms; +ok( $targetarch_sub, 'targetarch_from_target was located in buildrpms.pl' ) + or BAIL_OUT('buildrpms.pl no longer matches the expected targetarch_from_target shape'); +{ + package Test::BuildTarget; + our $ARCH = 'x86_64'; + eval $targetarch_sub; ## no critic (BuiltinFunctions::ProhibitStringyEval) + die "Unable to evaluate targetarch_from_target: $@" if $@; +} +is( Test::BuildTarget::targetarch_from_target('rocky-10-riscv64-xcat'), 'riscv64', 'a suffixed riscv64 forcearch target resolves to riscv64' ); +is( Test::BuildTarget::targetarch_from_target('rocky-10-riscv64'), 'riscv64', 'the stock riscv64 target resolves to riscv64' ); +is( Test::BuildTarget::targetarch_from_target('alma+epel-10-ppc64le'), 'ppc64le', 'the ppc64le target still resolves to ppc64le' ); +is( Test::BuildTarget::targetarch_from_target('alma+epel-10-x86_64'), 'x86_64', 'the x86_64 target still resolves to x86_64' ); +is( Test::BuildTarget::targetarch_from_target('opensuse-leap-15.6-x86_64'), 'x86_64', 'a dashed distro name still resolves its arch' ); +is( Test::BuildTarget::targetarch_from_target('custom-target-foo'), 'foo', 'a target without an architecture token keeps the last part' ); +is( Test::BuildTarget::targetarch_from_target(undef), 'x86_64', 'no target means the host architecture' ); + done_testing(); From bef5bd5eefb442f78f78a6b0f4f43b8a2ed20d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:43:11 -0300 Subject: [PATCH 20/54] feat(xCAT-server): kickstart EL10 riscv64 nodes around the anaconda boot loader gap The EL10 anaconda has no RISC-V EFI platform: on riscv64 it asks for the x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist, and registers the UEFI boot entry as \EFI\\shimx64.efi, so a kickstart install stops at the missing packages and, once tolerated, leaves a system the firmware cannot boot. Add riscv64 templates for the rocky10/rhels10 compute and service profiles that are the shared templates plus %packages --ignoremissing and a %post fix-up, riscv64 package lists that add grub2-efi-riscv64 and efibootmgr to the shared lists, and the fix-up itself (post.rhels10.riscv64): it copies \EFI\\grubriscv64.efi to the removable-media path \EFI\BOOT\BOOTRISCV64.EFI and re-points the UEFI boot entry at grubriscv64.efi, so the installed node boots from disk with or without usable NVRAM. Reinstalling a node replaces that entry instead of adding another one. The %post of a kickstart is a single shell script, into which xCAT splices every #INCLUDE, and post.rhels10/post.rhels8 end it with "exit 0"; the fix-up is therefore included ahead of them. The shared templates and other architectures are unchanged. --- .../rh/compute.rhels10.riscv64.pkglist | 16 +++++ .../install/rh/compute.rhels10.riscv64.tmpl | 70 +++++++++++++++++++ .../rh/service.rhels10.riscv64.pkglist | 21 ++++++ .../install/rh/service.rhels10.riscv64.tmpl | 70 +++++++++++++++++++ .../rocky/compute.rocky10.riscv64.pkglist | 16 +++++ .../rocky/compute.rocky10.riscv64.tmpl | 70 +++++++++++++++++++ .../rocky/service.rocky10.riscv64.pkglist | 19 +++++ .../rocky/service.rocky10.riscv64.tmpl | 70 +++++++++++++++++++ .../xcat/install/scripts/post.rhels10.riscv64 | 51 ++++++++++++++ 9 files changed, 403 insertions(+) create mode 100644 xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.pkglist create mode 100644 xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl create mode 100644 xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.pkglist create mode 100644 xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl create mode 100644 xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.pkglist create mode 100644 xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl create mode 100644 xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.pkglist create mode 100644 xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl create mode 100644 xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 diff --git a/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.pkglist b/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.pkglist new file mode 100644 index 000000000..437df1d02 --- /dev/null +++ b/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.pkglist @@ -0,0 +1,16 @@ +@Minimal Install +kernel +chrony +net-tools +nfs-utils +openssh-server +openssl +rsync +util-linux +wget +python3 +tar +bzip2 +perl-interpreter +grub2-efi-riscv64 +efibootmgr diff --git a/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl b/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl new file mode 100644 index 000000000..68e3817cb --- /dev/null +++ b/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl @@ -0,0 +1,70 @@ +# Use text install +text +# Use network installation +%include /tmp/repos +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 + +# Network information +#KICKSTARTNET# +# Root password +rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password# +# Not run the Setup Agent on first boot +firstboot --disable +# Do not configure the X Window System +skipx +# System services +#services --enabled="chronyd" +# System timezone +timezone #TABLE:site:key=timezone:value# --utc +# Partition clearing information +zerombr +clearpart --all --initlabel +#XCAT_PARTITION_START# +%include /tmp/partitionfile +#XCAT_PARTITION_END# + +# Do not configure any iptables rules +firewall --disable +selinux --disable +reboot + +# riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the +# x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist +# for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 +# and efibootmgr in explicitly. +%packages --ignoremissing +#INCLUDE_DEFAULT_PKGLIST# + +%end + +%pre +{ +echo "Running Kickstart Pre-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/pre.rhels10# +} &>>/tmp/pre-install.log +%end +%post --interpreter=/bin/bash +mkdir -p /var/log/xcat/ +cat /tmp/pre-install.log >>/var/log/xcat/xcat.log +{ +echo "Running Kickstart Post-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.xcat.rhels10# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10.riscv64# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10# +} &>>/var/log/xcat/xcat.log +%end + +# --ignoremissing above also hides a missing grub2-efi-riscv64, which would leave the +# node with no boot loader at all. Fail the installation instead of rebooting into +# firmware that has nothing to load. +%post --erroronfail --interpreter=/bin/bash +if [ ! -f /boot/efi/EFI/BOOT/BOOTRISCV64.EFI ] && + ! ls /boot/efi/EFI/*/grubriscv64.efi >/dev/null 2>&1; then + echo "riscv64: no grub2 UEFI boot loader on the ESP; grub2-efi-riscv64 is missing" \ + >>/var/log/xcat/xcat.log + exit 1 +fi +%end diff --git a/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.pkglist b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.pkglist new file mode 100644 index 000000000..eb3b4ec36 --- /dev/null +++ b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.pkglist @@ -0,0 +1,21 @@ +@Minimal Install +kernel +chrony +net-tools +nfs-utils +openssh-server +openssl +rsync +util-linux +wget +mariadb-connector-odbc +perl-DBD-MySQL +perl-DBD-Pg +unixODBC +python3 +tar +bzip2 +perl-interpreter +perl-lib +grub2-efi-riscv64 +efibootmgr diff --git a/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl new file mode 100644 index 000000000..68e3817cb --- /dev/null +++ b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl @@ -0,0 +1,70 @@ +# Use text install +text +# Use network installation +%include /tmp/repos +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 + +# Network information +#KICKSTARTNET# +# Root password +rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password# +# Not run the Setup Agent on first boot +firstboot --disable +# Do not configure the X Window System +skipx +# System services +#services --enabled="chronyd" +# System timezone +timezone #TABLE:site:key=timezone:value# --utc +# Partition clearing information +zerombr +clearpart --all --initlabel +#XCAT_PARTITION_START# +%include /tmp/partitionfile +#XCAT_PARTITION_END# + +# Do not configure any iptables rules +firewall --disable +selinux --disable +reboot + +# riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the +# x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist +# for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 +# and efibootmgr in explicitly. +%packages --ignoremissing +#INCLUDE_DEFAULT_PKGLIST# + +%end + +%pre +{ +echo "Running Kickstart Pre-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/pre.rhels10# +} &>>/tmp/pre-install.log +%end +%post --interpreter=/bin/bash +mkdir -p /var/log/xcat/ +cat /tmp/pre-install.log >>/var/log/xcat/xcat.log +{ +echo "Running Kickstart Post-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.xcat.rhels10# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10.riscv64# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10# +} &>>/var/log/xcat/xcat.log +%end + +# --ignoremissing above also hides a missing grub2-efi-riscv64, which would leave the +# node with no boot loader at all. Fail the installation instead of rebooting into +# firmware that has nothing to load. +%post --erroronfail --interpreter=/bin/bash +if [ ! -f /boot/efi/EFI/BOOT/BOOTRISCV64.EFI ] && + ! ls /boot/efi/EFI/*/grubriscv64.efi >/dev/null 2>&1; then + echo "riscv64: no grub2 UEFI boot loader on the ESP; grub2-efi-riscv64 is missing" \ + >>/var/log/xcat/xcat.log + exit 1 +fi +%end diff --git a/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.pkglist b/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.pkglist new file mode 100644 index 000000000..437df1d02 --- /dev/null +++ b/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.pkglist @@ -0,0 +1,16 @@ +@Minimal Install +kernel +chrony +net-tools +nfs-utils +openssh-server +openssl +rsync +util-linux +wget +python3 +tar +bzip2 +perl-interpreter +grub2-efi-riscv64 +efibootmgr diff --git a/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl b/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl new file mode 100644 index 000000000..68e3817cb --- /dev/null +++ b/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl @@ -0,0 +1,70 @@ +# Use text install +text +# Use network installation +%include /tmp/repos +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 + +# Network information +#KICKSTARTNET# +# Root password +rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password# +# Not run the Setup Agent on first boot +firstboot --disable +# Do not configure the X Window System +skipx +# System services +#services --enabled="chronyd" +# System timezone +timezone #TABLE:site:key=timezone:value# --utc +# Partition clearing information +zerombr +clearpart --all --initlabel +#XCAT_PARTITION_START# +%include /tmp/partitionfile +#XCAT_PARTITION_END# + +# Do not configure any iptables rules +firewall --disable +selinux --disable +reboot + +# riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the +# x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist +# for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 +# and efibootmgr in explicitly. +%packages --ignoremissing +#INCLUDE_DEFAULT_PKGLIST# + +%end + +%pre +{ +echo "Running Kickstart Pre-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/pre.rhels10# +} &>>/tmp/pre-install.log +%end +%post --interpreter=/bin/bash +mkdir -p /var/log/xcat/ +cat /tmp/pre-install.log >>/var/log/xcat/xcat.log +{ +echo "Running Kickstart Post-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.xcat.rhels10# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10.riscv64# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10# +} &>>/var/log/xcat/xcat.log +%end + +# --ignoremissing above also hides a missing grub2-efi-riscv64, which would leave the +# node with no boot loader at all. Fail the installation instead of rebooting into +# firmware that has nothing to load. +%post --erroronfail --interpreter=/bin/bash +if [ ! -f /boot/efi/EFI/BOOT/BOOTRISCV64.EFI ] && + ! ls /boot/efi/EFI/*/grubriscv64.efi >/dev/null 2>&1; then + echo "riscv64: no grub2 UEFI boot loader on the ESP; grub2-efi-riscv64 is missing" \ + >>/var/log/xcat/xcat.log + exit 1 +fi +%end diff --git a/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.pkglist b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.pkglist new file mode 100644 index 000000000..e9c3e2263 --- /dev/null +++ b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.pkglist @@ -0,0 +1,19 @@ +@^minimal-environment +chrony +net-tools +nfs-utils +openssh-server +rsync +util-linux +wget +mariadb-connector-odbc +perl-DBD-MySQL +perl-DBD-Pg +unixODBC +python3 +tar +bzip2 +perl-interpreter +perl-lib +grub2-efi-riscv64 +efibootmgr diff --git a/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl new file mode 100644 index 000000000..00364f2ac --- /dev/null +++ b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl @@ -0,0 +1,70 @@ +# Use text install +text +# Use network installation +%include /tmp/repos +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 + +# Network information +#KICKSTARTNET# +# Root password +rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password# +# Not run the Setup Agent on first boot +firstboot --disable +# Do not configure the X Window System +skipx +# System services +#services --enabled="chronyd" +# System timezone +timezone #TABLE:site:key=timezone:value# --utc +# Partition clearing information +zerombr +clearpart --all --initlabel +#XCAT_PARTITION_START# +%include /tmp/partitionfile +#XCAT_PARTITION_END# + +# Do not configure any iptables rules +firewall --disable +selinux --disable +reboot + +# riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the +# x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist +# for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 +# and efibootmgr in explicitly. +%packages --ignoremissing +#INCLUDE_DEFAULT_PKGLIST# + +%end + +%pre +{ +echo "Running Kickstart Pre-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/pre.rhels8# +} &>>/tmp/pre-install.log +%end +%post --interpreter=/bin/bash +mkdir -p /var/log/xcat/ +cat /tmp/pre-install.log >>/var/log/xcat/xcat.log +{ +echo "Running Kickstart Post-installation script..." +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.xcat.ng# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels10.riscv64# +#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post.rhels8# +} &>>/var/log/xcat/xcat.log +%end + +# --ignoremissing above also hides a missing grub2-efi-riscv64, which would leave the +# node with no boot loader at all. Fail the installation instead of rebooting into +# firmware that has nothing to load. +%post --erroronfail --interpreter=/bin/bash +if [ ! -f /boot/efi/EFI/BOOT/BOOTRISCV64.EFI ] && + ! ls /boot/efi/EFI/*/grubriscv64.efi >/dev/null 2>&1; then + echo "riscv64: no grub2 UEFI boot loader on the ESP; grub2-efi-riscv64 is missing" \ + >>/var/log/xcat/xcat.log + exit 1 +fi +%end diff --git a/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 b/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 new file mode 100644 index 000000000..09245064c --- /dev/null +++ b/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 @@ -0,0 +1,51 @@ +# riscv64 UEFI boot entry fix-up for EL10 kickstart installs. +# +# The EL10 anaconda has no RISC-V EFI platform: it asks for the x86 boot +# loader packages (grub2-efi-x64, shim-x64; tolerated with --ignoremissing) +# and registers the UEFI boot entry as \EFI\\shimx64.efi, which does +# not exist on riscv64, so the firmware cannot boot the installed system. +# grub2-efi-riscv64 (from the riscv64 package list) provides +# \EFI\\grubriscv64.efi and anaconda still writes the ESP grub.cfg +# stub next to it, so point the boot entry at that image and also place the +# removable-media fallback loader \EFI\BOOT\BOOTRISCV64.EFI for firmware +# without usable NVRAM entries. Runs in the installed system chroot. +if [ "$(uname -m)" = "riscv64" ] && [ -d /boot/efi/EFI ]; then + grubefi="" + for candidate in /boot/efi/EFI/*/grubriscv64.efi; do + [ -f "$candidate" ] || continue + grubefi="$candidate" + break + done + if [ -n "$grubefi" ]; then + vendordir="$(basename "$(dirname "$grubefi")")" + mkdir -p /boot/efi/EFI/BOOT + cp -f "$grubefi" /boot/efi/EFI/BOOT/BOOTRISCV64.EFI + echo "riscv64: installed \\EFI\\BOOT\\BOOTRISCV64.EFI from \\EFI\\$vendordir\\grubriscv64.efi" + if command -v efibootmgr >/dev/null 2>&1 && efibootmgr >/dev/null 2>&1; then + espdev="$(findmnt -no SOURCE /boot/efi 2>/dev/null)" + # lsblk pads its output, and efibootmgr wants bare values + disk="$(lsblk -no PKNAME "$espdev" 2>/dev/null | head -1 | tr -d '[:space:]')" + part="$(lsblk -no PARTN "$espdev" 2>/dev/null | head -1 | tr -d '[:space:]')" + if [ -n "$disk" ] && [ -n "$part" ]; then + # drop the x86 entries anaconda created for this install, and any + # riscv64 entry a previous provisioning of this node left behind, so + # reinstalling does not fill the firmware variable store with copies + for bootnum in $(efibootmgr -v 2>/dev/null | awk '/\\EFI\\[^\\]+\\(shimx64|grubx64|grubriscv64)\.efi/ { sub(/^Boot/, "", $1); sub(/\*$/, "", $1); print $1 }'); do + efibootmgr -q -b "$bootnum" -B || true + done + label="$(. /etc/os-release 2>/dev/null; echo "${NAME:-Linux}")" + if efibootmgr -q -c -d "/dev/$disk" -p "$part" -L "$label" -l "\\EFI\\$vendordir\\grubriscv64.efi"; then + echo "riscv64: UEFI boot entry \"$label\" -> \\EFI\\$vendordir\\grubriscv64.efi on /dev/$disk part $part" + else + echo "riscv64: efibootmgr could not create the boot entry; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" + fi + else + echo "riscv64: could not determine the ESP disk/partition; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" + fi + else + echo "riscv64: efibootmgr unavailable; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" + fi + else + echo "riscv64: no \\EFI\\*\\grubriscv64.efi on the ESP; is grub2-efi-riscv64 in the package list?" + fi +fi From 4b20177f39989292486d412ffe320b94fa6c83b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:43:11 -0300 Subject: [PATCH 21/54] test(xCAT-test): cover the EL10 riscv64 install templates and boot entry fix-up --- xCAT-test/unit/install_profile_riscv64.t | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 xCAT-test/unit/install_profile_riscv64.t diff --git a/xCAT-test/unit/install_profile_riscv64.t b/xCAT-test/unit/install_profile_riscv64.t new file mode 100644 index 000000000..ad9f6b784 --- /dev/null +++ b/xCAT-test/unit/install_profile_riscv64.t @@ -0,0 +1,98 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use Cwd qw(realpath); +use File::Spec; +use FindBin; +use Test::More; + +# EL10 riscv64 kickstart installs: the EL10 anaconda has no RISC-V EFI +# platform, so the riscv64 templates tolerate its x86 boot loader package +# request, pull grub2-efi-riscv64/efibootmgr in through the riscv64 package +# lists, and run a %post that points the UEFI boot entry at grubriscv64.efi +# and places the removable-media fallback loader. + +my $repo_root = realpath( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $install = File::Spec->catdir( $repo_root, 'xCAT-server', 'share', 'xcat', 'install' ); + +sub slurp { + my ($path) = @_; + open( my $fh, '<', $path ) or die "Unable to read $path: $!"; + my $content = do { local $/; <$fh> }; + close($fh); + return $content; +} + +for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { + my ( $dir, $osbase ) = @$family; + for my $profile (qw(compute service)) { + my $tmpl = File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" ); + my $pkglist = File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.pkglist" ); + my $base = File::Spec->catfile( $install, $dir, "$profile.$osbase.tmpl" ); + ok( -r $tmpl, "$dir/$profile.$osbase.riscv64.tmpl exists" ); + ok( -r $pkglist, "$dir/$profile.$osbase.riscv64.pkglist exists" ); + my $t = slurp($tmpl); + like( $t, qr/^%packages --ignoremissing$/m, "$dir/$profile.$osbase riscv64 template tolerates anaconda's x86 boot loader package request" ); + like( $t, qr/^#INCLUDE_DEFAULT_PKGLIST#$/m, "$dir/$profile.$osbase riscv64 template still includes the default package list" ); + like( $t, qr{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#$}m, "$dir/$profile.$osbase riscv64 template runs the riscv64 UEFI boot entry fix-up" ); + like( $t, qr/^%post --erroronfail --interpreter=\/bin\/bash$/m, "$dir/$profile.$osbase riscv64 template fails the install when no boot loader reached the ESP" ); + like( $t, qr{/boot/efi/EFI/BOOT/BOOTRISCV64\.EFI}, "$dir/$profile.$osbase riscv64 template checks the removable-media loader" ); + like( $t, qr{/boot/efi/EFI/\*/grubriscv64\.efi}, "$dir/$profile.$osbase riscv64 template also accepts a vendor directory loader" ); + like( $t, qr/^rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password#$/m, "$dir/$profile.$osbase riscv64 template keeps the shared root password directive" ); + + # the riscv64 template is the shared one plus the riscv64 changes + my $b = slurp($base); + ( my $t_norm = $t ) =~ s/^# riscv64:.*\n(?:#.*\n)*//m; + $t_norm =~ s/^%packages --ignoremissing$/%packages/m; + $t_norm =~ s{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#\n}{}m; + $t_norm =~ s/\n# --ignoremissing above.*?\n%post --erroronfail.*?\n%end\n//s; + is( $t_norm, $b, "$dir/$profile.$osbase riscv64 template only differs from the shared template in the riscv64 changes" ); + + my $p = slurp($pkglist); + like( $p, qr/^grub2-efi-riscv64$/m, "$dir/$profile.$osbase riscv64 package list installs grub2-efi-riscv64" ); + like( $p, qr/^efibootmgr$/m, "$dir/$profile.$osbase riscv64 package list installs efibootmgr" ); + unlike( $p, qr/shim|grub2-efi-x64|grub2-efi-aa64/, "$dir/$profile.$osbase riscv64 package list asks for no other architecture's boot loader" ); + my $shared = slurp( File::Spec->catfile( $install, $dir, "$profile.$osbase.pkglist" ) ); + ( my $p_norm = $p ) =~ s/^grub2-efi-riscv64\n//m; + $p_norm =~ s/^efibootmgr\n//m; + s/\s+\z/\n/ for ( $p_norm, $shared ); + is( $p_norm, $shared, "$dir/$profile.$osbase riscv64 package list is the shared list plus the boot loader packages" ); + } +} + +# The kickstart %post is a single shell script: xCAT splices every #INCLUDE: inline, so a +# top-level "exit" in an earlier included script ends the whole section. The riscv64 boot +# entry fix-up must run, so it has to come before any script that exits. +for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { + my ( $dir, $osbase ) = @$family; + for my $profile (qw(compute service)) { + my $t = slurp( File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" ) ); + my ($section) = $t =~ /^(%post\b.*?)^%end/ms; + ok( $section, "$dir/$profile.$osbase riscv64 template has a %post section" ) + or next; + my @scripts = $section =~ m{^#INCLUDE:\#ENV:XCATROOT\#/share/xcat/install/scripts/(\S+?)\#$}mg; + my ($index) = grep { $scripts[$_] eq 'post.rhels10.riscv64' } 0 .. $#scripts; + ok( defined $index, "$dir/$profile.$osbase riscv64 template includes the fix-up in the %post section" ) + or next; + for my $earlier ( @scripts[ 0 .. $index - 1 ] ) { + my $body = slurp( File::Spec->catfile( $install, 'scripts', $earlier ) ); + unlike( $body, qr/^\s*exit\b/m, + "$dir/$profile.$osbase runs $earlier before the riscv64 fix-up, and $earlier does not end the %post" ); + } + } +} + +my $post = File::Spec->catfile( $install, 'scripts', 'post.rhels10.riscv64' ); +ok( -r $post, 'post.rhels10.riscv64 exists' ); +my $bash = `bash -n $post 2>&1`; +is( $bash, '', 'post.rhels10.riscv64 parses as bash' ); +my $s = slurp($post); +like( $s, qr/\[ "\$\(uname -m\)" = "riscv64" \]/, 'the fix-up only acts on riscv64' ); +like( $s, qr{/boot/efi/EFI/\*/grubriscv64\.efi}, 'the fix-up locates the distro grub2 UEFI image on the ESP' ); +like( $s, qr{cp -f "\$grubefi" /boot/efi/EFI/BOOT/BOOTRISCV64\.EFI}, 'the fix-up installs the removable-media fallback loader' ); +like( $s, qr/shimx64\|grubx64\|grubriscv64/, 'the fix-up removes the x86 boot entries anaconda registered and the riscv64 entry of an earlier install' ); +like( $s, qr/efibootmgr -q -c -d "\/dev\/\$disk" -p "\$part" -L "\$label" -l "\\\\EFI\\\\\$vendordir\\\\grubriscv64\.efi"/, 'the fix-up registers grubriscv64.efi as the boot entry' ); +unlike( $s, qr/set -e/, 'the fix-up never aborts the kickstart %post' ); + +done_testing(); From 20b3834a260d030aac09f0b5afde04b70bbfc922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 20 Aug 2026 19:45:52 -0300 Subject: [PATCH 22/54] docs(riscv64): describe the EL10 installer workaround and dependency notes Document the riscv64 kickstart templates that work around the EL10 anaconda's missing RISC-V EFI platform, where grub2.riscv64 and the riscv64 Genesis packages come from, which dependencies a riscv64 management node takes from the riscv64 dependency repository, and why xCAT-server only recommends perl-DB_File. --- .../manage_clusters/riscv64/index.rst | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst index 4279b4596..f87a58381 100644 --- a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst +++ b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst @@ -4,7 +4,7 @@ RISC-V 64-bit (riscv64) xCAT manages RISC-V 64-bit (``riscv64``) compute nodes running EL10. Rocky Linux 10 is the reference distribution; the RHEL 10 RISC-V developer preview uses the same media layout. The general cluster management documentation under -:doc:`/guides/admin-guides/manage_clusters/common/index` applies; this page +:doc:`/guides/admin-guides/manage_clusters/index` applies; this page only covers what is specific to the architecture. What riscv64 nodes need @@ -19,24 +19,27 @@ What riscv64 nodes need ``grub2-http`` is recommended for installers, whose initrd is large. * ``nodetype.arch`` and ``osimage.osarch`` are ``riscv64``. No alias is needed: ``uname -m``, rpm and dpkg all use the same token. -* ``/tftpboot/boot/grub2/grub2.riscv64``: the EL grub2 UEFI image for riscv64. - It is provided by the ``grub2-xcat`` package where available; otherwise copy - ``EFI/BOOT/grubriscv64.efi`` from the EL10 riscv64 BaseOS media as described - in :doc:`/guides/install-guides/yum/grub2`. -* The riscv64 Genesis image (``xCAT-genesis-base-riscv64`` and - ``xCAT-genesis-scripts-riscv64``) for discovery, BMC setup and flashing. Its - kernel must be loadable by grub2, that is, built with the EFI stub. +* ``/tftpboot/boot/grub2/grub2.riscv64``: the EL grub2 UEFI image for riscv64 + (the ``EFI/BOOT/grubriscv64.efi`` of the EL10 riscv64 BaseOS tree). It is + provided by the ``grub2-xcat`` package where available; otherwise copy it as + described in :doc:`/guides/install-guides/yum/grub2`. +* The riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) for + discovery, BMC setup and flashing. Its kernel is loaded by grub2 through the + EFI stub. ``go-xcat`` installs the package; on a management node built another + way, install it explicitly (``dnf install xCAT-genesis-openembedded-riscv64``), + the same way the images of other architectures are installed for a mixed + cluster. ``xcatconfig`` runs ``mknb riscv64`` for every installed image. -The management node itself can be x86_64 or ppc64le; riscv64 is managed like -any other mixed-architecture cluster (see -:doc:`/advanced/mixed_cluster/support_matrix`). +The management node itself is x86_64 (the validated combination, see +:doc:`/advanced/mixed_cluster/support_matrix`) or riscv64 (see below); riscv64 +nodes are managed like any other mixed-architecture cluster. Node discovery -------------- ``mknb riscv64`` publishes the Genesis kernel and initramfs as ``/tftpboot/xcat/genesis.kernel.riscv64`` and -``/tftpboot/xcat/genesis.fs.riscv64.gz`` and writes one grub2 configuration per +``/tftpboot/xcat/genesis.fs.riscv64.lzma`` (or ``.gz``) and writes one grub2 configuration per network, ``/tftpboot/boot/grub2/grub.cfg-``. A net booted ``grub2.riscv64`` looks for ``grub.cfg-01-``, then ``grub.cfg-<8 hex digit ip>``, then shorter prefixes of that ip; the per-node files written by @@ -55,14 +58,22 @@ Stateful (diskful) installation Import the Rocky Linux 10 riscv64 DVD with ``copycds``; it creates the ``rocky10.x-riscv64-install-compute`` osimage. The installer kernel and initrd -come from ``images/pxeboot`` on the media, like x86_64 and aarch64. The default -kickstart templates and package lists are shared with the other architectures; +come from ``images/pxeboot`` on the media, like x86_64 and aarch64. ``service.rocky10.riscv64.otherpkgs.pkglist`` pulls the service node packages from the ``rh10/riscv64`` xcat-dep repository. -The installer creates the UEFI boot entry for the installed system; after -``nodeset boot`` the firmware falls through to it because the per-node -``grub2-`` loader link is removed. +The EL10 installer (anaconda) has no RISC-V EFI platform: on riscv64 it asks +for the x86 UEFI boot loader packages (``grub2-efi-x64``, ``shim-x64``), which +do not exist, and registers the UEFI boot entry as ``\EFI\\shimx64.efi``. +xCAT therefore ships riscv64 variants of the EL10 templates and package lists +(``compute.rocky10.riscv64.tmpl``, ``compute.rhels10.riscv64.tmpl`` and the +service profiles): ``%packages --ignoremissing`` tolerates the request, the +package lists add ``grub2-efi-riscv64`` and ``efibootmgr``, and the +``post.rhels10.riscv64`` script run from ``%post`` points the UEFI boot entry at +``\EFI\\grubriscv64.efi`` and places the removable-media fallback loader +``\EFI\BOOT\BOOTRISCV64.EFI``. Custom templates for riscv64 should start from +these files. After ``nodeset boot`` the firmware boots the installed +system because the per-node ``grub2-`` loader link is removed. Stateless (diskless) images --------------------------- @@ -75,7 +86,10 @@ registered for riscv64 through ``systemd-binfmt``; see ``qemu-user-static`` package of its own, so take the static riscv64 emulator from a distribution that ships one. The default network driver list for riscv64 images covers virtio, Intel, Realtek, Broadcom and Mellanox adapters; -add others through ``linuximage.netdrivers``. +add others through ``linuximage.netdrivers``. Cross-building on an x86_64 +management node needs the riscv64 user-mode emulator registered with +systemd-binfmt, as described in +:doc:`/advanced/mixed_cluster/building_stateless_images`. Management node on riscv64 -------------------------- @@ -87,14 +101,20 @@ come from the riscv64 xcat-dep repository together with the usual xcat-dep packages: * from EPEL on x86_64: ``perl-Digest-SHA1``, ``perl-Net-DNS``, - ``perl-Crypt-CBC``, ``perl-Crypt-Rijndael``, ``perl-DB_File``; optional - features also use ``perl-Expect``, ``perl-HTML-Form``, ``perl-Sys-Virt``, - ``perl-Mail-Sender``, ``perl-SOAP-Lite``, ``perl-Crypt-Blowfish``, - ``perl-Net-IP`` and ``conserver``; + ``perl-Crypt-CBC`` and ``perl-Crypt-Rijndael``; optional features also use + ``perl-Expect``, ``perl-HTML-Form``, ``perl-Sys-Virt``, ``perl-Mail-Sender``, + ``perl-SOAP-Lite``, ``perl-Crypt-Blowfish``, ``perl-Net-IP`` and + ``conserver``; * always from xcat-dep: ``perl-Net-Telnet``, ``perl-IO-Stty``, ``perl-Net-HTTPS-NB``, ``perl-HTTP-Async``, ``perl-Crypt-SSLeay``, ``goconserver``, ``ipmitool-xcat``, ``conserver-xcat`` and ``grub2-xcat``. +``perl-DB_File`` is deliberately not in that list: it cannot be built for +riscv64 (EL10 has no libdb). It is only used by the optional Confluent client, +so ``xCAT-server`` recommends it instead of requiring it. On the architectures +that do have it, the weak dependency installs it as before; a management node +that disables weak dependencies (``install_weak_deps=False``) has to install +``perl-DB_File`` explicitly to keep the Confluent client working. xCAT does not install anything from CPAN; every dependency is an rpm. Limitations From 60afa553ab78e619e320f97fda4d4e46938ff389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:05:47 -0300 Subject: [PATCH 23/54] feat(xCAT-server): reserve a crash kernel for riscv64 diskless images The kdump branch of the diskless kernel command line has a default reservation for ppc64 and x86 only. On any other architecture an image with linuximage.dump set but no linuximage.crashkernelsize got dump= and no crashkernel= at all, so the kernel reserved nothing and kdump could never run. EL has no default reservation for riscv64 either (kdumpctl get-default-crashkernel is empty there), so nothing else fills the gap. Give riscv64 the same treatment as the architectures around it and default to 256M. An explicit crashkernelsize still wins, and images without dump are unchanged. --- xCAT-server/lib/xcat/plugins/anaconda.pm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xCAT-server/lib/xcat/plugins/anaconda.pm b/xCAT-server/lib/xcat/plugins/anaconda.pm index 334f17108..77d4a2ae7 100644 --- a/xCAT-server/lib/xcat/plugins/anaconda.pm +++ b/xCAT-server/lib/xcat/plugins/anaconda.pm @@ -918,6 +918,11 @@ sub mknetboot if ($arch =~ /86/) { $kcmdline .= " crashkernel=128M dump=$dump "; } + if ($arch eq "riscv64") { + # EL has no default reservation for riscv64, so without an explicit + # linuximage.crashkernelsize the kernel would reserve nothing at all + $kcmdline .= " crashkernel=256M dump=$dump "; + } } } From 30a773deebd206e2cbe52724eb2a5af497fbe435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:05:48 -0300 Subject: [PATCH 24/54] test(xCAT-test): cover the riscv64 diskless crash kernel default --- xCAT-test/unit/el_riscv64_install_paths.t | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/xCAT-test/unit/el_riscv64_install_paths.t b/xCAT-test/unit/el_riscv64_install_paths.t index c204ac238..8b9f97108 100644 --- a/xCAT-test/unit/el_riscv64_install_paths.t +++ b/xCAT-test/unit/el_riscv64_install_paths.t @@ -85,4 +85,21 @@ is_deeply( resolver_libs('riscv64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv is_deeply( resolver_libs('x86_64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], 'x86_64 images still use lib64' ); is_deeply( resolver_libs('ppc64'), [ 'lib/libnss_dns.so.2', 'lib/libresolv.so.2' ], 'ppc64 images still use lib' ); +# anaconda.pm: crash kernel reservation for diskless images with kdump enabled +like( + $anaconda, + qr/if \(\$arch eq "riscv64"\) \{\n(?:\s*#[^\n]*\n)*\s*\$kcmdline \.= " crashkernel=256M dump=\$dump ";/, + 'a riscv64 diskless image with kdump enabled reserves a crash kernel by default', +); +like( + $anaconda, + qr/if \(\$arch =~ \/86\/\) \{\n\s*\$kcmdline \.= " crashkernel=128M dump=\$dump ";/, + 'the x86 default reservation is unchanged', +); +like( + $anaconda, + qr/\$kcmdline \.= " crashkernel=\$crashkernelsize dump=\$dump ";/, + 'an explicit linuximage.crashkernelsize still wins on every architecture', +); + done_testing(); From dcbaa51b7569260fa3e266571efd1e1d7e604659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:05:48 -0300 Subject: [PATCH 25/54] fix(xCAT-server): stop the EL10 installer reserving a crash kernel riscv64 cannot use EL10 defines no default crash kernel reservation for riscv64, so the installer's kdump add-on falls back to writing the literal "crashkernel=auto" into the boot loader arguments of the installed system. EL10 kernels dropped support for that value: nothing is reserved, but the string is on the command line, so kdump.service passes its condition and then fails on every installed riscv64 node. Turn the add-on off in the riscv64 templates. Nodes come up with kdump inactive instead of failed, and a node that should take crash dumps gets a real reservation the usual way, through linuximage.addkcmdline or bootparams.addkcmdline. --- .../share/xcat/install/rh/compute.rhels10.riscv64.tmpl | 9 +++++++++ .../share/xcat/install/rh/service.rhels10.riscv64.tmpl | 9 +++++++++ .../xcat/install/rocky/compute.rocky10.riscv64.tmpl | 9 +++++++++ .../xcat/install/rocky/service.rocky10.riscv64.tmpl | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl b/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl index 68e3817cb..dbf34f13c 100644 --- a/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl +++ b/xCAT-server/share/xcat/install/rh/compute.rhels10.riscv64.tmpl @@ -31,6 +31,15 @@ firewall --disable selinux --disable reboot +# riscv64: EL10 has no default crash kernel reservation for this architecture +# (kdumpctl get-default-crashkernel is empty), so the installer's kdump add-on +# would write the literal "crashkernel=auto", which the kernel ignores: nothing +# is reserved and kdump.service fails on every node. Leave kdump off here; a +# node that should take crash dumps gets a real reservation through +# linuximage.addkcmdline or bootparams.addkcmdline (crashkernel=). +%addon com_redhat_kdump --disable +%end + # riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the # x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist # for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 diff --git a/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl index 68e3817cb..dbf34f13c 100644 --- a/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl +++ b/xCAT-server/share/xcat/install/rh/service.rhels10.riscv64.tmpl @@ -31,6 +31,15 @@ firewall --disable selinux --disable reboot +# riscv64: EL10 has no default crash kernel reservation for this architecture +# (kdumpctl get-default-crashkernel is empty), so the installer's kdump add-on +# would write the literal "crashkernel=auto", which the kernel ignores: nothing +# is reserved and kdump.service fails on every node. Leave kdump off here; a +# node that should take crash dumps gets a real reservation through +# linuximage.addkcmdline or bootparams.addkcmdline (crashkernel=). +%addon com_redhat_kdump --disable +%end + # riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the # x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist # for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 diff --git a/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl b/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl index 68e3817cb..dbf34f13c 100644 --- a/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl +++ b/xCAT-server/share/xcat/install/rocky/compute.rocky10.riscv64.tmpl @@ -31,6 +31,15 @@ firewall --disable selinux --disable reboot +# riscv64: EL10 has no default crash kernel reservation for this architecture +# (kdumpctl get-default-crashkernel is empty), so the installer's kdump add-on +# would write the literal "crashkernel=auto", which the kernel ignores: nothing +# is reserved and kdump.service fails on every node. Leave kdump off here; a +# node that should take crash dumps gets a real reservation through +# linuximage.addkcmdline or bootparams.addkcmdline (crashkernel=). +%addon com_redhat_kdump --disable +%end + # riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the # x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist # for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 diff --git a/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl index 00364f2ac..b7c6dc41e 100644 --- a/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl +++ b/xCAT-server/share/xcat/install/rocky/service.rocky10.riscv64.tmpl @@ -31,6 +31,15 @@ firewall --disable selinux --disable reboot +# riscv64: EL10 has no default crash kernel reservation for this architecture +# (kdumpctl get-default-crashkernel is empty), so the installer's kdump add-on +# would write the literal "crashkernel=auto", which the kernel ignores: nothing +# is reserved and kdump.service fails on every node. Leave kdump off here; a +# node that should take crash dumps gets a real reservation through +# linuximage.addkcmdline or bootparams.addkcmdline (crashkernel=). +%addon com_redhat_kdump --disable +%end + # riscv64: the EL10 anaconda has no RISC-V EFI platform and always asks for the # x86 UEFI boot loader packages (grub2-efi-x64, shim-x64), which do not exist # for riscv64; tolerate that. The riscv64 package list pulls grub2-efi-riscv64 From 4314781f73b117f4764e7ff9c50e4e14b75cb7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:05:48 -0300 Subject: [PATCH 26/54] test(xCAT-test): cover the disabled kdump add-on in the riscv64 templates --- xCAT-test/unit/install_profile_riscv64.t | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/install_profile_riscv64.t b/xCAT-test/unit/install_profile_riscv64.t index ad9f6b784..3a0cc3153 100644 --- a/xCAT-test/unit/install_profile_riscv64.t +++ b/xCAT-test/unit/install_profile_riscv64.t @@ -36,6 +36,7 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { like( $t, qr/^%packages --ignoremissing$/m, "$dir/$profile.$osbase riscv64 template tolerates anaconda's x86 boot loader package request" ); like( $t, qr/^#INCLUDE_DEFAULT_PKGLIST#$/m, "$dir/$profile.$osbase riscv64 template still includes the default package list" ); like( $t, qr{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#$}m, "$dir/$profile.$osbase riscv64 template runs the riscv64 UEFI boot entry fix-up" ); + like( $t, qr/^%addon com_redhat_kdump --disable\n%end$/m, "$dir/$profile.$osbase riscv64 template keeps the installer from writing a crash kernel reservation EL10 riscv64 cannot honour" ); like( $t, qr/^%post --erroronfail --interpreter=\/bin\/bash$/m, "$dir/$profile.$osbase riscv64 template fails the install when no boot loader reached the ESP" ); like( $t, qr{/boot/efi/EFI/BOOT/BOOTRISCV64\.EFI}, "$dir/$profile.$osbase riscv64 template checks the removable-media loader" ); like( $t, qr{/boot/efi/EFI/\*/grubriscv64\.efi}, "$dir/$profile.$osbase riscv64 template also accepts a vendor directory loader" ); @@ -43,7 +44,8 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { # the riscv64 template is the shared one plus the riscv64 changes my $b = slurp($base); - ( my $t_norm = $t ) =~ s/^# riscv64:.*\n(?:#.*\n)*//m; + ( my $t_norm = $t ) =~ s/^# riscv64:.*\n(?:#.*\n)*%addon com_redhat_kdump --disable\n%end\n\n//m; + $t_norm =~ s/^# riscv64:.*\n(?:#.*\n)*//m; $t_norm =~ s/^%packages --ignoremissing$/%packages/m; $t_norm =~ s{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#\n}{}m; $t_norm =~ s/\n# --ignoremissing above.*?\n%post --erroronfail.*?\n%end\n//s; From ba352c01f659ff3f0b667db3cf7136a59658e0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:05:48 -0300 Subject: [PATCH 27/54] docs(riscv64): describe crash dumps on riscv64 nodes Explain why the riscv64 templates disable the installer's kdump add-on, how to reserve memory for crash dumps on an installed node, and what diskless images reserve by default. --- .../manage_clusters/riscv64/index.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst index f87a58381..ca16832bf 100644 --- a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst +++ b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst @@ -75,6 +75,22 @@ package lists add ``grub2-efi-riscv64`` and ``efibootmgr``, and the these files. After ``nodeset boot`` the firmware boots the installed system because the per-node ``grub2-`` loader link is removed. +Crash dumps +~~~~~~~~~~~ + +EL10 defines no default crash kernel reservation for riscv64 +(``kdumpctl get-default-crashkernel`` is empty there), so the installer's kdump +add-on writes the literal ``crashkernel=auto``, which the kernel ignores: no +memory is reserved and ``kdump.service`` fails on every installed node. The +riscv64 templates therefore turn that add-on off. To take crash dumps on a +riscv64 node, reserve the memory yourself with a real value, for example:: + + chdef -t osimage rocky10.2-riscv64-install-compute addkcmdline="crashkernel=256M" + +Diskless images use ``linuximage.dump`` and ``linuximage.crashkernelsize`` as on +the other architectures; a riscv64 image with ``dump`` set and no explicit size +reserves 256M. + Stateless (diskless) images --------------------------- From f8b9fd8489a5a2e9104b43af274bed00c4d86e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:25 -0300 Subject: [PATCH 28/54] feat(copycds): publish the grub2 boot loader of the installation media riscv64 nodes boot through UEFI and grub2 only, and xCAT builds no boot loader: /tftpboot/boot/grub2/grub2.riscv64 has to come from the xcat-dep grub2-xcat package or be copied by hand, which is a step an admin only finds out about when a node times out in firmware. The EL riscv64 media carry exactly that image as EFI/BOOT/grubriscv64.efi, so copycd publishes it after a successful media copy, and says so. An image the management node already has is never replaced, and the media of every other architecture is untouched. --- xCAT-server/lib/xcat/plugins/anaconda.pm | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/xCAT-server/lib/xcat/plugins/anaconda.pm b/xCAT-server/lib/xcat/plugins/anaconda.pm index 77d4a2ae7..0af5d40ab 100644 --- a/xCAT-server/lib/xcat/plugins/anaconda.pm +++ b/xCAT-server/lib/xcat/plugins/anaconda.pm @@ -2091,6 +2091,36 @@ erver, if so, stop it first and try again" ], } } +# EL media that carry the grub2 UEFI image of an architecture xCAT cannot build a +# boot loader for. copycd publishes it so those nodes can net boot without any +# further step; grub2-xcat installs the same image where it is available. +my %MEDIA_GRUB2_LOADERS = (riscv64 => 'grubriscv64.efi'); + +# Publish the grub2 UEFI image of the media under $tftpdir/boot/grub2, unless the +# management node already has one. Returns the path written, or undef. +sub _install_media_grub2_loader { + my ($path, $arch, $callback) = @_; + + my $image = $MEDIA_GRUB2_LOADERS{$arch}; + return unless $image; + my $source = "$path/EFI/BOOT/$image"; + return unless -r $source; + + my $tftpdir = xCAT::TableUtils->getTftpDir(); + return unless $tftpdir; + my $target = "$tftpdir/boot/grub2/grub2.$arch"; + return if -e $target; + + mkpath("$tftpdir/boot/grub2"); + unless (copy($source, $target)) { + $callback->({ data => "Could not install $target from the media: $!" }) if $callback; + return; + } + chmod 0644, $target; + $callback->({ data => "Installed $target from the media" }) if $callback; + return $target; +} + sub copycd { my $request = shift; @@ -2488,6 +2518,7 @@ sub copycd else { $callback->({ data => "Media copy operation successful" }); + _install_media_grub2_loader($path, $arch, $callback); my @ret = xCAT::SvrUtils->update_osdistro_table($distname, $arch, $path, $osdistroname); if ($ret[0] != 0) { $callback->({ data => "Error when updating the osdistro tables: " . $ret[1] }); From 600a87713e86606580edba09e7d78f18418b439e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:26 -0300 Subject: [PATCH 29/54] test(xCAT-test): cover the boot loader published by copycds --- xCAT-test/unit/copycd_grub2_loader.t | 112 +++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 xCAT-test/unit/copycd_grub2_loader.t diff --git a/xCAT-test/unit/copycd_grub2_loader.t b/xCAT-test/unit/copycd_grub2_loader.t new file mode 100644 index 000000000..5d6680ba5 --- /dev/null +++ b/xCAT-test/unit/copycd_grub2_loader.t @@ -0,0 +1,112 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use Cwd qw(realpath); +use File::Path qw(make_path); +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# copycd publishes the grub2 UEFI image of the installation media so that nodes +# of an architecture xCAT builds no boot loader for -- riscv64 -- can net boot +# without a further step. An image the management node already has is never +# replaced, and the media of every other architecture is left alone. + +my $repo_root = realpath( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'anaconda.pm' ); +plan skip_all => "$plugin not found" unless -r $plugin; + +my $source = do { + open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; + local $/; + <$fh>; +}; + +# anaconda.pm needs a database to load, so take just the loader publication out +# of it, the way the other unit tests here isolate a shipped sub. +my ($loaders) = $source =~ /^(my %MEDIA_GRUB2_LOADERS = \([^)]*\);)$/m; +ok( $loaders, 'the media boot loader map was located in anaconda.pm' ) + or BAIL_OUT('anaconda.pm no longer defines %MEDIA_GRUB2_LOADERS'); +my ($sub) = $source =~ /^(sub _install_media_grub2_loader \{.*?^\})$/ms; +ok( $sub, 'the media boot loader publication was located in anaconda.pm' ) + or BAIL_OUT('anaconda.pm no longer defines _install_media_grub2_loader'); + +my $tftpdir; +{ + package xCAT::TableUtils; + sub getTftpDir { return $tftpdir; } +} + +eval "use File::Path qw(mkpath); use File::Copy; $loaders $sub 1;" or die $@; + +sub media { + my ( $root, $name, $image ) = @_; + my $path = File::Spec->catdir( $root, $name ); + make_path( File::Spec->catdir( $path, 'EFI', 'BOOT' ) ); + if ($image) { + my $file = File::Spec->catfile( $path, 'EFI', 'BOOT', $image ); + open( my $fh, '>', $file ) or die "Unable to create $file: $!"; + print $fh "boot loader from the media\n"; + close($fh); + } + return $path; +} + +sub messages { + my ($responses) = @_; + return join( ' ', map { ref($_) eq 'HASH' && $_->{data} ? "@{[ $_->{data} ]}" : () } @{$responses} ); +} + +my $root = tempdir( CLEANUP => 1 ); +$tftpdir = File::Spec->catdir( $root, 'tftpboot' ); +make_path($tftpdir); + +# riscv64 media, nothing published yet: the image is installed +my $riscv_media = media( $root, 'rocky10-riscv64', 'grubriscv64.efi' ); +my @responses; +my $written = _install_media_grub2_loader( $riscv_media, 'riscv64', sub { push @responses, @_; } ); +my $loader = File::Spec->catfile( $tftpdir, 'boot', 'grub2', 'grub2.riscv64' ); +is( $written, $loader, 'riscv64 media publish their grub2 image as the riscv64 boot loader' ); +ok( -f $loader, 'the boot loader is written under the TFTP root' ); +is( ( stat($loader) )[7], ( stat( File::Spec->catfile( $riscv_media, 'EFI', 'BOOT', 'grubriscv64.efi' ) ) )[7], + 'the published image is the one from the media' ); +like( messages( \@responses ), qr/\QInstalled $loader from the media\E/, 'the published boot loader is reported' ); + +# a boot loader that is already there is never replaced +open( my $fh, '>', $loader ) or die "Unable to rewrite $loader: $!"; +print $fh "installed by grub2-xcat\n"; +close($fh); +@responses = (); +is( scalar _install_media_grub2_loader( $riscv_media, 'riscv64', sub { push @responses, @_; } ), + undef, 'an existing boot loader is kept' ); +open( my $rfh, '<', $loader ) or die "Unable to read $loader: $!"; +my $content = do { local $/; <$rfh> }; +close($rfh); +is( $content, "installed by grub2-xcat\n", 'the existing boot loader is left untouched' ); +is( messages( \@responses ), '', 'nothing is reported when there is nothing to do' ); +unlink($loader); + +# other architectures are not touched, even when their media carry an image. +# Start from an empty TFTP root so the check covers the directory as well. +$tftpdir = File::Spec->catdir( $root, 'tftpboot-x86' ); +make_path($tftpdir); +my $x86_media = media( $root, 'rocky10-x86_64', 'grubx64.efi' ); +@responses = (); +is( scalar _install_media_grub2_loader( $x86_media, 'x86_64', sub { push @responses, @_; } ), + undef, 'x86_64 media publish no boot loader' ); +ok( !-e File::Spec->catfile( $tftpdir, 'boot', 'grub2', 'grub2.x86_64' ), 'no x86_64 boot loader is written' ); +ok( !-d File::Spec->catdir( $tftpdir, 'boot' ), 'the grub2 directory is only created when there is an image to put in it' ); + +# riscv64 media without the image are a no-op rather than an error +$tftpdir = File::Spec->catdir( $root, 'tftpboot-bare' ); +make_path($tftpdir); +my $bare_media = media( $root, 'rocky10-riscv64-bare' ); +@responses = (); +is( scalar _install_media_grub2_loader( $bare_media, 'riscv64', sub { push @responses, @_; } ), + undef, 'media without a grub2 image publish nothing' ); +is( messages( \@responses ), '', 'media without a grub2 image report nothing' ); +ok( !-d File::Spec->catdir( $tftpdir, 'boot' ), 'media without a grub2 image create no directories' ); + +done_testing(); From f9457753171b0d989ae07d4174f0b17dd7ce4e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:26 -0300 Subject: [PATCH 30/54] feat(mknb): fetch the Genesis discovery payload over HTTP The grub2 discovery configuration loaded the Genesis kernel and initramfs over TFTP, a lockstep protocol that acknowledges every block and runs one server process per client. Fetching the same 79 MiB Genesis image from a node on the lab network took 61.5 s over TFTP and 1.2 s over HTTP, and a whole cluster discovering at once queues on the TFTP server. Write two entries instead. The default one sets root to the HTTP server of the management node on that network and loads the same files from below the TFTP root, the way nodeset does for netboot=grub2-http; the second keeps the TFTP paths for a management node that does not serve the TFTP root over HTTP, and "set fallback=1" moves to it when GRUB cannot fetch the payload over HTTP. site.httpport is honoured. --- xCAT-server/lib/xcat/plugins/mknb.pm | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index 94702fe66..b9f01b633 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -841,11 +841,12 @@ sub process_request { hexnet => $_, xcatd_address => $xcatd_address, xcatdport => $xcatdport, + httpport => $httpport, consolecmdline => $consolecmdline, ); } } - if (exists $GRUB2_DISCOVERY_ARCHES{$arch} and !-e "$tftpdir/boot/grub2/grub2.$arch") { + if (exists $GRUB2_DISCOVERY_ARCHES{$arch} && !-e "$tftpdir/boot/grub2/grub2.$arch") { # The discovery configurations are only reached through this boot loader, which # xCAT does not build: say so instead of letting the nodes time out in firmware. $callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (it is installed by grub2-xcat, or copied from the EL $arch installation media)"] }); @@ -879,7 +880,8 @@ sub _grub2_discovery_arches { # then grub.cfg-<8 hex digit ip>, then ever shorter prefixes of that ip, and # finally grub.cfg. nodeset writes the per-node files (full ip and mac), so a # per-network prefix is only reached by clients without a node configuration: -# discovery. The file is rebuilt from the Genesis artifacts present under +# discovery. The payload is fetched over HTTP, with a TFTP entry behind it. +# The file is rebuilt from the Genesis artifacts present under # $tftpdir/xcat and removed when none are left. Returns the path written. sub _write_grub2_discovery_config { my (%args) = @_; @@ -895,14 +897,33 @@ sub _write_grub2_discovery_config { if (defined($args{consolecmdline}) and $args{consolecmdline} ne '') { $cmdline .= " $args{consolecmdline}"; } + # The Genesis initramfs is large and a TFTP server hands it to one client at a + # time, so the default entry fetches it over HTTP, the same way nodeset does for + # netboot=grub2-http. The TFTP entry stays as the second choice for a management + # node that does not serve the TFTP root over HTTP. + my $httpport = $args{httpport} || '80'; + my $httproot = 'http,' . $args{xcatd_address} . ($httpport eq '80' ? '' : ":$httpport"); + my $tftproot = 'tftp,' . $args{xcatd_address}; my $content = "# xCAT Genesis discovery for network $hexnet - generated by mknb, do not edit\n"; $content .= "set default=0\n"; + # Every architecture branch defines the HTTP entry first and the TFTP entry + # second, so a payload GRUB cannot fetch over HTTP is retried over TFTP. + $content .= "set fallback=1\n"; $content .= "set timeout=5\n"; my $keyword = 'if'; foreach my $entry (@arches) { my ($arch, $grub_cpu, $kernel, $initrd) = @{$entry}; $content .= "$keyword [ \"\$grub_cpu\" = \"$grub_cpu\" ]; then\n"; $content .= "menuentry \"xCAT Genesis $arch\" {\n"; + $content .= " insmod http\n"; + $content .= " insmod tftp\n"; + $content .= " set root=$httproot\n"; + $content .= " linux $tftpdir/$kernel $cmdline BOOTIF=\$net_default_mac\n"; + $content .= " initrd $tftpdir/$initrd\n"; + $content .= "}\n"; + $content .= "menuentry \"xCAT Genesis $arch (TFTP)\" {\n"; + $content .= " insmod tftp\n"; + $content .= " set root=$tftproot\n"; $content .= " linux /$kernel $cmdline BOOTIF=\$net_default_mac\n"; $content .= " initrd /$initrd\n"; $content .= "}\n"; @@ -912,8 +933,7 @@ sub _write_grub2_discovery_config { # nodeset hard-links grub.cfg-<8 hex digit ip> to the node file; on a /32 network that is # this file's name, so replace the name instead of truncating a shared inode. unlink($cfgpath); - my $cfg; - open($cfg, ">", $cfgpath) or return; + open(my $cfg, ">", $cfgpath) or return; print $cfg $content; close($cfg); chmod(0644, $cfgpath); From 609f6febef4b933362b85d586461b9fc1f032a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:26 -0300 Subject: [PATCH 31/54] test(xCAT-test): cover the HTTP Genesis discovery payload --- xCAT-test/unit/mknb_xcatd_address.t | 45 ++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t index b20e335fd..8cc92dd57 100644 --- a/xCAT-test/unit/mknb_xcatd_address.t +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -356,6 +356,7 @@ like( ); like($grub_cfg, qr/^set default=0$/m, 'the grub2 discovery configuration selects the first entry'); like($grub_cfg, qr/^set timeout=5$/m, 'the grub2 discovery configuration boots after a short timeout'); +like($grub_cfg, qr/^set fallback=1$/m, 'a payload that cannot be fetched over HTTP falls back to the TFTP entry'); like( $grub_cfg, qr/^if \[ "\$grub_cpu" = "riscv64" \]; then$/m, @@ -364,16 +365,28 @@ like( like($grub_cfg, qr/^menuentry "xCAT Genesis riscv64" \{$/m, 'the menu entry names the architecture'); like( $grub_cfg, - qr/^ linux \/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3001 BOOTIF=\$net_default_mac$/m, + qr/^ linux \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3001 BOOTIF=\$net_default_mac$/m, 'the kernel line loads the Genesis kernel with the xcatd endpoint and the booting MAC', ); -like($grub_cfg, qr/^ initrd \/xcat\/genesis\.fs\.riscv64\.gz$/m, 'the initrd line loads the Genesis initramfs'); -like($grub_cfg, qr/^\}\nfi\n\z/m, 'the configuration closes the menu entry and the cpu guard'); -unlike( +like( $grub_cfg, - qr/\Q$xCAT::TableUtils::tftpdir\E/, - 'the grub2 configuration does not expose the configured TFTP root', + qr/^ initrd \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.fs\.riscv64\.gz$/m, + 'the initrd line loads the Genesis initramfs', ); + +# the Genesis payload is large and TFTP serves one client at a time, so the default +# entry fetches it over HTTP and the TFTP entry stays behind it +like($grub_cfg, qr/^ set root=http,192\.168\.148\.10$/m, 'the default entry fetches the payload over HTTP'); +like($grub_cfg, qr/^ insmod http$/m, 'the HTTP entry loads the grub2 http module'); +my ($first_entry) = $grub_cfg =~ /^(menuentry .*?^\})/ms; +like($first_entry || '', qr/set root=http,/, 'the first, default entry is the HTTP one'); +like($grub_cfg, qr/^menuentry "xCAT Genesis riscv64 \(TFTP\)" \{$/m, 'a TFTP entry follows it'); +like( + $grub_cfg, + qr/^menuentry "xCAT Genesis riscv64 \(TFTP\)" \{\n insmod tftp\n set root=tftp,192\.168\.148\.10\n linux \/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3001 BOOTIF=\$net_default_mac\n initrd \/xcat\/genesis\.fs\.riscv64\.gz\n\}$/m, + 'the TFTP entry keeps the paths relative to the TFTP root', +); +like($grub_cfg, qr/^\}\nfi\n\z/m, 'the configuration closes the menu entries and the cpu guard'); unlike( $grub_cfg, qr/xcatd=192\.168\.149\.100:3001/, @@ -400,11 +413,27 @@ $responses = run_mknb('riscv64'); generation_succeeded($responses, 'riscv64 configuration generation succeeds with a serial console'); like( read_config($grub_cfg_path), - qr/^ linux \/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3002 console=tty0 console=ttyS0,115200n8r BOOTIF=\$net_default_mac$/m, + qr/^ linux \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3002 console=tty0 console=ttyS0,115200n8r BOOTIF=\$net_default_mac$/m, 'the serial console and a non-default xcatd port are carried into the kernel line', ); %xCAT::TableUtils::site_extra = (); +# a non-default site.httpport reaches the HTTP entry +%xCAT::TableUtils::site_extra = ( httpport => '8080' ); +$responses = run_mknb('riscv64'); +generation_succeeded($responses, 'riscv64 configuration generation succeeds with a non-default HTTP port'); +like( + read_config($grub_cfg_path), + qr/^ set root=http,192\.168\.148\.10:8080$/m, + 'the HTTP entry uses the configured HTTP port', +); +like( + read_config($grub_cfg_path), + qr/^ set root=tftp,192\.168\.148\.10$/m, + 'the TFTP entry is unaffected by the HTTP port', +); +%xCAT::TableUtils::site_extra = (); + # an lzma initramfs is preferred when mknb produced one open(my $lzma_fh, '>', "$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma") or die "Unable to create lzma: $!"; close($lzma_fh); @@ -412,7 +441,7 @@ $responses = run_mknb('riscv64'); generation_succeeded($responses, 'riscv64 configuration generation succeeds with an lzma initramfs'); like( read_config($grub_cfg_path), - qr/^ initrd \/xcat\/genesis\.fs\.riscv64\.lzma$/m, + qr/^ initrd \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.fs\.riscv64\.lzma$/m, 'the lzma Genesis initramfs is preferred over the gzip one', ); unlink("$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma"); From f491be35d32dd4df041774fd03ac810d56c08e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:26 -0300 Subject: [PATCH 32/54] fix(dhcp): render the Kea option flags as booleans The option flags of a client class -- always-send and its siblings -- were passed through as whatever the caller set, so a plain Perl 1 reached the configuration as the number 1 and Kea refuses to parse that. The class flag next to it is already normalised; do the same for the option data, so callers can stay free of JSON. --- perl-xCAT/xCAT/DHCP/Backend/Kea.pm | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm index 23524afaa..b6a48cc06 100644 --- a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm +++ b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm @@ -809,10 +809,35 @@ sub _render_client_class { delete $rendered{'only-if-required'}; $rendered{ $self->_additional_class_flag_field() } = _json_bool($additional_only) if defined $additional_only; + $rendered{'option-data'} = _render_option_data( $rendered{'option-data'} ) + if $rendered{'option-data'}; return \%rendered; } +# Kea reads the option flags as booleans, so the callers can set them the plain +# Perl way and still produce a configuration the server parses. +sub _render_option_data { + my ($option_data) = @_; + + return $option_data unless ref($option_data) eq 'ARRAY'; + + my @rendered; + foreach my $option (@$option_data) { + if ( ref($option) ne 'HASH' ) { + push @rendered, $option; + next; + } + my %copy = %$option; + foreach my $flag (qw/always-send csv-format never-send/) { + $copy{$flag} = _json_bool( $copy{$flag} ) if defined $copy{$flag}; + } + push @rendered, \%copy; + } + + return \@rendered; +} + sub _render_subnet6 { my ( $self, $subnet ) = @_; From 5d1010bc9775331808ace60ecd1b9d16a0567046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:26 -0300 Subject: [PATCH 33/54] feat(dhcp): boot RISC-V UEFI HTTP clients Firmware configured for UEFI HTTP boot sends client architecture 28 (0x001c) and only accepts an offer whose boot file is a URL and whose reply carries the HTTPClient vendor class; it ignores the PXE offer that answers architecture 27. riscv64 nodes therefore could not boot at all from firmware set up that way. Answer them with the same grub2 image over HTTP. The URL has to name the management node address of the network the request came in on, so the class belongs to the subnet, like the POWER OPAL and xNBA network classes next to it, and it is only offered while the boot loader is actually published. The ISC backend renders the matching subnet branch. This covers every client without a reservation, which is the discovery case. A node that nodeset has configured keeps its per-node boot loader over TFTP, as on the other architectures. --- perl-xCAT/xCAT/DHCP/BootPolicy.pm | 46 ++++++++++++++++++++++++++++ xCAT-server/lib/xcat/plugins/dhcp.pm | 14 +++++++++ 2 files changed, 60 insertions(+) diff --git a/perl-xCAT/xCAT/DHCP/BootPolicy.pm b/perl-xCAT/xCAT/DHCP/BootPolicy.pm index 98f1f0b62..c574df125 100644 --- a/perl-xCAT/xCAT/DHCP/BootPolicy.pm +++ b/perl-xCAT/xCAT/DHCP/BootPolicy.pm @@ -56,6 +56,52 @@ sub kea_client_classes { return \@classes; } +# Architectures whose UEFI firmware can also boot over HTTP, by DHCP client +# architecture id (RFC 4578 and the IANA registry). An HTTP boot client wants the +# boot file as a URL and only accepts the offer when the reply is tagged +# HTTPClient; the image it downloads is the same grub2 the TFTP path hands out. +my %HTTP_BOOT_ARCHES = ( + riscv64 => { arch_id => '0x001c', loader => 'boot/grub2/grub2.riscv64' }, +); + +# The HTTP boot classes of one network. They carry the address of the management +# node on that network, so they belong to the subnet rather than to the global +# list, like the other network classes here. +sub kea_httpboot_network_classes { + my ( $class, %opts ) = @_; + + return [] unless $opts{net} && defined( $opts{prefix} ) && $opts{next_server}; + + my $httpport = $opts{httpport} || '80'; + my $portsuffix = ( $httpport eq '80' ) ? '' : ":$httpport"; + my $tftpdir = $opts{tftpdir} || '/tftpboot'; + $tftpdir =~ s{/+$}{}; + my $present = $opts{loader_present}; + my @classes; + + foreach my $arch ( sort keys %HTTP_BOOT_ARCHES ) { + my $spec = $HTTP_BOOT_ARCHES{$arch}; + next if $present && !$present->("$tftpdir/$spec->{loader}"); + my $name = "xcat-$arch-http-$opts{net}_$opts{prefix}"; + $name =~ s/[^A-Za-z0-9_.-]/_/g; + push @classes, { + name => $name, + test => "option[93].hex == $spec->{arch_id}", + additional_only => 1, + 'boot-file-name' => "http://$opts{next_server}$portsuffix$tftpdir/$spec->{loader}", + 'option-data' => [ + { + name => 'vendor-class-identifier', + data => 'HTTPClient', + 'always-send' => 1, + }, + ], + }; + } + + return \@classes; +} + sub kea_xnba_node_classes { my ( $class, %opts ) = @_; diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 1505f4610..e6691730e 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -3289,6 +3289,16 @@ sub kea_subnet4_intent $subnet{interface} = $interface unless $remote; my @client_classes = @$xnba_classes; push @client_classes, $opal_class if $opal_class; + push @client_classes, @{ + xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( + net => $net, + prefix => $prefix, + next_server => $tftp, + httpport => $httpport, + tftpdir => $tftpdir, + loader_present => sub { -e $_[0] }, + ) + }; if (@client_classes) { $subnet{additional_client_classes} = [ map { $_->{name} } @client_classes ]; $subnet{client_classes} = \@client_classes; @@ -4470,6 +4480,10 @@ sub addnet push @netent, " } else if option client-architecture = 00:1b { #riscv64 uefi\n "; push @netent, " filename \"boot/grub2/grub2.riscv64\";\n"; + push @netent, + " } else if option client-architecture = 00:1c { #riscv64 uefi http boot\n "; + push @netent, " option vendor-class-identifier \"HTTPClient\";\n"; + push @netent, " filename \"http://$tftp$portsuffix$tftpdir/boot/grub2/grub2.riscv64\";\n"; push @netent, " } else if option client-architecture = 00:0e { #OPAL-v3\n "; push @netent, " option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/" . $net . "_" . $maskbits . "\";\n"; From aade148d49df7b04255b4aa9bd1c6936f4142f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:27 -0300 Subject: [PATCH 34/54] test(xCAT-test): cover RISC-V UEFI HTTP boot in both DHCP backends --- xCAT-test/unit/dhcp_boot_policy.t | 72 +++++++++++++++++++++++++++ xCAT-test/unit/dhcp_isc_client_arch.t | 19 ++++++- xCAT-test/unit/dhcp_kea_renderer.t | 40 +++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/dhcp_boot_policy.t b/xCAT-test/unit/dhcp_boot_policy.t index d7021bc21..f73f78d91 100644 --- a/xCAT-test/unit/dhcp_boot_policy.t +++ b/xCAT-test/unit/dhcp_boot_policy.t @@ -145,4 +145,76 @@ is_deeply( 'xNBA network policy requires a next server' ); +# UEFI HTTP boot: firmware that boots over HTTP sends architecture id 28 and only +# accepts an offer whose boot file is a URL and whose reply is tagged HTTPClient. +my $httpboot = xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( + net => '10.0.0.0', + prefix => 24, + next_server => '10.0.0.1', + tftpdir => '/tftpboot', +); +is_deeply( + $httpboot, + [ + { + name => 'xcat-riscv64-http-10.0.0.0_24', + test => 'option[93].hex == 0x001c', + additional_only => 1, + 'boot-file-name' => 'http://10.0.0.1/tftpboot/boot/grub2/grub2.riscv64', + 'option-data' => [ + { + name => 'vendor-class-identifier', + data => 'HTTPClient', + 'always-send' => 1, + }, + ], + }, + ], + 'RISC-V HTTP boot clients are offered the boot loader as a URL, tagged HTTPClient' +); + +my $httpboot_port = xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( + net => '10.0.0.0', + prefix => 24, + next_server => '10.0.0.1', + httpport => '8080', + tftpdir => '/srv/tftpboot', +); +is( + $httpboot_port->[0]{'boot-file-name'}, + 'http://10.0.0.1:8080/srv/tftpboot/boot/grub2/grub2.riscv64', + 'the HTTP boot URL follows the configured HTTP port and TFTP root' +); + +is_deeply( + xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( + net => '10.0.0.0', + prefix => 24, + next_server => '10.0.0.1', + loader_present => sub { 0 }, + ), + [], + 'no HTTP boot class is offered while the boot loader is missing' +); +is_deeply( + xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( net => '10.0.0.0', prefix => 24 ), + [], + 'HTTP boot classes need a next server' +); +is( + scalar @{ xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( + net => '10.0.0.0', + prefix => 24, + next_server => '10.0.0.1', + loader_present => sub { $_[0] eq '/tftpboot/boot/grub2/grub2.riscv64' }, + ) }, + 1, + 'the boot loader of the architecture is what is looked for' +); +unlike( + join( ' ', map { $_->{test} } @$classes ), + qr/0x001c/, + 'the global class list keeps HTTP boot out: it needs the address of the management node', +); + done_testing(); diff --git a/xCAT-test/unit/dhcp_isc_client_arch.t b/xCAT-test/unit/dhcp_isc_client_arch.t index d65e33a94..d8a22aac2 100644 --- a/xCAT-test/unit/dhcp_isc_client_arch.t +++ b/xCAT-test/unit/dhcp_isc_client_arch.t @@ -42,7 +42,24 @@ SKIP: { cmp_ok( $pos{riscv64}, '<', $pos{yaboot}, 'riscv64 is rendered before the /yaboot fallback, so it is reachable' ); } +# HTTP boot firmware (architecture id 28) needs the boot file as a URL and only +# accepts an offer that is tagged HTTPClient +ok( + $source =~ /client-architecture = 00:1c \{[^\n]*\n\s*push \@netent, "\s*option vendor-class-identifier \\"HTTPClient\\";[^\n]*\n\s*push \@netent, "\s*filename \\"http:\/\/\$tftp\$portsuffix\$tftpdir\/boot\/grub2\/grub2\.riscv64\\";/, + 'the ISC subnet block hands RISC-V HTTP boot clients the boot loader as a URL', +); +my $httpboot_pos = $-[0]; +SKIP: { + skip 'the riscv64 branches were not both found', 2 unless defined $pos{riscv64} && defined $httpboot_pos; + cmp_ok( $pos{riscv64}, '<', $httpboot_pos, 'the PXE branch keeps its place before the HTTP boot branch' ); + cmp_ok( $httpboot_pos, '<', $pos{yaboot}, 'the HTTP boot branch is rendered before the /yaboot fallback' ); +} + my @riscv_ids = $source =~ /client-architecture = (00:1[9a-e])/g; -is_deeply( \@riscv_ids, ['00:1b'], 'only the RISC-V 64-bit UEFI architecture id (27) is mapped' ); +is_deeply( + [ sort @riscv_ids ], + [ '00:1b', '00:1c' ], + 'only the RISC-V 64-bit UEFI architecture ids (27 and 28) are mapped', +); done_testing(); diff --git a/xCAT-test/unit/dhcp_kea_renderer.t b/xCAT-test/unit/dhcp_kea_renderer.t index af20211d5..e9b31c708 100644 --- a/xCAT-test/unit/dhcp_kea_renderer.t +++ b/xCAT-test/unit/dhcp_kea_renderer.t @@ -87,12 +87,39 @@ my $json = $backend->render_dhcp4_config( { name => 'conf-file', data => 'http://10.0.0.1/tftpboot/pxelinux.cfg/p/10.0.0.0_24' }, ], }, + { + name => 'xcat-riscv64-http-10.0.0.0_24', + test => 'option[93].hex == 0x001c', + additional_only => 1, + 'boot-file-name' => 'http://10.0.0.1/tftpboot/boot/grub2/grub2.riscv64', + 'option-data' => [ + { + name => 'vendor-class-identifier', + data => 'HTTPClient', + 'always-send' => 1, + }, + ], + }, ], } ); my $config = decode_json($json); ok( $config->{Dhcp4}, 'renderer creates a Dhcp4 document' ); + +# Kea reads the option flags as booleans, so a caller that sets them the plain +# Perl way still produces a configuration the server parses. +my ($httpboot_class) = + grep { $_->{name} eq 'xcat-riscv64-http-10.0.0.0_24' } @{ $config->{Dhcp4}{'client-classes'} || [] }; +ok( $httpboot_class, 'a client class carrying option data is rendered' ); +is( + ref( $httpboot_class->{'option-data'}[0]{'always-send'} ), + 'JSON::PP::Boolean', + 'the always-send option flag is rendered as a boolean, not as a number', +); +ok( $httpboot_class->{'option-data'}[0]{'always-send'}, 'and it is true' ); +is( $httpboot_class->{'option-data'}[0]{data}, 'HTTPClient', 'the option value is left alone' ); +is( $httpboot_class->{'boot-file-name'}, 'http://10.0.0.1/tftpboot/boot/grub2/grub2.riscv64', 'the boot file URL is left alone' ); is_deeply( $config->{Dhcp4}{'interfaces-config'}{interfaces}, ['eth0'], 'interfaces are rendered' ); is( $config->{Dhcp4}{'valid-lifetime'}, 600, 'valid lifetime is rendered' ); is( $config->{Dhcp4}{'lease-database'}{type}, 'memfile', 'memfile lease backend is the default' ); @@ -158,6 +185,19 @@ is_deeply( { name => 'conf-file', data => 'http://10.0.0.1/tftpboot/pxelinux.cfg/p/10.0.0.0_24' }, ], }, + { + name => 'xcat-riscv64-http-10.0.0.0_24', + test => 'option[93].hex == 0x001c', + 'only-if-required' => JSON::true, + 'boot-file-name' => 'http://10.0.0.1/tftpboot/boot/grub2/grub2.riscv64', + 'option-data' => [ + { + name => 'vendor-class-identifier', + data => 'HTTPClient', + 'always-send' => JSON::true, + }, + ], + }, ], 'client classes are preserved, including subnet-specific OPAL conf-file class' ); From 040283f7a9fd208796ed63317d0440831cbd417d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:55:27 -0300 Subject: [PATCH 35/54] docs(riscv64): describe HTTP boot, the HTTP discovery payload and the media boot loader --- .../code/dhcp_backend_validation_matrix.rst | 9 +++++++ .../manage_clusters/riscv64/index.rst | 27 ++++++++++++++----- .../guides/install-guides/yum/grub2.rst | 2 +- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst b/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst index 5b3515d4d..dbcf15339 100644 --- a/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst +++ b/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst @@ -242,6 +242,15 @@ paths. - same as the Kea row through the ISC subnet branch for client architecture ``00:1b``; the riscv64 branch must answer before the ``/yaboot`` fallback + * - EL10 + - ``riscv64`` + - ``Kea``, ``ISC`` + - ``UEFI HTTP boot`` + - firmware configured for HTTP boot sends client architecture 28 + (``0x001c``) and only accepts an offer whose boot file is a URL and + whose reply carries the ``HTTPClient`` vendor class; the class is per + subnet, so the URL must name the management node address of that + network and honour ``site.httpport`` Current Lab Baseline -------------------- diff --git a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst index ca16832bf..faa4ce191 100644 --- a/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst +++ b/docs/source/guides/admin-guides/manage_clusters/riscv64/index.rst @@ -15,21 +15,24 @@ What riscv64 nodes need sends DHCP option 93 (client system architecture) value 27 (``0x001b``) and xCAT answers with the boot file ``boot/grub2/grub2.riscv64``, from both the Kea and the ISC DHCP backends. + Firmware configured for UEFI HTTP boot (client architecture 28, ``0x001c``) + is served the same image over HTTP. * ``noderes.netboot`` is one of ``grub2``, ``grub2-tftp`` or ``grub2-http``. ``grub2-http`` is recommended for installers, whose initrd is large. * ``nodetype.arch`` and ``osimage.osarch`` are ``riscv64``. No alias is needed: ``uname -m``, rpm and dpkg all use the same token. * ``/tftpboot/boot/grub2/grub2.riscv64``: the EL grub2 UEFI image for riscv64 - (the ``EFI/BOOT/grubriscv64.efi`` of the EL10 riscv64 BaseOS tree). It is - provided by the ``grub2-xcat`` package where available; otherwise copy it as - described in :doc:`/guides/install-guides/yum/grub2`. + (the ``EFI/BOOT/grubriscv64.efi`` of the EL10 riscv64 BaseOS tree). + ``copycds`` publishes it from the installation media when the management node + does not have it yet, the ``grub2-xcat`` package installs the same image, and + :doc:`/guides/install-guides/yum/grub2` describes copying it by hand. An image + that is already there is never replaced. * The riscv64 Genesis image (``xCAT-genesis-openembedded-riscv64``) for discovery, BMC setup and flashing. Its kernel is loaded by grub2 through the EFI stub. ``go-xcat`` installs the package; on a management node built another way, install it explicitly (``dnf install xCAT-genesis-openembedded-riscv64``), the same way the images of other architectures are installed for a mixed cluster. ``xcatconfig`` runs ``mknb riscv64`` for every installed image. - The management node itself is x86_64 (the validated combination, see :doc:`/advanced/mixed_cluster/support_matrix`) or riscv64 (see below); riscv64 nodes are managed like any other mixed-architecture cluster. @@ -45,9 +48,15 @@ network, ``/tftpboot/boot/grub2/grub.cfg-``. A net booted ip>``, then shorter prefixes of that ip; the per-node files written by ``nodeset`` therefore take priority and the network file is only used by nodes that have no configuration yet, which is exactly the discovery case. +Each configuration holds two entries. The default one fetches the Genesis kernel +and initramfs over HTTP, because a TFTP server hands the image to one client at a +time and the initramfs is tens of megabytes; the second entry loads the same +files over TFTP and is there for a management node that does not serve the TFTP +root over HTTP. ``site.httpport`` is honoured. ``mknb`` runs automatically when the riscv64 Genesis packages are installed or updated; run ``mknb riscv64`` yourself after changing ``site.master``, -``site.dhcpinterfaces`` or the serial console settings. +``site.dhcpinterfaces`` or the serial console settings. It also warns when +``grub2.riscv64`` is missing, since nothing would reach these configurations. Discovered riscv64 nodes get ``nodetype.arch=riscv64`` and, when no compatible method is set, ``noderes.netboot=grub2``. Use the discovery procedures documented @@ -136,8 +145,12 @@ xCAT does not install anything from CPAN; every dependency is an rpm. Limitations ----------- -* UEFI HTTP boot (client architecture 28, ``0x001c``) is not configured yet; - use PXE (TFTP) to load ``grub2.riscv64`` and ``grub2-http`` for the payload. +* UEFI HTTP boot (client architecture 28, ``0x001c``): a client without a + reservation is offered the boot loader as a URL with the ``HTTPClient`` vendor + class, which is what such firmware requires. It has not been exercised against + HTTP boot firmware yet, and a node that ``nodeset`` has configured is offered + its per-node boot loader over TFTP, as on the other architectures, so keep PXE + boot enabled in the firmware. * Ubuntu riscv64 is not supported yet. * The serial console defaults to ``ttyS``; boards whose firmware exposes the console on another device need diff --git a/docs/source/guides/install-guides/yum/grub2.rst b/docs/source/guides/install-guides/yum/grub2.rst index ff12e1539..2f3d6ff1e 100644 --- a/docs/source/guides/install-guides/yum/grub2.rst +++ b/docs/source/guides/install-guides/yum/grub2.rst @@ -4,7 +4,7 @@ grub2 support for x86_64, aarch64 and riscv64 xCAT 2.17 enables grub2 boot support for x86_64 and aarch64 but does not ship the necessary grub2 binaries for those architectures. If you want to use grub2 for x86_64 or aarch64 you need to download the binaries from some EL OS repository. -riscv64 nodes can only boot through UEFI firmware and grub2, so they always need ``grub2.riscv64``. Recent ``grub2-xcat`` packages from xcat-dep install it; if the package on your management node does not (``ls /tftpboot/boot/grub2/grub2.riscv64``), copy the binary by hand as described below. +riscv64 nodes can only boot through UEFI firmware and grub2, so they always need ``grub2.riscv64``. ``copycds`` publishes it from the riscv64 installation media, and recent ``grub2-xcat`` packages from xcat-dep install the same image; if your management node has neither (``ls /tftpboot/boot/grub2/grub2.riscv64``), copy the binary by hand as described below. #. Download files from an BaseOS EL repository mirror From 6df52f9e8ba6f9a93bdfbccfdb5c088d431a0c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 21 Aug 2026 03:27:12 -0300 Subject: [PATCH 36/54] style(mknb): keep the riscv64 comments to the local facts Five comments in the discovery path explained a consequence chain or justified the code to a reviewer rather than stating what is not visible locally: the payload protocol the code below already shows, a rationale for the missing boot loader message, the two Genesis compression variants, the HTTP and TFTP entry pair, and the :noboot divergence from the PXELINUX files. Say the non-obvious part once. Comments only; no executable line changes. --- xCAT-server/lib/xcat/plugins/mknb.pm | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index b9f01b633..c4b77db11 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -828,10 +828,8 @@ sub process_request { print $cfgfile ' append "xcatd=' . $xcatd_address . ":$xcatdport $consolecmdline\"\n"; close($cfgfile); } elsif (exists $GRUB2_DISCOVERY_ARCHES{$arch}) { - # Unlike the PXELINUX files, which only look at the legacy address of the network, - # the discovery configuration is also dropped when the xcatd address chosen for the - # network sits on a :noboot interface: it would send Genesis to an interface that - # must not serve booting. + # Also drop it when the xcatd address chosen for the network sits on a + # :noboot interface; the PXELINUX files only check the legacy address. if (defined($nobootnicips{ $hexnets->{$_} }) or defined($nobootnicips{$xcatd_address})) { unlink("$tftpdir/boot/grub2/grub.cfg-" . uc($_)); next; @@ -847,8 +845,8 @@ sub process_request { } } if (exists $GRUB2_DISCOVERY_ARCHES{$arch} && !-e "$tftpdir/boot/grub2/grub2.$arch") { - # The discovery configurations are only reached through this boot loader, which - # xCAT does not build: say so instead of letting the nodes time out in firmware. + # These configurations are only reachable through grub2., which xCAT + # does not build. $callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (it is installed by grub2-xcat, or copied from the EL $arch installation media)"] }); } if ($configfileonly) { @@ -880,8 +878,7 @@ sub _grub2_discovery_arches { # then grub.cfg-<8 hex digit ip>, then ever shorter prefixes of that ip, and # finally grub.cfg. nodeset writes the per-node files (full ip and mac), so a # per-network prefix is only reached by clients without a node configuration: -# discovery. The payload is fetched over HTTP, with a TFTP entry behind it. -# The file is rebuilt from the Genesis artifacts present under +# discovery. The file is rebuilt from the Genesis artifacts present under # $tftpdir/xcat and removed when none are left. Returns the path written. sub _write_grub2_discovery_config { my (%args) = @_; @@ -897,9 +894,8 @@ sub _write_grub2_discovery_config { if (defined($args{consolecmdline}) and $args{consolecmdline} ne '') { $cmdline .= " $args{consolecmdline}"; } - # The Genesis initramfs is large and a TFTP server hands it to one client at a - # time, so the default entry fetches it over HTTP, the same way nodeset does for - # netboot=grub2-http. The TFTP entry stays as the second choice for a management + # TFTP hands the large Genesis image to one client at a time, so the default entry + # loads it over HTTP like netboot=grub2-http; the TFTP entry is for a management # node that does not serve the TFTP root over HTTP. my $httpport = $args{httpport} || '80'; my $httproot = 'http,' . $args{xcatd_address} . ($httpport eq '80' ? '' : ":$httpport"); From aaa0068db7394326eac52b7be133f6794033d309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:21:14 -0300 Subject: [PATCH 37/54] test: reuse repository file helpers --- xCAT-test/unit/copycd_grub2_loader.t | 13 ++--- xCAT-test/unit/dhcp_isc_client_arch.t | 11 ++-- xCAT-test/unit/el_riscv64_install_paths.t | 26 ++++----- xCAT-test/unit/install_profile_riscv64.t | 44 +++++++-------- xCAT-test/unit/lookup_netboot_arch.t | 23 +++----- xCAT-test/unit/mknb_xcatd_address.t | 10 ++-- xCAT-test/unit/netboot_profile_riscv64.t | 53 ++++++++++++------- xCAT-test/unit/nodediscover_default_netboot.t | 9 ++-- xCAT-test/unit/riscv64_packaging.t | 41 +++++++------- 9 files changed, 116 insertions(+), 114 deletions(-) diff --git a/xCAT-test/unit/copycd_grub2_loader.t b/xCAT-test/unit/copycd_grub2_loader.t index 5d6680ba5..dde0d982f 100644 --- a/xCAT-test/unit/copycd_grub2_loader.t +++ b/xCAT-test/unit/copycd_grub2_loader.t @@ -2,27 +2,24 @@ use strict; use warnings; -use Cwd qw(realpath); use File::Path qw(make_path); use File::Spec; use File::Temp qw(tempdir); use FindBin; +use lib "$FindBin::Bin/../lib"; use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # copycd publishes the grub2 UEFI image of the installation media so that nodes # of an architecture xCAT builds no boot loader for -- riscv64 -- can net boot # without a further step. An image the management node already has is never # replaced, and the media of every other architecture is left alone. -my $repo_root = realpath( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); -my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'anaconda.pm' ); +my $plugin = repo_path('xCAT-server/lib/xcat/plugins/anaconda.pm'); plan skip_all => "$plugin not found" unless -r $plugin; -my $source = do { - open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; - local $/; - <$fh>; -}; +my $source = slurp_repo_file('xCAT-server/lib/xcat/plugins/anaconda.pm'); # anaconda.pm needs a database to load, so take just the loader publication out # of it, the way the other unit tests here isolate a shipped sub. diff --git a/xCAT-test/unit/dhcp_isc_client_arch.t b/xCAT-test/unit/dhcp_isc_client_arch.t index d8a22aac2..8642528aa 100644 --- a/xCAT-test/unit/dhcp_isc_client_arch.t +++ b/xCAT-test/unit/dhcp_isc_client_arch.t @@ -3,23 +3,22 @@ use strict; use warnings; use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # The ISC dhcpd subnet block in dhcp.pm maps DHCP option 93 (client system # architecture) to a boot file. The block is rendered inside a large # database-backed subroutine, so this pins the shipped source text: every # architecture branch must appear before the catch-all that hands unknown # clients /yaboot, otherwise the branch is unreachable. -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); -my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'dhcp.pm' ); +my $plugin = repo_path('xCAT-server/lib/xcat/plugins/dhcp.pm'); plan skip_all => "$plugin not found" unless -r $plugin; -open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; -my $source = do { local $/; <$fh> }; -close($fh); +my $source = slurp_repo_file('xCAT-server/lib/xcat/plugins/dhcp.pm'); my %branch = ( aarch64 => qr/client-architecture = 00:0b \{[^\n]*\n\s*push \@netent, "\s*filename \\"boot\/grub2\/grub2\.aarch64\\";/, diff --git a/xCAT-test/unit/el_riscv64_install_paths.t b/xCAT-test/unit/el_riscv64_install_paths.t index 8b9f97108..7b7438a0b 100644 --- a/xCAT-test/unit/el_riscv64_install_paths.t +++ b/xCAT-test/unit/el_riscv64_install_paths.t @@ -3,13 +3,15 @@ use strict; use warnings; use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; use Test::More; use lib "$FindBin::Bin/../../perl-xCAT"; use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils"; use imgutils; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # EL riscv64 media and diskless images: the installer kernel/initrd live under # images/pxeboot like x86 and aarch64 media, and riscv64 diskless images need # their own default network drivers and lib64 resolver libraries. The @@ -17,21 +19,19 @@ use imgutils; # conditions are asserted in the shipped source and the resolver block is # extracted and evaluated directly. -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); - -sub slurp { - my ($relative) = @_; - my $path = File::Spec->catfile( $repo_root, split( m{/}, $relative ) ); +my @source_files = ( + 'xCAT-server/lib/xcat/plugins/anaconda.pm', + 'xCAT-server/lib/xcat/plugins/geninitrd.pm', + 'xCAT-server/share/xcat/netboot/rh/genimage', +); +foreach my $relative (@source_files) { + my $path = repo_path($relative); plan skip_all => "$path not found" unless -r $path; - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $content = do { local $/; <$fh> }; - close($fh); - return $content; } -my $anaconda = slurp('xCAT-server/lib/xcat/plugins/anaconda.pm'); -my $geninitrd = slurp('xCAT-server/lib/xcat/plugins/geninitrd.pm'); -my $genimage = slurp('xCAT-server/share/xcat/netboot/rh/genimage'); +my $anaconda = slurp_repo_file($source_files[0]); +my $geninitrd = slurp_repo_file($source_files[1]); +my $genimage = slurp_repo_file($source_files[2]); # anaconda.pm: stateful install kernel/initrd discovery like( diff --git a/xCAT-test/unit/install_profile_riscv64.t b/xCAT-test/unit/install_profile_riscv64.t index 3a0cc3153..4a61ccae6 100644 --- a/xCAT-test/unit/install_profile_riscv64.t +++ b/xCAT-test/unit/install_profile_riscv64.t @@ -2,27 +2,20 @@ use strict; use warnings; -use Cwd qw(realpath); use File::Spec; use FindBin; +use lib "$FindBin::Bin/../lib"; use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # EL10 riscv64 kickstart installs: the EL10 anaconda has no RISC-V EFI # platform, so the riscv64 templates tolerate its x86 boot loader package # request, pull grub2-efi-riscv64/efibootmgr in through the riscv64 package # lists, and run a %post that points the UEFI boot entry at grubriscv64.efi # and places the removable-media fallback loader. -my $repo_root = realpath( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); -my $install = File::Spec->catdir( $repo_root, 'xCAT-server', 'share', 'xcat', 'install' ); - -sub slurp { - my ($path) = @_; - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $content = do { local $/; <$fh> }; - close($fh); - return $content; -} +my $install = File::Spec->catdir( 'xCAT-server', 'share', 'xcat', 'install' ); for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { my ( $dir, $osbase ) = @$family; @@ -30,9 +23,9 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { my $tmpl = File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" ); my $pkglist = File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.pkglist" ); my $base = File::Spec->catfile( $install, $dir, "$profile.$osbase.tmpl" ); - ok( -r $tmpl, "$dir/$profile.$osbase.riscv64.tmpl exists" ); - ok( -r $pkglist, "$dir/$profile.$osbase.riscv64.pkglist exists" ); - my $t = slurp($tmpl); + ok( -r repo_path($tmpl), "$dir/$profile.$osbase.riscv64.tmpl exists" ); + ok( -r repo_path($pkglist), "$dir/$profile.$osbase.riscv64.pkglist exists" ); + my $t = slurp_repo_file($tmpl); like( $t, qr/^%packages --ignoremissing$/m, "$dir/$profile.$osbase riscv64 template tolerates anaconda's x86 boot loader package request" ); like( $t, qr/^#INCLUDE_DEFAULT_PKGLIST#$/m, "$dir/$profile.$osbase riscv64 template still includes the default package list" ); like( $t, qr{^#INCLUDE:#ENV:XCATROOT#/share/xcat/install/scripts/post\.rhels10\.riscv64#$}m, "$dir/$profile.$osbase riscv64 template runs the riscv64 UEFI boot entry fix-up" ); @@ -43,7 +36,7 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { like( $t, qr/^rootpw --iscrypted #CRYPT:passwd:key=system,username=root:password#$/m, "$dir/$profile.$osbase riscv64 template keeps the shared root password directive" ); # the riscv64 template is the shared one plus the riscv64 changes - my $b = slurp($base); + my $b = slurp_repo_file($base); ( my $t_norm = $t ) =~ s/^# riscv64:.*\n(?:#.*\n)*%addon com_redhat_kdump --disable\n%end\n\n//m; $t_norm =~ s/^# riscv64:.*\n(?:#.*\n)*//m; $t_norm =~ s/^%packages --ignoremissing$/%packages/m; @@ -51,11 +44,13 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { $t_norm =~ s/\n# --ignoremissing above.*?\n%post --erroronfail.*?\n%end\n//s; is( $t_norm, $b, "$dir/$profile.$osbase riscv64 template only differs from the shared template in the riscv64 changes" ); - my $p = slurp($pkglist); + my $p = slurp_repo_file($pkglist); like( $p, qr/^grub2-efi-riscv64$/m, "$dir/$profile.$osbase riscv64 package list installs grub2-efi-riscv64" ); like( $p, qr/^efibootmgr$/m, "$dir/$profile.$osbase riscv64 package list installs efibootmgr" ); unlike( $p, qr/shim|grub2-efi-x64|grub2-efi-aa64/, "$dir/$profile.$osbase riscv64 package list asks for no other architecture's boot loader" ); - my $shared = slurp( File::Spec->catfile( $install, $dir, "$profile.$osbase.pkglist" ) ); + my $shared = slurp_repo_file( + File::Spec->catfile( $install, $dir, "$profile.$osbase.pkglist" ) + ); ( my $p_norm = $p ) =~ s/^grub2-efi-riscv64\n//m; $p_norm =~ s/^efibootmgr\n//m; s/\s+\z/\n/ for ( $p_norm, $shared ); @@ -69,7 +64,9 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { my ( $dir, $osbase ) = @$family; for my $profile (qw(compute service)) { - my $t = slurp( File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" ) ); + my $t = slurp_repo_file( + File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" ) + ); my ($section) = $t =~ /^(%post\b.*?)^%end/ms; ok( $section, "$dir/$profile.$osbase riscv64 template has a %post section" ) or next; @@ -78,7 +75,9 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { ok( defined $index, "$dir/$profile.$osbase riscv64 template includes the fix-up in the %post section" ) or next; for my $earlier ( @scripts[ 0 .. $index - 1 ] ) { - my $body = slurp( File::Spec->catfile( $install, 'scripts', $earlier ) ); + my $body = slurp_repo_file( + File::Spec->catfile( $install, 'scripts', $earlier ) + ); unlike( $body, qr/^\s*exit\b/m, "$dir/$profile.$osbase runs $earlier before the riscv64 fix-up, and $earlier does not end the %post" ); } @@ -86,10 +85,11 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { } my $post = File::Spec->catfile( $install, 'scripts', 'post.rhels10.riscv64' ); -ok( -r $post, 'post.rhels10.riscv64 exists' ); -my $bash = `bash -n $post 2>&1`; +my $post_path = repo_path($post); +ok( -r $post_path, 'post.rhels10.riscv64 exists' ); +my $bash = `bash -n $post_path 2>&1`; is( $bash, '', 'post.rhels10.riscv64 parses as bash' ); -my $s = slurp($post); +my $s = slurp_repo_file($post); like( $s, qr/\[ "\$\(uname -m\)" = "riscv64" \]/, 'the fix-up only acts on riscv64' ); like( $s, qr{/boot/efi/EFI/\*/grubriscv64\.efi}, 'the fix-up locates the distro grub2 UEFI image on the ESP' ); like( $s, qr{cp -f "\$grubefi" /boot/efi/EFI/BOOT/BOOTRISCV64\.EFI}, 'the fix-up installs the removable-media fallback loader' ); diff --git a/xCAT-test/unit/lookup_netboot_arch.t b/xCAT-test/unit/lookup_netboot_arch.t index 15676ce8a..337d64b17 100644 --- a/xCAT-test/unit/lookup_netboot_arch.t +++ b/xCAT-test/unit/lookup_netboot_arch.t @@ -3,9 +3,11 @@ use strict; use warnings; use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # riscv64 nodes boot through UEFI and grub2 only. This test pins the three # places that declare which noderes.netboot values an architecture accepts: # xCAT::Utils::lookupNetboot (used by nodeset/rinstall validation), the @@ -16,25 +18,16 @@ use Test::More; # time, so the shipped subroutines are extracted from the source and evaluated # directly instead of loading the whole modules. -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); -my $utils_pm = File::Spec->catfile( $repo_root, 'perl-xCAT', 'xCAT', 'Utils.pm' ); -my $pnu_pm = File::Spec->catfile( $repo_root, 'perl-xCAT', 'xCAT', 'ProfiledNodeUtils.pm' ); +my $utils_pm = repo_path('perl-xCAT/xCAT/Utils.pm'); +my $pnu_pm = repo_path('perl-xCAT/xCAT/ProfiledNodeUtils.pm'); plan skip_all => "$utils_pm not found" unless -r $utils_pm; plan skip_all => "$pnu_pm not found" unless -r $pnu_pm; -sub slurp { - my ($path) = @_; - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $source = do { local $/; <$fh> }; - close($fh); - return $source; -} - # --------------------------------------------------------------------------- # xCAT::Utils::lookupNetboot # --------------------------------------------------------------------------- -my $utils_source = slurp($utils_pm); +my $utils_source = slurp_repo_file('perl-xCAT/xCAT/Utils.pm'); my ($lookup_sub) = $utils_source =~ m{^(sub lookupNetboot \{.*?^\})}ms; ok( $lookup_sub, 'lookupNetboot was located in xCAT::Utils' ) or BAIL_OUT('Utils.pm no longer matches the expected lookupNetboot shape'); @@ -69,7 +62,7 @@ is( Test::LookupNetboot::lookupNetboot( 'xCAT::Utils', 'rocky10.2', 'riscv64', ' # --------------------------------------------------------------------------- # xCAT::ProfiledNodeUtils netboot rule table + cal_netboot # --------------------------------------------------------------------------- -my $pnu_source = slurp($pnu_pm); +my $pnu_source = slurp_repo_file('perl-xCAT/xCAT/ProfiledNodeUtils.pm'); my ($netboot_dict) = $pnu_source =~ m{^(\s*my %netboot_dict = \(.*?^\s*\);)}ms; ok( $netboot_dict, 'the profiled-node netboot rule table was located' ) or BAIL_OUT('ProfiledNodeUtils.pm no longer matches the expected %netboot_dict shape'); @@ -102,7 +95,7 @@ is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'aarch64', 'rhels', '9', # --------------------------------------------------------------------------- SKIP: { skip 'xCAT::Schema is not loadable here', 3 - unless eval { require lib; lib->import( File::Spec->catdir( $repo_root, 'perl-xCAT' ) ); require xCAT::Schema; 1 }; + unless eval { require lib; lib->import( repo_path('perl-xCAT') ); require xCAT::Schema; 1 }; like( $xCAT::Schema::tabspec{nodetype}{descriptions}{arch}, qr/\briscv64\b/, 'nodetype.arch documents riscv64 as a valid value' ); diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t index 8cc92dd57..77836d302 100644 --- a/xCAT-test/unit/mknb_xcatd_address.t +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -4,9 +4,11 @@ use warnings; ## no critic (Modules::RequireFilenameMatchesPackage, TestingAndDebugging::ProhibitNoStrict, TestingAndDebugging::ProhibitNoWarnings) use FindBin; +use lib "$FindBin::Bin/../lib"; use File::Path qw(make_path); use File::Temp qw(tempdir); use Test::More; +use XCAT::Test::File qw(repo_path); BEGIN { package xCAT::Utils; @@ -50,12 +52,8 @@ BEGIN { $INC{'xCAT/NodeRange.pm'} = __FILE__; } -my $source_mknb_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/mknb.pm"; -if (-f $source_mknb_plugin) { - require $source_mknb_plugin; -} else { - require xCAT_plugin::mknb; -} +my $source_mknb_plugin = repo_path('xCAT-server/lib/xcat/plugins/mknb.pm'); +require $source_mknb_plugin; my ($legacy, $selected) = xCAT_plugin::mknb::_select_network_addresses( { diff --git a/xCAT-test/unit/netboot_profile_riscv64.t b/xCAT-test/unit/netboot_profile_riscv64.t index 56a8f23c2..254e516bb 100644 --- a/xCAT-test/unit/netboot_profile_riscv64.t +++ b/xCAT-test/unit/netboot_profile_riscv64.t @@ -4,10 +4,12 @@ use warnings; ## no critic (Modules::RequireFilenameMatchesPackage) use FindBin; +use lib "$FindBin::Bin/../lib"; use File::Spec; -use Cwd qw(realpath); use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # EL10 riscv64 diskless and service profiles are plain data files resolved by # imgutils::get_profile_def_filename (osver.arch first, then osbase.arch, then # the arch-less fallbacks). Pin that the riscv64 files exist, win the lookup @@ -35,20 +37,15 @@ BEGIN { $INC{'xCAT/SvrUtils.pm'} = __FILE__; } -my $repo_root = realpath( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); -my $share = File::Spec->catdir( $repo_root, 'xCAT-server', 'share', 'xcat' ); -my $imgutils = File::Spec->catfile( $share, 'netboot', 'imgutils', 'imgutils.pm' ); +my $share_relative = File::Spec->catdir( 'xCAT-server', 'share', 'xcat' ); +my $share = repo_path($share_relative); +my $imgutils_relative = File::Spec->catfile( + $share_relative, 'netboot', 'imgutils', 'imgutils.pm' +); +my $imgutils = repo_path($imgutils_relative); plan skip_all => "$imgutils not found" unless -r $imgutils; require $imgutils; -sub slurp { - my ($path) = @_; - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $content = do { local $/; <$fh> }; - close($fh); - return $content; -} - my @families = ( [ 'rocky', 'rocky10', 'rocky10.2' ], [ 'rh', 'rhels10', 'rhels10.2' ], @@ -56,6 +53,7 @@ my @families = ( for my $family (@families) { my ( $dir, $osbase, $osver ) = @$family; + my $base_relative = File::Spec->catdir( $share_relative, 'netboot', $dir ); my $base = File::Spec->catdir( $share, 'netboot', $dir ); for my $profile (qw(compute service)) { @@ -68,34 +66,49 @@ for my $family (@families) { "$osver riscv64 $profile $ext resolves to the riscv64 file", ); } - my $x86 = slurp( File::Spec->catfile( $base, "$profile.$osbase.x86_64.pkglist" ) ); - my $rv = slurp( File::Spec->catfile( $base, "$profile.$osbase.riscv64.pkglist" ) ); + my $x86 = slurp_repo_file( + File::Spec->catfile( $base_relative, "$profile.$osbase.x86_64.pkglist" ) + ); + my $rv = slurp_repo_file( + File::Spec->catfile( $base_relative, "$profile.$osbase.riscv64.pkglist" ) + ); s/\s+\z/\n/ for ( $x86, $rv ); is( $rv, $x86, "$dir/$profile.$osbase riscv64 pkglist matches the x86_64 list (no arch-specific packages)" ); unlike( $rv, qr/^(?:microcode_ctl|grub2-efi-x64|shim-x64|syslinux|xnba)/m, "$dir/$profile.$osbase riscv64 pkglist has no x86-only packages" ); - my $exlist = slurp( File::Spec->catfile( $base, "$profile.$osbase.riscv64.exlist" ) ); + my $exlist = slurp_repo_file( + File::Spec->catfile( $base_relative, "$profile.$osbase.riscv64.exlist" ) + ); like( $exlist, qr{^\./lib/kbd/keymaps/include\*$}m, "$dir/$profile.$osbase riscv64 exlist excludes the kbd keymap includes" ); unlike( $exlist, qr{^\./lib/kdb/}m, "$dir/$profile.$osbase riscv64 exlist has no kdb typo" ); is( scalar( () = $exlist =~ m{^\./usr/share/man\*$}mg ), 1, "$dir/$profile.$osbase riscv64 exlist lists usr/share/man once" ); - my $postinstall = slurp( File::Spec->catfile( $base, "$profile.$osbase.riscv64.postinstall" ) ); + my $postinstall = slurp_repo_file( + File::Spec->catfile( $base_relative, "$profile.$osbase.riscv64.postinstall" ) + ); like( $postinstall, qr/^#!\/bin\/sh/, "$dir/$profile.$osbase riscv64 postinstall is a shell script" ); like( $postinstall, qr/SELINUX=disabled/, "$dir/$profile.$osbase riscv64 postinstall disables SELinux in the image" ); } my $otherpkgs = File::Spec->catfile( $base, "service.$osbase.riscv64.otherpkgs.pkglist" ); + my $otherpkgs_relative = File::Spec->catfile( + $base_relative, "service.$osbase.riscv64.otherpkgs.pkglist" + ); is( imgutils::get_profile_def_filename( $osver, 'service', 'riscv64', $base, 'otherpkgs.pkglist' ), $otherpkgs, "$osver riscv64 service otherpkgs resolves to the riscv64 file", ); - like( slurp($otherpkgs), qr{^xcat/xcat-dep/rh10/riscv64/goconserver$}m, "$dir netboot service otherpkgs pulls goconserver from the riscv64 EL10 dep repo" ); + like( slurp_repo_file($otherpkgs_relative), qr{^xcat/xcat-dep/rh10/riscv64/goconserver$}m, "$dir netboot service otherpkgs pulls goconserver from the riscv64 EL10 dep repo" ); - my $install_otherpkgs = File::Spec->catfile( $share, 'install', $dir, "service.$osbase.riscv64.otherpkgs.pkglist" ); + my $install_otherpkgs_relative = File::Spec->catfile( + $share_relative, 'install', $dir, + "service.$osbase.riscv64.otherpkgs.pkglist" + ); + my $install_otherpkgs = repo_path($install_otherpkgs_relative); ok( -r $install_otherpkgs, "install/$dir/service.$osbase.riscv64.otherpkgs.pkglist exists" ); - like( slurp($install_otherpkgs), qr{^xcat/xcat-dep/rh10/riscv64/goconserver$}m, "$dir install service otherpkgs pulls goconserver from the riscv64 EL10 dep repo" ); - like( slurp($install_otherpkgs), qr{^xcat/xcat-core/xCATsn$}m, "$dir install service otherpkgs installs xCATsn" ); + like( slurp_repo_file($install_otherpkgs_relative), qr{^xcat/xcat-dep/rh10/riscv64/goconserver$}m, "$dir install service otherpkgs pulls goconserver from the riscv64 EL10 dep repo" ); + like( slurp_repo_file($install_otherpkgs_relative), qr{^xcat/xcat-core/xCATsn$}m, "$dir install service otherpkgs installs xCATsn" ); } # an architecture without its own files still falls back to the arch-less ones diff --git a/xCAT-test/unit/nodediscover_default_netboot.t b/xCAT-test/unit/nodediscover_default_netboot.t index 39641171d..5edb8556c 100644 --- a/xCAT-test/unit/nodediscover_default_netboot.t +++ b/xCAT-test/unit/nodediscover_default_netboot.t @@ -4,9 +4,11 @@ use warnings; ## no critic (Modules::RequireFilenameMatchesPackage) use FindBin; -use File::Spec; +use lib "$FindBin::Bin/../lib"; use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # nodediscover sets noderes.netboot for a freshly discovered node when the # admin did not pick a method that fits the reported architecture. The ladder # lives in _default_netboot() so it can be checked without a database: stub @@ -31,8 +33,7 @@ BEGIN { $INC{'xCAT/DiscoveryUtils.pm'} = __FILE__; } -my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); -my $plugin = File::Spec->catfile( $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'nodediscover.pm' ); +my $plugin = repo_path('xCAT-server/lib/xcat/plugins/nodediscover.pm'); plan skip_all => "$plugin not found" unless -r $plugin; require $plugin; @@ -71,7 +72,7 @@ is( $warnings, 0, 'no warnings were emitted for undefined inputs' ); # The discovery request is stored as discovery data key by key, so reading the platform # of a node that never reported one must not add the key to the request. -my $source = do { open( my $fh, '<', $plugin ) or die "Unable to read $plugin: $!"; local $/; <$fh> }; +my $source = slurp_repo_file('xCAT-server/lib/xcat/plugins/nodediscover.pm'); like( $source, qr/my \$platform = exists \$request->\{platform\} \? \$request->\{platform\}->\[0\] : undef;/, diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t index 99c5fd482..9f1fb0341 100644 --- a/xCAT-test/unit/riscv64_packaging.t +++ b/xCAT-test/unit/riscv64_packaging.t @@ -2,27 +2,18 @@ use strict; use warnings; -use File::Spec; use FindBin; +use lib "$FindBin::Bin/../lib"; use Test::More; +use XCAT::Test::File qw(repo_path slurp_repo_file); + # riscv64 packaging: the arch-named packages (xCAT, xCATsn) must resolve their # architecture token and dependencies for riscv64, and the build scripts must # know the riscv64 package names. The Genesis image comes from the OpenEmbedded # packages, so there is no arch-named Genesis package to resolve here. -my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); - -sub read_file { - my ($relative) = @_; - my $path = File::Spec->catfile( $repo_root, split( m{/}, $relative ) ); - open( my $fh, '<', $path ) or die "Unable to read $path: $!"; - my $content = do { local $/; <$fh> }; - close($fh); - return $content; -} - -my $xcat = read_file('xCAT/xCAT.spec'); +my $xcat = slurp_repo_file('xCAT/xCAT.spec'); like( $xcat, qr/^%ifarch riscv64\n(?:#[^\n]*\n)*Requires: ipmitool-xcat >= 1\.8\.18-4\n%endif$/m, 'xCAT.spec requires ipmitool-xcat on riscv64' ); my ($xcat_rv) = $xcat =~ /^%ifarch riscv64\n((?:#[^\n]*\n|Requires:[^\n]*\n)*)%endif$/m; unlike( $xcat_rv || '', qr/xnba-undi|syslinux-xcat|elilo-xcat/, 'xCAT.spec does not require the x86 PXE loaders on riscv64' ); @@ -32,16 +23,26 @@ unlike( $xcat_rv || '', qr/xnba-undi|syslinux-xcat|elilo-xcat/, 'xCAT.spec does like( $xcat, qr/^%\{\?genesistarch:Requires: xCAT-genesis-scripts-%\{genesistarch\} = 1:%\{version\}-%\{release\}\}$/m, 'xCAT.spec asks for the legacy Genesis package only where the architecture has one' ); SKIP: { - skip 'rpmspec is not installed', 2 unless `sh -c 'command -v rpmspec' 2>/dev/null`; - my $requires = `rpmspec -q --target riscv64 --requires xCAT/xCAT.spec 2>/dev/null`; + my $rpmspec = qx(command -v rpmspec 2>/dev/null); + chomp($rpmspec); + skip 'rpmspec is not installed', 2 unless $rpmspec && -x $rpmspec; + + my $spec = repo_path('xCAT/xCAT.spec'); + open( my $requires_fh, '-|', + $rpmspec, '-q', '--target', 'riscv64', '--requires', $spec ) + or BAIL_OUT("unable to run $rpmspec: $!"); + my $requires = do { local $/; <$requires_fh> }; + close($requires_fh) + or BAIL_OUT("rpmspec failed for $spec with status " . ($? >> 8)); + unlike( $requires, qr/genesis-scripts/, 'a riscv64 build requires no legacy Genesis package' ); unlike( $requires, qr/\Q%{genesistarch}\E/, 'a riscv64 build leaves no unexpanded architecture macro' ); } -my $xcatsn = read_file('xCATsn/xCATsn.spec'); +my $xcatsn = slurp_repo_file('xCATsn/xCATsn.spec'); like( $xcatsn, qr/^%ifarch riscv64\nRequires: ipmitool-xcat >= 1\.8\.17-1\n%endif$/m, 'xCATsn.spec requires ipmitool-xcat on riscv64' ); -my $server = read_file('xCAT-server/xCAT-server.spec'); +my $server = slurp_repo_file('xCAT-server/xCAT-server.spec'); like( $server, qr/^Recommends: perl-DB_File$/m, 'xCAT-server.spec recommends perl-DB_File on EL10 (riscv64 has no EPEL to provide it)' ); like( $server, @@ -61,14 +62,14 @@ like( like( $server, qr/^Requires: perl-Net-Telnet perl-Net-DNS perl-Crypt-CBC perl-Crypt-Rijndael$/m, 'xCAT-server.spec keeps the other EL perl requires' ); -my $buildcore = read_file('buildcore.sh'); +my $buildcore = slurp_repo_file('buildcore.sh'); like( $buildcore, qr/^\s*for arch in x86_64 ppc64 ppc64le s390x aarch64 riscv64; do$/m, 'buildcore.sh builds xCAT and xCATsn for riscv64' ); -my $buildlocal = read_file('buildlocal.sh'); +my $buildlocal = slurp_repo_file('buildlocal.sh'); like( $buildlocal, qr/^\s*for arch in x86_64 ppc64 s390x aarch64 riscv64; do$/m, 'buildlocal.sh builds xCAT and xCATsn for riscv64' ); like( $buildlocal, qr{^\s*cp /root/rpmbuild/RPMS/riscv64/\* \$CURDIR/build/$}m, 'buildlocal.sh collects riscv64 rpms' ); -my $buildrpms = read_file('buildrpms.pl'); +my $buildrpms = slurp_repo_file('buildrpms.pl'); like( $buildrpms, qr/forcearch/, 'buildrpms.pl documents the forcearch mock configuration for riscv64 builds' ); # buildrpms.pl derives the rpm architecture from the mock target name; a suffixed From 4011a3415df7d78bca64ece50eff225628ef2a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:24:57 -0300 Subject: [PATCH 38/54] test(xCAT-test): exercise real RISC-V helper implementations --- xCAT-test/unit/copycd_grub2_loader.t | 50 ++++++---------- xCAT-test/unit/lookup_netboot_arch.t | 59 ++++--------------- xCAT-test/unit/netboot_profile_riscv64.t | 25 +------- xCAT-test/unit/nodediscover_default_netboot.t | 49 +++++++++++---- 4 files changed, 70 insertions(+), 113 deletions(-) diff --git a/xCAT-test/unit/copycd_grub2_loader.t b/xCAT-test/unit/copycd_grub2_loader.t index dde0d982f..f663beacc 100644 --- a/xCAT-test/unit/copycd_grub2_loader.t +++ b/xCAT-test/unit/copycd_grub2_loader.t @@ -1,15 +1,19 @@ #!/usr/bin/env perl use strict; use warnings; +no warnings 'once'; use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); use File::Spec; use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; use Test::More; -use XCAT::Test::File qw(repo_path slurp_repo_file); +use XCAT::Test::File qw(repo_path); # copycd publishes the grub2 UEFI image of the installation media so that nodes # of an architecture xCAT builds no boot loader for -- riscv64 -- can net boot @@ -18,25 +22,13 @@ use XCAT::Test::File qw(repo_path slurp_repo_file); my $plugin = repo_path('xCAT-server/lib/xcat/plugins/anaconda.pm'); plan skip_all => "$plugin not found" unless -r $plugin; - -my $source = slurp_repo_file('xCAT-server/lib/xcat/plugins/anaconda.pm'); - -# anaconda.pm needs a database to load, so take just the loader publication out -# of it, the way the other unit tests here isolate a shipped sub. -my ($loaders) = $source =~ /^(my %MEDIA_GRUB2_LOADERS = \([^)]*\);)$/m; -ok( $loaders, 'the media boot loader map was located in anaconda.pm' ) - or BAIL_OUT('anaconda.pm no longer defines %MEDIA_GRUB2_LOADERS'); -my ($sub) = $source =~ /^(sub _install_media_grub2_loader \{.*?^\})$/ms; -ok( $sub, 'the media boot loader publication was located in anaconda.pm' ) - or BAIL_OUT('anaconda.pm no longer defines _install_media_grub2_loader'); +$ENV{XCATROOT} ||= repo_path('xCAT-server'); +require $plugin; my $tftpdir; -{ - package xCAT::TableUtils; - sub getTftpDir { return $tftpdir; } -} - -eval "use File::Path qw(mkpath); use File::Copy; $loaders $sub 1;" or die $@; +no warnings 'redefine'; +local *xCAT::TableUtils::getTftpDir = sub { return $tftpdir; }; +use warnings; sub media { my ( $root, $name, $image ) = @_; @@ -44,9 +36,7 @@ sub media { make_path( File::Spec->catdir( $path, 'EFI', 'BOOT' ) ); if ($image) { my $file = File::Spec->catfile( $path, 'EFI', 'BOOT', $image ); - open( my $fh, '>', $file ) or die "Unable to create $file: $!"; - print $fh "boot loader from the media\n"; - close($fh); + write_text( $file, "boot loader from the media\n" ); } return $path; } @@ -63,7 +53,9 @@ make_path($tftpdir); # riscv64 media, nothing published yet: the image is installed my $riscv_media = media( $root, 'rocky10-riscv64', 'grubriscv64.efi' ); my @responses; -my $written = _install_media_grub2_loader( $riscv_media, 'riscv64', sub { push @responses, @_; } ); +my $written = xCAT_plugin::anaconda::_install_media_grub2_loader( + $riscv_media, 'riscv64', sub { push @responses, @_; } +); my $loader = File::Spec->catfile( $tftpdir, 'boot', 'grub2', 'grub2.riscv64' ); is( $written, $loader, 'riscv64 media publish their grub2 image as the riscv64 boot loader' ); ok( -f $loader, 'the boot loader is written under the TFTP root' ); @@ -72,15 +64,11 @@ is( ( stat($loader) )[7], ( stat( File::Spec->catfile( $riscv_media, 'EFI', 'BOO like( messages( \@responses ), qr/\QInstalled $loader from the media\E/, 'the published boot loader is reported' ); # a boot loader that is already there is never replaced -open( my $fh, '>', $loader ) or die "Unable to rewrite $loader: $!"; -print $fh "installed by grub2-xcat\n"; -close($fh); +write_text( $loader, "installed by grub2-xcat\n" ); @responses = (); -is( scalar _install_media_grub2_loader( $riscv_media, 'riscv64', sub { push @responses, @_; } ), +is( scalar xCAT_plugin::anaconda::_install_media_grub2_loader( $riscv_media, 'riscv64', sub { push @responses, @_; } ), undef, 'an existing boot loader is kept' ); -open( my $rfh, '<', $loader ) or die "Unable to read $loader: $!"; -my $content = do { local $/; <$rfh> }; -close($rfh); +my $content = read_text($loader); is( $content, "installed by grub2-xcat\n", 'the existing boot loader is left untouched' ); is( messages( \@responses ), '', 'nothing is reported when there is nothing to do' ); unlink($loader); @@ -91,7 +79,7 @@ $tftpdir = File::Spec->catdir( $root, 'tftpboot-x86' ); make_path($tftpdir); my $x86_media = media( $root, 'rocky10-x86_64', 'grubx64.efi' ); @responses = (); -is( scalar _install_media_grub2_loader( $x86_media, 'x86_64', sub { push @responses, @_; } ), +is( scalar xCAT_plugin::anaconda::_install_media_grub2_loader( $x86_media, 'x86_64', sub { push @responses, @_; } ), undef, 'x86_64 media publish no boot loader' ); ok( !-e File::Spec->catfile( $tftpdir, 'boot', 'grub2', 'grub2.x86_64' ), 'no x86_64 boot loader is written' ); ok( !-d File::Spec->catdir( $tftpdir, 'boot' ), 'the grub2 directory is only created when there is an image to put in it' ); @@ -101,7 +89,7 @@ $tftpdir = File::Spec->catdir( $root, 'tftpboot-bare' ); make_path($tftpdir); my $bare_media = media( $root, 'rocky10-riscv64-bare' ); @responses = (); -is( scalar _install_media_grub2_loader( $bare_media, 'riscv64', sub { push @responses, @_; } ), +is( scalar xCAT_plugin::anaconda::_install_media_grub2_loader( $bare_media, 'riscv64', sub { push @responses, @_; } ), undef, 'media without a grub2 image publish nothing' ); is( messages( \@responses ), '', 'media without a grub2 image report nothing' ); ok( !-d File::Spec->catdir( $tftpdir, 'boot' ), 'media without a grub2 image create no directories' ); diff --git a/xCAT-test/unit/lookup_netboot_arch.t b/xCAT-test/unit/lookup_netboot_arch.t index 337d64b17..00d22cc15 100644 --- a/xCAT-test/unit/lookup_netboot_arch.t +++ b/xCAT-test/unit/lookup_netboot_arch.t @@ -4,9 +4,12 @@ use warnings; use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT"; use Test::More; -use XCAT::Test::File qw(repo_path slurp_repo_file); +use XCAT::Test::File qw(repo_path); +use xCAT::ProfiledNodeUtils; +use xCAT::Utils; # riscv64 nodes boot through UEFI and grub2 only. This test pins the three # places that declare which noderes.netboot values an architecture accepts: @@ -14,31 +17,9 @@ use XCAT::Test::File qw(repo_path slurp_repo_file); # profiled-node netboot rule table in xCAT::ProfiledNodeUtils, and the # schema descriptions that document the valid values. # -# xCAT::Utils and xCAT::ProfiledNodeUtils pull in the database layer at load -# time, so the shipped subroutines are extracted from the source and evaluated -# directly instead of loading the whole modules. - -my $utils_pm = repo_path('perl-xCAT/xCAT/Utils.pm'); -my $pnu_pm = repo_path('perl-xCAT/xCAT/ProfiledNodeUtils.pm'); - -plan skip_all => "$utils_pm not found" unless -r $utils_pm; -plan skip_all => "$pnu_pm not found" unless -r $pnu_pm; - # --------------------------------------------------------------------------- # xCAT::Utils::lookupNetboot # --------------------------------------------------------------------------- -my $utils_source = slurp_repo_file('perl-xCAT/xCAT/Utils.pm'); -my ($lookup_sub) = $utils_source =~ m{^(sub lookupNetboot \{.*?^\})}ms; -ok( $lookup_sub, 'lookupNetboot was located in xCAT::Utils' ) - or BAIL_OUT('Utils.pm no longer matches the expected lookupNetboot shape'); - -{ - package Test::LookupNetboot; - my $code = $lookup_sub; - eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval) - die "Unable to evaluate lookupNetboot: $@" if $@; -} - my @lookup_cases = ( [ 'rocky10.2', 'riscv64', 'Linux', 'grub2,grub2-tftp,grub2-http', 'riscv64 EL10 boots with grub2 over tftp or http' ], [ 'rhels10.2', 'riscv64', 'Linux', 'grub2,grub2-tftp,grub2-http', 'riscv64 RHEL 10 uses the same grub2 methods' ], @@ -53,41 +34,23 @@ my @lookup_cases = ( for my $case (@lookup_cases) { my ( $osvers, $osarch, $imgtype, $expected, $label ) = @$case; - is( Test::LookupNetboot::lookupNetboot( $osvers, $osarch, $imgtype ), $expected, $label ); + is( xCAT::Utils->lookupNetboot( $osvers, $osarch, $imgtype ), $expected, $label ); } -is( Test::LookupNetboot::lookupNetboot( 'xCAT::Utils', 'rocky10.2', 'riscv64', 'Linux' ), - 'grub2,grub2-tftp,grub2-http', 'the class-method calling convention works for riscv64' ); - # --------------------------------------------------------------------------- # xCAT::ProfiledNodeUtils netboot rule table + cal_netboot # --------------------------------------------------------------------------- -my $pnu_source = slurp_repo_file('perl-xCAT/xCAT/ProfiledNodeUtils.pm'); -my ($netboot_dict) = $pnu_source =~ m{^(\s*my %netboot_dict = \(.*?^\s*\);)}ms; -ok( $netboot_dict, 'the profiled-node netboot rule table was located' ) - or BAIL_OUT('ProfiledNodeUtils.pm no longer matches the expected %netboot_dict shape'); -my ($cal_netboot) = $pnu_source =~ m{^(sub cal_netboot \{.*?^\})}ms; -ok( $cal_netboot, 'cal_netboot was located in xCAT::ProfiledNodeUtils' ) - or BAIL_OUT('ProfiledNodeUtils.pm no longer matches the expected cal_netboot shape'); +my $rule_table = \%xCAT::ProfiledNodeUtils::NETBOOT_RULES; -my $rule_table; -{ - package Test::ProfiledNetboot; - my $code = "$cal_netboot\n sub rule_table { $netboot_dict return \\%netboot_dict; }"; - eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval) - die "Unable to evaluate the profiled-node netboot rules: $@" if $@; - $rule_table = rule_table(); -} - -is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'riscv64', 'rhels', '10', '*' ] ), +is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'riscv64', 'rhels', '10', '*' ] ), 'grub2', 'profiled riscv64 nodes default to grub2' ); -is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'riscv64', 'rhels', '10', 'ipmi' ] ), +is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'riscv64', 'rhels', '10', 'ipmi' ] ), 'grub2', 'riscv64 grub2 does not depend on the management method' ); -is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'x86_64', 'rhels', '10', '*' ] ), +is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'x86_64', 'rhels', '10', '*' ] ), 'xnba', 'x86_64 profiled nodes still default to xnba' ); -is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'ppc64le', 'rhels', '9', 'ipmi' ] ), +is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'ppc64le', 'rhels', '9', 'ipmi' ] ), 'petitboot', 'ppc64le ipmi profiled nodes still default to petitboot' ); -is( Test::ProfiledNetboot::cal_netboot( $rule_table, [ 'aarch64', 'rhels', '9', '*' ] ), +is( xCAT::ProfiledNodeUtils::cal_netboot( $rule_table, [ 'aarch64', 'rhels', '9', '*' ] ), '0', 'aarch64 profiled nodes are still undefined in the rule table' ); # --------------------------------------------------------------------------- diff --git a/xCAT-test/unit/netboot_profile_riscv64.t b/xCAT-test/unit/netboot_profile_riscv64.t index 254e516bb..40c00dab9 100644 --- a/xCAT-test/unit/netboot_profile_riscv64.t +++ b/xCAT-test/unit/netboot_profile_riscv64.t @@ -5,38 +5,19 @@ use warnings; use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; use File::Spec; use Test::More; use XCAT::Test::File qw(repo_path slurp_repo_file); +use xCAT::SvrUtils; # EL10 riscv64 diskless and service profiles are plain data files resolved by # imgutils::get_profile_def_filename (osver.arch first, then osbase.arch, then # the arch-less fallbacks). Pin that the riscv64 files exist, win the lookup # for rocky10/rhels10 point releases, and carry the right content. -BEGIN { - # imgutils pulls in xCAT::SvrUtils only for the OS search list; emulate - # the point-release walk (rocky10.2 -> rocky10.2, rocky10.1, ..., rocky10, - # rocky) without the database-backed module. - package xCAT::SvrUtils; - sub get_os_search_list { - my ($os) = @_; - my @word = split( /\./, $os ); - my @list; - while ( @word && $word[-1] =~ /^[0-9]+$/ ) { - my $last = pop @word; - while ( $last >= 0 ) { - push @list, join( '.', @word, $last ); - $last--; - } - } - push @list, join( '.', @word ); - return @list; - } - $INC{'xCAT/SvrUtils.pm'} = __FILE__; -} - my $share_relative = File::Spec->catdir( 'xCAT-server', 'share', 'xcat' ); my $share = repo_path($share_relative); my $imgutils_relative = File::Spec->catfile( diff --git a/xCAT-test/unit/nodediscover_default_netboot.t b/xCAT-test/unit/nodediscover_default_netboot.t index 5edb8556c..964268636 100644 --- a/xCAT-test/unit/nodediscover_default_netboot.t +++ b/xCAT-test/unit/nodediscover_default_netboot.t @@ -7,7 +7,7 @@ use FindBin; use lib "$FindBin::Bin/../lib"; use Test::More; -use XCAT::Test::File qw(repo_path slurp_repo_file); +use XCAT::Test::File qw(repo_path); # nodediscover sets noderes.netboot for a freshly discovered node when the # admin did not pick a method that fits the reported architecture. The ladder @@ -16,8 +16,23 @@ use XCAT::Test::File qw(repo_path slurp_repo_file); BEGIN { package xCAT::Table; + sub new { + my ( $class, $name ) = @_; + return bless { name => $name }, 'Local::DiscoveryTable'; + } $INC{'xCAT/Table.pm'} = __FILE__; + package Local::DiscoveryTable; + our @writes; + sub getNodeAttribs { return {}; } + sub setNodeAttribs { + my ( $self, $node, $attrs ) = @_; + push @writes, [ $self->{name}, $node, { %{$attrs} } ]; + return; + } + sub close { return; } + sub commit { return; } + package xCAT::Utils; $INC{'xCAT/Utils.pm'} = __FILE__; @@ -70,18 +85,28 @@ is( default_netboot( undef, undef, undef ), undef, 'a missing arch sets nothi is( $warnings, 0, 'no warnings were emitted for undefined inputs' ); -# The discovery request is stored as discovery data key by key, so reading the platform -# of a node that never reported one must not add the key to the request. -my $source = slurp_repo_file('xCAT-server/lib/xcat/plugins/nodediscover.pm'); -like( - $source, - qr/my \$platform = exists \$request->\{platform\} \? \$request->\{platform\}->\[0\] : undef;/, - 'the netboot ladder only reads the platform of a request that carries one', +# Discovery data are later stored key by key. Exercise the real request path to +# prove that an absent platform is not added while selecting the netboot method. +@Local::DiscoveryTable::writes = (); +my $request = { + node => ['node1'], + arch => ['riscv64'], +}; +my @callbacks; +xCAT_plugin::nodediscover::process_request( + $request, + sub { push @callbacks, @_ }, + sub { }, ); -unlike( - $source, - qr/_default_netboot\(\s*\$request->\{arch\}->\[0\],\s*\$request->\{platform\}->\[0\]/, - 'the netboot ladder does not autovivify the platform key of the discovery request', +ok( !exists $request->{platform}, 'discovery does not add a missing platform to the request' ); +ok( + scalar( grep { + $_->[0] eq 'noderes' + && $_->[1] eq 'node1' + && $_->[2]->{netboot} eq 'grub2' + } @Local::DiscoveryTable::writes ), + 'the request path applies the riscv64 grub2 default', ); +is( scalar @callbacks, 1, 'the isolated request stops at the missing client address boundary' ); done_testing(); From ef6897ec875f4aaefdd7aa6b07fb396c711a5d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:53:57 -0300 Subject: [PATCH 39/54] refactor(build): make target architecture parser reusable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- build-utils/lib/XCAT/BuildUtils.pm | 27 +++++++++++++++++++++++++++ buildrpms.pl | 22 ++++++---------------- 2 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 build-utils/lib/XCAT/BuildUtils.pm diff --git a/build-utils/lib/XCAT/BuildUtils.pm b/build-utils/lib/XCAT/BuildUtils.pm new file mode 100644 index 000000000..60ecf0877 --- /dev/null +++ b/build-utils/lib/XCAT/BuildUtils.pm @@ -0,0 +1,27 @@ +package XCAT::BuildUtils; + +use strict; +use warnings; + +use Exporter qw(import); + +our @EXPORT_OK = qw(targetarch_from_target); + +sub targetarch_from_target { + my ( $target, $default_arch ) = @_; + return $default_arch unless defined($target) && length($target); + + my @parts = map { + my $part = lc($_); + $part =~ s/^\s+|\s+$//g; + $part; + } split /-/, $target; + + for my $part (reverse @parts) { + return $part + if $part =~ /^(?:x86_64|i[3-6]86|ppc64le|ppc64|aarch64|riscv64|s390x|armv7hl)$/; + } + return $parts[-1]; +} + +1; diff --git a/buildrpms.pl b/buildrpms.pl index 8029c8def..72f95ba97 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -51,6 +51,8 @@ use Pod::Usage qw(pod2usage); use autodie; use autodie qw(cp); +require "$Bin/build-utils/lib/XCAT/BuildUtils.pm"; + my $SOURCES = "$ENV{HOME}/rpmbuild/SOURCES"; # Ensure the rpmbuild tree exists. buildrpms stages source tarballs into $SOURCES, but it only # runs rpmdev-setuptree in the one-time env-setup path -- so on a host where that never ran (or @@ -294,20 +296,6 @@ sub genesis_tarch_from_targetarch { return $targetarch; } -sub targetarch_from_target { - my ($target) = @_; - return $ARCH unless defined $target && length $target; - # mock configs are named --, but a site config may carry a - # suffix (e.g. rocky-10-riscv64-xcat, the forcearch config for riscv64 builds on an - # x86_64 host), so take the last part that names an architecture and only fall back - # to the last part when none does. - my @parts = map { my $p = lc $_; $p =~ s/^\s+|\s+$//g; $p } split /-/, $target; - for my $part (reverse @parts) { - return $part if $part =~ /^(?:x86_64|i[3-6]86|ppc64le|ppc64|aarch64|riscv64|s390x|armv7hl)$/; - } - return $parts[-1]; -} - # product(\@A, \@B) returns the catersian product of \@A and \@B sub product { my ($a, $b) = @_; @@ -467,7 +455,8 @@ sub buildspkgs { my $ext = $opts{mock_uniqueext} ? "-$opts{mock_uniqueext}" : ""; my $chroot = "$pkg-$target$ext"; - my $targetarch = targetarch_from_target($target); + my $targetarch = + XCAT::BuildUtils::targetarch_from_target( $target, $ARCH ); my $genesis_tarch = genesis_tarch_from_targetarch($targetarch); my $diskcache = ( @@ -523,7 +512,8 @@ sub buildpkgs { my $chroot = "$pkg-$target$ext"; # get x86_64 from alma+epel-9-x86_64 - my $targetarch = targetarch_from_target($target); + my $targetarch = + XCAT::BuildUtils::targetarch_from_target( $target, $ARCH ); # xCAT genesis packages include the translated target arch in their file names. my $arch = is_in($pkg, @NATIVE_PACKAGES) ? $targetarch : "noarch"; From 86108d2a33ca39f6e631898a1d08d6d4e4781e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:53:58 -0300 Subject: [PATCH 40/54] test(riscv64): decouple helper coverage from source layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/el_riscv64_install_paths.t | 29 ++--------------- xCAT-test/unit/mknb_xcatd_address.t | 14 ++++---- xCAT-test/unit/riscv64_packaging.t | 39 ++++++++++++----------- 3 files changed, 31 insertions(+), 51 deletions(-) diff --git a/xCAT-test/unit/el_riscv64_install_paths.t b/xCAT-test/unit/el_riscv64_install_paths.t index 7b7438a0b..363ceb0f7 100644 --- a/xCAT-test/unit/el_riscv64_install_paths.t +++ b/xCAT-test/unit/el_riscv64_install_paths.t @@ -48,18 +48,8 @@ like( # geninitrd.pm: diskless installer initrd source like( $geninitrd, - qr/if \(\$arch =~ \/x86\/ or \(\$arch =~ \/riscv64\/ and \$osvers !~ \/sles\|suse\/\)\) \{\n\s*if \(\$osvers =~ \/\(\^ol\[0-9\]\.\*\)\|\(centos\.\*\)\|\(alma\.\*\)\|\(rocky\.\*\)\|\(rh\.\*\)\|\(fedora\.\*\)\|\(SL\.\*\)\/\) \{\n\s*\$kernelpath = "\$tftppath\/vmlinuz";\n\s*copy\("\$pkgdir\/images\/pxeboot\/vmlinuz", \$kernelpath\);/, - 'geninitrd copies riscv64 EL kernels from images/pxeboot', -); -like( - $geninitrd, - qr/\$arch =~ \/riscv64\/ and \$osvers !~ \/sles\|suse\//, - 'a SUSE osimage on riscv64 keeps the unsupported-architecture error instead of reading SUSE installer media paths', -); -unlike( - $geninitrd, - qr/\} elsif \(\$arch =~ \/riscv64\/\)/, - 'geninitrd does not need a separate riscv64 branch', + qr/if \(\$arch =~ \/x86\/ or \(\$arch =~ \/riscv64\/ and \$osvers !~ \/sles\|suse\/\)\) \{/, + 'geninitrd routes riscv64 EL media through the installer pxeboot path', ); is_deeply( @@ -67,7 +57,6 @@ is_deeply( [qw/e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net/], 'riscv64 diskless images default to virtio, Intel, Realtek, Broadcom and Mellanox drivers', ); - # rh/genimage: resolver libraries for the boot image my ($lib_block) = $genimage =~ m{^(\s*if \(\$arch =~ /x86_64/ or \$arch =~ /aarch64/ or \$arch =~ /riscv64/\) \{\n\s*push \@filestoadd, "lib64/libnss_dns\.so\.2";\n.*?^\s*\}\n)}ms; ok( $lib_block, 'the resolver library block was located in rh/genimage' ) @@ -82,24 +71,10 @@ sub resolver_libs { } is_deeply( resolver_libs('riscv64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], 'riscv64 images take the resolver libraries from lib64' ); -is_deeply( resolver_libs('x86_64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], 'x86_64 images still use lib64' ); -is_deeply( resolver_libs('ppc64'), [ 'lib/libnss_dns.so.2', 'lib/libresolv.so.2' ], 'ppc64 images still use lib' ); - # anaconda.pm: crash kernel reservation for diskless images with kdump enabled like( $anaconda, qr/if \(\$arch eq "riscv64"\) \{\n(?:\s*#[^\n]*\n)*\s*\$kcmdline \.= " crashkernel=256M dump=\$dump ";/, 'a riscv64 diskless image with kdump enabled reserves a crash kernel by default', ); -like( - $anaconda, - qr/if \(\$arch =~ \/86\/\) \{\n\s*\$kcmdline \.= " crashkernel=128M dump=\$dump ";/, - 'the x86 default reservation is unchanged', -); -like( - $anaconda, - qr/\$kcmdline \.= " crashkernel=\$crashkernelsize dump=\$dump ";/, - 'an explicit linuximage.crashkernelsize still wins on every architecture', -); - done_testing(); diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t index 77836d302..df8167dc8 100644 --- a/xCAT-test/unit/mknb_xcatd_address.t +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -5,6 +5,7 @@ use warnings; use FindBin; use lib "$FindBin::Bin/../lib"; +use File::Slurper qw(write_text); use File::Path qw(make_path); use File::Temp qw(tempdir); use Test::More; @@ -433,8 +434,9 @@ like( %xCAT::TableUtils::site_extra = (); # an lzma initramfs is preferred when mknb produced one -open(my $lzma_fh, '>', "$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma") or die "Unable to create lzma: $!"; -close($lzma_fh); +write_text( + "$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma", '' +); $responses = run_mknb('riscv64'); generation_succeeded($responses, 'riscv64 configuration generation succeeds with an lzma initramfs'); like( @@ -455,8 +457,7 @@ unlink("$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma"); 'a missing grub2.riscv64 boot loader is reported', ); make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); - open(my $loader_fh, '>', $loader) or die "Unable to create $loader: $!"; - close($loader_fh); + write_text( $loader, '' ); my $present = run_mknb('riscv64'); ok( !grep({ ref($_) eq 'HASH' && $_->{data} && "@{$_->{data}}" =~ /is missing/ } @{$present}), @@ -495,8 +496,9 @@ $xCAT::NetworkUtils::nic_ips = { eth0 => '10.0.0.1', eth1 => '192.168.148.10' }; use_reporter_address_maps(); prepare_tftpdir($tmpdir, 'tftpboot-riscv64-noboot', 'riscv64'); make_path("$xCAT::TableUtils::tftpdir/boot/grub2"); -open(my $stale_fh, '>', "$xCAT::TableUtils::tftpdir/boot/grub2/grub.cfg-C0A89") or die "Unable to create stale config: $!"; -close($stale_fh); +write_text( + "$xCAT::TableUtils::tftpdir/boot/grub2/grub.cfg-C0A89", '' +); $responses = run_mknb('riscv64'); generation_succeeded($responses, 'riscv64 configuration generation succeeds with a :noboot interface'); ok( diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t index 9f1fb0341..aed324d63 100644 --- a/xCAT-test/unit/riscv64_packaging.t +++ b/xCAT-test/unit/riscv64_packaging.t @@ -4,9 +4,11 @@ use warnings; use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../build-utils/lib"; use Test::More; use XCAT::Test::File qw(repo_path slurp_repo_file); +use XCAT::BuildUtils qw(targetarch_from_target); # riscv64 packaging: the arch-named packages (xCAT, xCATsn) must resolve their # architecture token and dependencies for riscv64, and the build scripts must @@ -71,24 +73,25 @@ like( $buildlocal, qr{^\s*cp /root/rpmbuild/RPMS/riscv64/\* \$CURDIR/build/$}m, my $buildrpms = slurp_repo_file('buildrpms.pl'); like( $buildrpms, qr/forcearch/, 'buildrpms.pl documents the forcearch mock configuration for riscv64 builds' ); +unlike( + $buildrpms, + qr/^use XCAT::BuildUtils\b/m, + 'the dependency bootstrap does not treat the repository build utility as an RPM dependency' +); +like( + $buildrpms, + qr{require "\$Bin/build-utils/lib/XCAT/BuildUtils\.pm";}, + 'buildrpms loads the repository build utility by path at runtime' +); -# buildrpms.pl derives the rpm architecture from the mock target name; a suffixed -# site config (the riscv64 forcearch one) must still resolve to riscv64. -my ($targetarch_sub) = $buildrpms =~ m{^(sub targetarch_from_target \{.*?^\})}ms; -ok( $targetarch_sub, 'targetarch_from_target was located in buildrpms.pl' ) - or BAIL_OUT('buildrpms.pl no longer matches the expected targetarch_from_target shape'); -{ - package Test::BuildTarget; - our $ARCH = 'x86_64'; - eval $targetarch_sub; ## no critic (BuiltinFunctions::ProhibitStringyEval) - die "Unable to evaluate targetarch_from_target: $@" if $@; -} -is( Test::BuildTarget::targetarch_from_target('rocky-10-riscv64-xcat'), 'riscv64', 'a suffixed riscv64 forcearch target resolves to riscv64' ); -is( Test::BuildTarget::targetarch_from_target('rocky-10-riscv64'), 'riscv64', 'the stock riscv64 target resolves to riscv64' ); -is( Test::BuildTarget::targetarch_from_target('alma+epel-10-ppc64le'), 'ppc64le', 'the ppc64le target still resolves to ppc64le' ); -is( Test::BuildTarget::targetarch_from_target('alma+epel-10-x86_64'), 'x86_64', 'the x86_64 target still resolves to x86_64' ); -is( Test::BuildTarget::targetarch_from_target('opensuse-leap-15.6-x86_64'), 'x86_64', 'a dashed distro name still resolves its arch' ); -is( Test::BuildTarget::targetarch_from_target('custom-target-foo'), 'foo', 'a target without an architecture token keeps the last part' ); -is( Test::BuildTarget::targetarch_from_target(undef), 'x86_64', 'no target means the host architecture' ); +# buildrpms.pl derives the rpm architecture from the mock target name through +# the loadable build utility, so exercise the implementation directly. +is( targetarch_from_target('rocky-10-riscv64-xcat', 'x86_64'), 'riscv64', 'a suffixed riscv64 forcearch target resolves to riscv64' ); +is( targetarch_from_target('rocky-10-riscv64', 'x86_64'), 'riscv64', 'the stock riscv64 target resolves to riscv64' ); +is( targetarch_from_target('alma+epel-10-ppc64le', 'x86_64'), 'ppc64le', 'the ppc64le target still resolves to ppc64le' ); +is( targetarch_from_target('alma+epel-10-x86_64', 'x86_64'), 'x86_64', 'the x86_64 target still resolves to x86_64' ); +is( targetarch_from_target('opensuse-leap-15.6-x86_64', 'x86_64'), 'x86_64', 'a dashed distro name still resolves its arch' ); +is( targetarch_from_target('custom-target-foo', 'x86_64'), 'foo', 'a target without an architecture token keeps the last part' ); +is( targetarch_from_target(undef, 'x86_64'), 'x86_64', 'no target means the host architecture' ); done_testing(); From 96b4e62fce5054f075cbe6e7d8fe348fce0cfa88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:00 -0300 Subject: [PATCH 41/54] refactor(dhcp): expose ISC client boot policy --- perl-xCAT/xCAT/DHCP/BootPolicy.pm | 44 ++++++++++++++++++++++++++ xCAT-server/lib/xcat/plugins/dhcp.pm | 47 +++++----------------------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/perl-xCAT/xCAT/DHCP/BootPolicy.pm b/perl-xCAT/xCAT/DHCP/BootPolicy.pm index c574df125..ba33b89b6 100644 --- a/perl-xCAT/xCAT/DHCP/BootPolicy.pm +++ b/perl-xCAT/xCAT/DHCP/BootPolicy.pm @@ -102,6 +102,50 @@ sub kea_httpboot_network_classes { return \@classes; } +sub isc_client_architecture_lines { + my ( $class, %opts ) = @_; + + my $tftp = $opts{next_server} // ''; + my $portsuffix = $opts{portsuffix} // ''; + my $tftpdir = $opts{tftpdir} // '/tftpboot'; + my $net = $opts{net} // ''; + my $maskbits = $opts{prefix} // ''; + + return [ + " if option user-class-identifier = \"xNBA\" and option client-architecture = 00:00 { #x86, xCAT Network Boot Agent\n", + " always-broadcast on;\n", + " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}\";\n", + " } else if option user-class-identifier = \"xNBA\" and option client-architecture = 00:09 { #x86, xCAT Network Boot Agent\n", + " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}.uefi\";\n", + " } else if option user-class-identifier = \"xNBA\" and option client-architecture = 00:07 { #x86-64 UEFI, xCAT Network Boot Agent\n", + " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/${net}_${maskbits}.uefi\";\n", + " } else if option client-architecture = 00:00 { #x86\n", + " filename \"xcat/xnba.kpxe\";\n", + " } else if option vendor-class-identifier = \"Etherboot-5.4\" { #x86\n", + " filename \"xcat/xnba.kpxe\";\n", + " } else if option client-architecture = 00:07 { #x86_64 uefi\n ", + " filename \"xcat/xnba.efi\";\n", + " } else if option client-architecture = 00:09 { #x86_64 uefi alternative id\n ", + " filename \"xcat/xnba.efi\";\n", + " } else if option client-architecture = 00:02 { #ia64\n ", + " filename \"elilo.efi\";\n", + " } else if option client-architecture = 00:0b { #aaarch64\n ", + " filename \"boot/grub2/grub2.aarch64\";\n", + " } else if option client-architecture = 00:1b { #riscv64 uefi\n ", + " filename \"boot/grub2/grub2.riscv64\";\n", + " } else if option client-architecture = 00:1c { #riscv64 uefi http boot\n ", + " option vendor-class-identifier \"HTTPClient\";\n", + " filename \"http://$tftp$portsuffix$tftpdir/boot/grub2/grub2.riscv64\";\n", + " } else if option client-architecture = 00:0e { #OPAL-v3\n ", + " option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/${net}_${maskbits}\";\n", + " } else if substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { #for onie on cumulus switch\n", + " option www-server = \"http://$tftp$portsuffix/install/onie/onie-installer\";\n", + " } else if substring(filename,0,1) = null { #otherwise, provide yaboot if the client isn't specific\n ", + " filename \"/yaboot\";\n", + " }\n", + ]; +} + sub kea_xnba_node_classes { my ( $class, %opts ) = @_; diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index e6691730e..150f8613d 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -4454,46 +4454,13 @@ sub addnet } # $lstatements = 'if exists gpxe.bus-id { filename = \"\"; } else if exists client-architecture { filename = \"xcat/xnba.kpxe\"; } '.$lstatements; - push @netent, " if option user-class-identifier = \"xNBA\" and option client-architecture = 00:00 { #x86, xCAT Network Boot Agent\n"; - push @netent, " always-broadcast on;\n"; - push @netent, " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/" . $net . "_" . $maskbits . "\";\n"; - push @netent, " } else if option user-class-identifier = \"xNBA\" and option client-architecture = 00:09 { #x86, xCAT Network Boot Agent\n"; - push @netent, " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/" . $net . "_" . $maskbits . ".uefi\";\n"; - push @netent, " } else if option user-class-identifier = \"xNBA\" and option client-architecture = 00:07 { #x86-64 UEFI, xCAT Network Boot Agent\n"; - push @netent, " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/" . $net . "_" . $maskbits . ".uefi\";\n"; - push @netent, " } else if option client-architecture = 00:00 { #x86\n"; - push @netent, " filename \"xcat/xnba.kpxe\";\n"; - push @netent, " } else if option vendor-class-identifier = \"Etherboot-5.4\" { #x86\n"; - push @netent, " filename \"xcat/xnba.kpxe\";\n"; - push @netent, - " } else if option client-architecture = 00:07 { #x86_64 uefi\n "; - push @netent, " filename \"xcat/xnba.efi\";\n"; - push @netent, - " } else if option client-architecture = 00:09 { #x86_64 uefi alternative id\n "; - push @netent, " filename \"xcat/xnba.efi\";\n"; - push @netent, - " } else if option client-architecture = 00:02 { #ia64\n "; - push @netent, " filename \"elilo.efi\";\n"; - push @netent, - " } else if option client-architecture = 00:0b { #aaarch64\n "; - push @netent, " filename \"boot/grub2/grub2.aarch64\";\n"; - push @netent, - " } else if option client-architecture = 00:1b { #riscv64 uefi\n "; - push @netent, " filename \"boot/grub2/grub2.riscv64\";\n"; - push @netent, - " } else if option client-architecture = 00:1c { #riscv64 uefi http boot\n "; - push @netent, " option vendor-class-identifier \"HTTPClient\";\n"; - push @netent, " filename \"http://$tftp$portsuffix$tftpdir/boot/grub2/grub2.riscv64\";\n"; - push @netent, - " } else if option client-architecture = 00:0e { #OPAL-v3\n "; - push @netent, " option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/" . $net . "_" . $maskbits . "\";\n"; - push @netent, - " } else if substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { #for onie on cumulus switch\n"; - push @netent, " option www-server = \"http://$tftp$portsuffix/install/onie/onie-installer\";\n"; - push @netent, - " } else if substring(filename,0,1) = null { #otherwise, provide yaboot if the client isn't specific\n "; - push @netent, " filename \"/yaboot\";\n"; - push @netent, " }\n"; + push @netent, @{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines( + next_server => $tftp, + portsuffix => $portsuffix, + tftpdir => $tftpdir, + net => $net, + prefix => $maskbits, + ) }; if ($range) { foreach my $singlerange (split /;/, $range) { From 6d32aa944c9f103925e1f0fb2c876ab1ac5cca68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:00 -0300 Subject: [PATCH 42/54] test(dhcp): exercise ISC client boot policy --- xCAT-test/unit/dhcp_isc_client_arch.t | 91 +++++++++++++-------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/xCAT-test/unit/dhcp_isc_client_arch.t b/xCAT-test/unit/dhcp_isc_client_arch.t index 8642528aa..b2bd38d00 100644 --- a/xCAT-test/unit/dhcp_isc_client_arch.t +++ b/xCAT-test/unit/dhcp_isc_client_arch.t @@ -3,62 +3,57 @@ use strict; use warnings; use FindBin; -use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../perl-xCAT"; use Test::More; -use XCAT::Test::File qw(repo_path slurp_repo_file); +use xCAT::DHCP::BootPolicy; -# The ISC dhcpd subnet block in dhcp.pm maps DHCP option 93 (client system -# architecture) to a boot file. The block is rendered inside a large -# database-backed subroutine, so this pins the shipped source text: every -# architecture branch must appear before the catch-all that hands unknown -# clients /yaboot, otherwise the branch is unreachable. +my $rendered = join '', @{ xCAT::DHCP::BootPolicy->isc_client_architecture_lines( + next_server => '192.0.2.10', + portsuffix => ':8080', + tftpdir => '/srv/tftp', + net => '192.0.2.0', + prefix => 24, + ) }; -my $plugin = repo_path('xCAT-server/lib/xcat/plugins/dhcp.pm'); - -plan skip_all => "$plugin not found" unless -r $plugin; - -my $source = slurp_repo_file('xCAT-server/lib/xcat/plugins/dhcp.pm'); - -my %branch = ( - aarch64 => qr/client-architecture = 00:0b \{[^\n]*\n\s*push \@netent, "\s*filename \\"boot\/grub2\/grub2\.aarch64\\";/, - riscv64 => qr/client-architecture = 00:1b \{[^\n]*\n\s*push \@netent, "\s*filename \\"boot\/grub2\/grub2\.riscv64\\";/, - opal => qr/client-architecture = 00:0e \{/, - yaboot => qr/substring\(filename,0,1\) = null \{[^\n]*\n\s*push \@netent, "\s*filename \\"\/yaboot\\";/, +like( + $rendered, + qr/client-architecture = 00:0b \{ #aaarch64\n\s+filename "boot\/grub2\/grub2\.aarch64";/, + 'the ISC policy renders the aarch64 boot branch', +); +like( + $rendered, + qr/client-architecture = 00:1b \{ #riscv64 uefi\n\s+filename "boot\/grub2\/grub2\.riscv64";/, + 'the ISC policy renders the riscv64 TFTP boot branch', +); +like( + $rendered, + qr/client-architecture = 00:1c \{ #riscv64 uefi http boot\n\s+option vendor-class-identifier "HTTPClient";\n\s+filename "http:\/\/192\.0\.2\.10:8080\/srv\/tftp\/boot\/grub2\/grub2\.riscv64";/, + 'the ISC policy renders the riscv64 HTTP boot branch with the subnet URL', +); +like( + $rendered, + qr/option conf-file = "http:\/\/192\.0\.2\.10:8080\/tftpboot\/pxelinux\.cfg\/p\/192\.0\.2\.0_24";/, + 'the existing OPAL branch keeps its subnet URL', ); -my %pos; -for my $name ( sort keys %branch ) { - ok( $source =~ $branch{$name}, "the ISC subnet block renders the $name branch" ) - or next; - $pos{$name} = $-[0]; -} - -SKIP: { - skip 'not every branch was found', 3 unless 4 == scalar keys %pos; - cmp_ok( $pos{aarch64}, '<', $pos{riscv64}, 'riscv64 follows the aarch64 branch' ); - cmp_ok( $pos{riscv64}, '<', $pos{opal}, 'riscv64 is rendered before the POWER OPAL branch' ); - cmp_ok( $pos{riscv64}, '<', $pos{yaboot}, 'riscv64 is rendered before the /yaboot fallback, so it is reachable' ); -} - -# HTTP boot firmware (architecture id 28) needs the boot file as a URL and only -# accepts an offer that is tagged HTTPClient -ok( - $source =~ /client-architecture = 00:1c \{[^\n]*\n\s*push \@netent, "\s*option vendor-class-identifier \\"HTTPClient\\";[^\n]*\n\s*push \@netent, "\s*filename \\"http:\/\/\$tftp\$portsuffix\$tftpdir\/boot\/grub2\/grub2\.riscv64\\";/, - 'the ISC subnet block hands RISC-V HTTP boot clients the boot loader as a URL', -); -my $httpboot_pos = $-[0]; -SKIP: { - skip 'the riscv64 branches were not both found', 2 unless defined $pos{riscv64} && defined $httpboot_pos; - cmp_ok( $pos{riscv64}, '<', $httpboot_pos, 'the PXE branch keeps its place before the HTTP boot branch' ); - cmp_ok( $httpboot_pos, '<', $pos{yaboot}, 'the HTTP boot branch is rendered before the /yaboot fallback' ); -} - -my @riscv_ids = $source =~ /client-architecture = (00:1[9a-e])/g; +my @riscv_ids = $rendered =~ /client-architecture = (00:1[9a-e])/g; is_deeply( - [ sort @riscv_ids ], + \@riscv_ids, [ '00:1b', '00:1c' ], - 'only the RISC-V 64-bit UEFI architecture ids (27 and 28) are mapped', + 'only the RISC-V 64-bit UEFI architecture ids are mapped', ); +my $aarch64_pos = index($rendered, 'client-architecture = 00:0b'); +my $tftp_pos = index($rendered, 'client-architecture = 00:1b'); +my $http_pos = index($rendered, 'client-architecture = 00:1c'); +my $opal_pos = index($rendered, 'client-architecture = 00:0e'); +my $fallback_pos = index($rendered, 'substring(filename,0,1) = null'); + +cmp_ok($aarch64_pos, '<', $tftp_pos, 'riscv64 follows the aarch64 branch'); +cmp_ok($tftp_pos, '<', $http_pos, 'the TFTP branch precedes the HTTP branch'); +cmp_ok($http_pos, '<', $opal_pos, 'the HTTP branch precedes the OPAL branch'); +cmp_ok($http_pos, '<', $fallback_pos, 'the HTTP branch is reachable before the fallback'); +like($rendered, qr/filename "\/yaboot";\n\s*\}\n\z/, 'the policy ends with the existing yaboot fallback'); + done_testing(); From 825fbb8d2b5be70611761ddf86afeb8f2956723a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:00 -0300 Subject: [PATCH 43/54] refactor(netboot): expose install image policies --- xCAT-server/lib/xcat/plugins/anaconda.pm | 75 ++++++++++++------- xCAT-server/lib/xcat/plugins/geninitrd.pm | 10 ++- .../share/xcat/netboot/imgutils/imgutils.pm | 7 ++ xCAT-server/share/xcat/netboot/rh/genimage | 8 +- 4 files changed, 67 insertions(+), 33 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/anaconda.pm b/xCAT-server/lib/xcat/plugins/anaconda.pm index 0af5d40ab..07c5945db 100644 --- a/xCAT-server/lib/xcat/plugins/anaconda.pm +++ b/xCAT-server/lib/xcat/plugins/anaconda.pm @@ -157,6 +157,44 @@ sub _find_genesis_boot_files return ($kernel, $initrd); } +sub _install_media_pxeboot_paths +{ + my ($pkgdir, $arch) = @_; + + return unless defined($pkgdir) && defined($arch); + return unless $arch =~ /x86|aarch64|riscv64/; + + my $kernel = "$pkgdir/images/pxeboot/vmlinuz"; + my $initrd = "$pkgdir/images/pxeboot/initrd.img"; + return unless -r $kernel; + + return ($kernel, -r $initrd ? $initrd : undef); +} + +sub _driver_disk_kernel_version +{ + my $path = shift; + + return unless defined($path); + return $1 if $path =~ m{/vmlinuz-(.*(?:x86_64|ppc64|el\d+|ppc64le|aarch64|riscv64))$}; + return; +} + +sub _default_crashkernel_args +{ + my ($arch, $dump, $fadump_flag, $fadump, $kdump) = @_; + + return '' unless defined($arch); + if ($arch eq 'ppc64') { + return $fadump_flag + ? " fadump=on fadump_reserve_mem=512M fadump_target=$fadump fadump_default=noreboot dump=$kdump " + : " crashkernel=256M\@64M dump=$dump "; + } + return " crashkernel=128M dump=$dump " if $arch =~ /86/; + return " crashkernel=256M dump=$dump " if $arch eq 'riscv64'; + return ''; +} + sub preprocess_request { my $req = shift; @@ -907,22 +945,9 @@ sub mknetboot } } else { - if ($arch eq "ppc64") { - if ($fadumpFlag) { - $kcmdline .= " fadump=on fadump_reserve_mem=512M fadump_target=$fadump fadump_default=noreboot dump=$kdump "; - } - else { - $kcmdline .= " crashkernel=256M\@64M dump=$dump "; - } - } - if ($arch =~ /86/) { - $kcmdline .= " crashkernel=128M dump=$dump "; - } - if ($arch eq "riscv64") { - # EL has no default reservation for riscv64, so without an explicit - # linuximage.crashkernelsize the kernel would reserve nothing at all - $kcmdline .= " crashkernel=256M dump=$dump "; - } + $kcmdline .= _default_crashkernel_args( + $arch, $dump, $fadumpFlag, $fadump, $kdump + ); } } @@ -1345,15 +1370,14 @@ sub mkinstall $pkvm = 1; } + my ($pxe_kernpath, $pxe_initrdpath) = + _install_media_pxeboot_paths($pkgdir, $arch); + $kernpath = $pxe_kernpath if defined($pxe_kernpath); + $initrdpath = $pxe_initrdpath if defined($pxe_initrdpath); + if ( ( - ( $arch =~ /x86/ or $arch =~ /aarch64/ or $arch =~ /riscv64/ ) and - ( - -r "$pkgdir/images/pxeboot/vmlinuz" - and $kernpath = "$pkgdir/images/pxeboot/vmlinuz" - and -r "$pkgdir/images/pxeboot/initrd.img" - and $initrdpath = "$pkgdir/images/pxeboot/initrd.img" - ) or ( #Handle the case seen in VMWare 4.0 ESX media + ( defined($pxe_kernpath) and defined($pxe_initrdpath) ) or ( #Handle the case seen in VMWare 4.0 ESX media #In VMWare 4.0 they dropped the pxe-optimized initrd #leaving us no recourse but the rather large optical disk #initrd, but perhaps we can mitigate with gPXE @@ -2834,8 +2858,9 @@ sub insert_dd { # and copy it to the /tftpboot my @new_kernels = <$dd_dir/rpm/boot/vmlinuz*>; foreach my $new_kernel (@new_kernels) { - if (-r $new_kernel && $new_kernel =~ /\/vmlinuz-(.*(x86_64|ppc64|el\d+|ppc64le|aarch64|riscv64))$/) { - $new_kernel_ver = $1; + my $candidate_version = _driver_disk_kernel_version($new_kernel); + if (-r $new_kernel && defined($candidate_version)) { + $new_kernel_ver = $candidate_version; $cmd = "/bin/mv -f $new_kernel $kernelpath"; xCAT::Utils->runcmd($cmd, -1); if ($::RUNCMD_RC != 0) { diff --git a/xCAT-server/lib/xcat/plugins/geninitrd.pm b/xCAT-server/lib/xcat/plugins/geninitrd.pm index 47b07e883..6f9388cef 100644 --- a/xCAT-server/lib/xcat/plugins/geninitrd.pm +++ b/xCAT-server/lib/xcat/plugins/geninitrd.pm @@ -15,6 +15,14 @@ use xCAT::TableUtils; use xCAT::Table; use xCAT::Scope; +sub _uses_x86_install_media_layout +{ + my ($arch, $osvers) = @_; + + return unless defined($arch) && defined($osvers); + return $arch =~ /x86/ || ($arch =~ /riscv64/ && $osvers !~ /sles|suse/); +} + sub handled_commands { return { @@ -191,7 +199,7 @@ sub geninitrd { } # riscv64 installation media exist for the EL family only; leave any other # distribution to the "unknow arch" error below instead of reading it as x86 - if ($arch =~ /x86/ or ($arch =~ /riscv64/ and $osvers !~ /sles|suse/)) { + if (_uses_x86_install_media_layout($arch, $osvers)) { if ($osvers =~ /(^ol[0-9].*)|(centos.*)|(alma.*)|(rocky.*)|(rh.*)|(fedora.*)|(SL.*)/) { $kernelpath = "$tftppath/vmlinuz"; copy("$pkgdir/images/pxeboot/vmlinuz", $kernelpath); diff --git a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm index a5881c984..644a6e671 100644 --- a/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm +++ b/xCAT-server/share/xcat/netboot/imgutils/imgutils.pm @@ -79,6 +79,13 @@ sub _profile_lookup_osbase_list { return grep { $_ ne $osver } xCAT::SvrUtils::get_os_search_list($osver); } +sub resolver_library_paths { + my $arch = shift; + my $libdir = $arch =~ /x86_64|aarch64|riscv64/ ? 'lib64' : 'lib'; + + return ( "$libdir/libnss_dns.so.2", "$libdir/libresolv.so.2" ); +} + sub get_profile_def_filename { my $osver = shift; my $profile = shift; diff --git a/xCAT-server/share/xcat/netboot/rh/genimage b/xCAT-server/share/xcat/netboot/rh/genimage index 3dc1c392d..452a4e3a9 100755 --- a/xCAT-server/share/xcat/netboot/rh/genimage +++ b/xCAT-server/share/xcat/netboot/rh/genimage @@ -1924,13 +1924,7 @@ EOMS } } - if ($arch =~ /x86_64/ or $arch =~ /aarch64/ or $arch =~ /riscv64/) { - push @filestoadd, "lib64/libnss_dns.so.2"; - push @filestoadd, "lib64/libresolv.so.2"; - } else { - push @filestoadd, "lib/libnss_dns.so.2"; - push @filestoadd, "lib/libresolv.so.2"; - } + push @filestoadd, imgutils::resolver_library_paths($arch); push @filestoadd, keys %libhash; find(\&isnetdriver, <$rootimg_dir/lib/modules/$kernelver/*>); From 41ac89cf1baef795aed3dd1e7c98ae5ab1bf22f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:01 -0300 Subject: [PATCH 44/54] test(netboot): exercise RISC-V install image policies --- xCAT-test/unit/el_riscv64_install_paths.t | 168 ++++++++++++++-------- 1 file changed, 108 insertions(+), 60 deletions(-) diff --git a/xCAT-test/unit/el_riscv64_install_paths.t b/xCAT-test/unit/el_riscv64_install_paths.t index 363ceb0f7..f7d7d7c45 100644 --- a/xCAT-test/unit/el_riscv64_install_paths.t +++ b/xCAT-test/unit/el_riscv64_install_paths.t @@ -2,79 +2,127 @@ use strict; use warnings; +use File::Path qw(make_path); +use File::Slurper qw(write_text); +use File::Spec; +use File::Temp qw(tempdir); use FindBin; -use lib "$FindBin::Bin/../lib"; use Test::More; -use lib "$FindBin::Bin/../../perl-xCAT"; -use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; -use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils"; -use imgutils; - -use XCAT::Test::File qw(repo_path slurp_repo_file); - -# EL riscv64 media and diskless images: the installer kernel/initrd live under -# images/pxeboot like x86 and aarch64 media, and riscv64 diskless images need -# their own default network drivers and lib64 resolver libraries. The -# behavior is pinned without running the database-backed plugins: the arch -# conditions are asserted in the shipped source and the resolver block is -# extracted and evaluated directly. - -my @source_files = ( - 'xCAT-server/lib/xcat/plugins/anaconda.pm', - 'xCAT-server/lib/xcat/plugins/geninitrd.pm', - 'xCAT-server/share/xcat/netboot/rh/genimage', -); -foreach my $relative (@source_files) { - my $path = repo_path($relative); - plan skip_all => "$path not found" unless -r $path; +BEGIN { + $ENV{XCATROOT} = "$FindBin::Bin/../../xCAT-server"; } -my $anaconda = slurp_repo_file($source_files[0]); -my $geninitrd = slurp_repo_file($source_files[1]); -my $genimage = slurp_repo_file($source_files[2]); +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins"; +use lib "$FindBin::Bin/../../xCAT-server/share/xcat/netboot/imgutils"; -# anaconda.pm: stateful install kernel/initrd discovery -like( - $anaconda, - qr/\( \$arch =~ \/x86\/ or \$arch =~ \/aarch64\/ or \$arch =~ \/riscv64\/ \) and\n\s*\(\n\s*-r "\$pkgdir\/images\/pxeboot\/vmlinuz"/, - 'anaconda looks for riscv64 installer kernels under images/pxeboot like x86 and aarch64', +require anaconda; +require geninitrd; +use imgutils; + +my $media = tempdir(CLEANUP => 1); +my $pxeboot = File::Spec->catdir($media, 'images', 'pxeboot'); +make_path($pxeboot); +write_text(File::Spec->catfile($pxeboot, 'vmlinuz'), "kernel\n"); +write_text(File::Spec->catfile($pxeboot, 'initrd.img'), "initrd\n"); + +my @paths = xCAT_plugin::anaconda::_install_media_pxeboot_paths($media, 'riscv64'); +is_deeply( + \@paths, + [ File::Spec->catfile($pxeboot, 'vmlinuz'), File::Spec->catfile($pxeboot, 'initrd.img') ], + 'riscv64 installer media use the images/pxeboot kernel and initrd', ); -like( - $anaconda, - qr/\/\\\/vmlinuz-\(\.\*\(x86_64\|ppc64\|el\\d\+\|ppc64le\|aarch64\|riscv64\)\)\$\//, +is_deeply( + [ xCAT_plugin::anaconda::_install_media_pxeboot_paths($media, 'x86_64') ], + \@paths, + 'x86_64 keeps the existing images/pxeboot layout', +); +is_deeply( + [ xCAT_plugin::anaconda::_install_media_pxeboot_paths($media, 'aarch64') ], + \@paths, + 'aarch64 keeps the existing images/pxeboot layout', +); +is_deeply( + [ xCAT_plugin::anaconda::_install_media_pxeboot_paths($media, 'ppc64') ], + [], + 'POWER media continue through their separate layout', +); +unlink(File::Spec->catfile($pxeboot, 'initrd.img')); +is_deeply( + [ xCAT_plugin::anaconda::_install_media_pxeboot_paths($media, 'riscv64') ], + [ File::Spec->catfile($pxeboot, 'vmlinuz'), undef ], + 'an incomplete pxeboot media pair preserves the existing partial lookup result', +); + +is( + xCAT_plugin::anaconda::_driver_disk_kernel_version('/tmp/rpm/boot/vmlinuz-6.12.0-55.riscv64'), + '6.12.0-55.riscv64', 'driver-disk kernel updates recognise riscv64 kernels', ); +is( + xCAT_plugin::anaconda::_driver_disk_kernel_version('/tmp/rpm/boot/vmlinuz-6.12.0-55.x86_64'), + '6.12.0-55.x86_64', + 'driver-disk kernel updates still recognise x86_64 kernels', +); +is( + xCAT_plugin::anaconda::_driver_disk_kernel_version('/tmp/rpm/boot/not-a-kernel-riscv64'), + undef, + 'driver-disk kernel parsing rejects unrelated files', +); -# geninitrd.pm: diskless installer initrd source -like( - $geninitrd, - qr/if \(\$arch =~ \/x86\/ or \(\$arch =~ \/riscv64\/ and \$osvers !~ \/sles\|suse\/\)\) \{/, - 'geninitrd routes riscv64 EL media through the installer pxeboot path', +ok( + xCAT_plugin::geninitrd::_uses_x86_install_media_layout('riscv64', 'rocky10'), + 'EL riscv64 media use the installer pxeboot layout', +); +ok( + !xCAT_plugin::geninitrd::_uses_x86_install_media_layout('riscv64', 'sles15'), + 'SLES riscv64 media do not enter the EL layout', +); +ok( + xCAT_plugin::geninitrd::_uses_x86_install_media_layout('x86_64', 'sles15'), + 'x86_64 SLES media retain their existing outer architecture route', +); +ok( + !xCAT_plugin::geninitrd::_uses_x86_install_media_layout('ppc64le', 'rocky10'), + 'POWER media retain their separate architecture route', ); is_deeply( [ imgutils::default_net_drivers( 'rh', 'riscv64' ) ], - [qw/e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net/], - 'riscv64 diskless images default to virtio, Intel, Realtek, Broadcom and Mellanox drivers', + [qw(e1000 e1000e igb ixgbe r8169 tg3 bnx2x mlx5_core virtio_net)], + 'riscv64 diskless images receive the intended default network drivers', ); -# rh/genimage: resolver libraries for the boot image -my ($lib_block) = $genimage =~ m{^(\s*if \(\$arch =~ /x86_64/ or \$arch =~ /aarch64/ or \$arch =~ /riscv64/\) \{\n\s*push \@filestoadd, "lib64/libnss_dns\.so\.2";\n.*?^\s*\}\n)}ms; -ok( $lib_block, 'the resolver library block was located in rh/genimage' ) - or BAIL_OUT('rh/genimage no longer matches the expected resolver library block'); - -sub resolver_libs { - my ($arch) = @_; - my $code = "sub { my \$arch = shift; my \@filestoadd;\n$lib_block\n return \@filestoadd; }"; - my $sub = eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval) - die "Unable to evaluate the resolver library block: $@" if $@; - return [ $sub->($arch) ]; -} - -is_deeply( resolver_libs('riscv64'), [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], 'riscv64 images take the resolver libraries from lib64' ); -# anaconda.pm: crash kernel reservation for diskless images with kdump enabled -like( - $anaconda, - qr/if \(\$arch eq "riscv64"\) \{\n(?:\s*#[^\n]*\n)*\s*\$kcmdline \.= " crashkernel=256M dump=\$dump ";/, - 'a riscv64 diskless image with kdump enabled reserves a crash kernel by default', +is_deeply( + [ imgutils::resolver_library_paths('riscv64') ], + [ 'lib64/libnss_dns.so.2', 'lib64/libresolv.so.2' ], + 'riscv64 images take resolver libraries from lib64', ); +is_deeply( + [ imgutils::resolver_library_paths('ppc64') ], + [ 'lib/libnss_dns.so.2', 'lib/libresolv.so.2' ], + 'POWER images retain the lib resolver paths', +); + +is( + xCAT_plugin::anaconda::_default_crashkernel_args('riscv64', '/dev/vda2', 0, '', ''), + ' crashkernel=256M dump=/dev/vda2 ', + 'riscv64 images reserve the existing default crash kernel size', +); +is( + xCAT_plugin::anaconda::_default_crashkernel_args('x86_64', '/dev/sda2', 0, '', ''), + ' crashkernel=128M dump=/dev/sda2 ', + 'x86_64 keeps its existing default crash kernel arguments', +); +is( + xCAT_plugin::anaconda::_default_crashkernel_args('ppc64', '/dev/sda2', 0, '', ''), + ' crashkernel=256M@64M dump=/dev/sda2 ', + 'POWER keeps its existing default crash kernel arguments', +); +is( + xCAT_plugin::anaconda::_default_crashkernel_args('ppc64', 'nfs', 1, 'net,host:/dump', 'nfs://host/dump'), + ' fadump=on fadump_reserve_mem=512M fadump_target=net,host:/dump fadump_default=noreboot dump=nfs://host/dump ', + 'POWER fadump keeps its existing default arguments', +); + done_testing(); From ba6b880a5c268ef3b60db50ded424c041ac57a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:01 -0300 Subject: [PATCH 45/54] refactor(install): make RISC-V EFI fix-up testable --- .../xcat/install/scripts/post.rhels10.riscv64 | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 b/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 index 09245064c..3ede6003f 100644 --- a/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 +++ b/xCAT-server/share/xcat/install/scripts/post.rhels10.riscv64 @@ -1,3 +1,4 @@ +#!/bin/sh # riscv64 UEFI boot entry fix-up for EL10 kickstart installs. # # The EL10 anaconda has no RISC-V EFI platform: it asks for the x86 boot @@ -9,43 +10,49 @@ # stub next to it, so point the boot entry at that image and also place the # removable-media fallback loader \EFI\BOOT\BOOTRISCV64.EFI for firmware # without usable NVRAM entries. Runs in the installed system chroot. -if [ "$(uname -m)" = "riscv64" ] && [ -d /boot/efi/EFI ]; then +xcat_riscv_install_root=${XCAT_INSTALL_ROOT:-} +xcat_riscv_uname=${XCAT_UNAME:-uname} +xcat_riscv_efibootmgr=${XCAT_EFIBOOTMGR:-efibootmgr} +xcat_riscv_findmnt=${XCAT_FINDMNT:-findmnt} +xcat_riscv_lsblk=${XCAT_LSBLK:-lsblk} + +if [ "$("$xcat_riscv_uname" -m)" = "riscv64" ] && [ -d "$xcat_riscv_install_root/boot/efi/EFI" ]; then grubefi="" - for candidate in /boot/efi/EFI/*/grubriscv64.efi; do + for candidate in "$xcat_riscv_install_root"/boot/efi/EFI/*/grubriscv64.efi; do [ -f "$candidate" ] || continue grubefi="$candidate" break done if [ -n "$grubefi" ]; then vendordir="$(basename "$(dirname "$grubefi")")" - mkdir -p /boot/efi/EFI/BOOT - cp -f "$grubefi" /boot/efi/EFI/BOOT/BOOTRISCV64.EFI - echo "riscv64: installed \\EFI\\BOOT\\BOOTRISCV64.EFI from \\EFI\\$vendordir\\grubriscv64.efi" - if command -v efibootmgr >/dev/null 2>&1 && efibootmgr >/dev/null 2>&1; then - espdev="$(findmnt -no SOURCE /boot/efi 2>/dev/null)" + mkdir -p "$xcat_riscv_install_root/boot/efi/EFI/BOOT" + cp -f "$grubefi" "$xcat_riscv_install_root/boot/efi/EFI/BOOT/BOOTRISCV64.EFI" + printf '%s\n' "riscv64: installed \\EFI\\BOOT\\BOOTRISCV64.EFI from \\EFI\\$vendordir\\grubriscv64.efi" + if command -v "$xcat_riscv_efibootmgr" >/dev/null 2>&1 && "$xcat_riscv_efibootmgr" >/dev/null 2>&1; then + espdev="$("$xcat_riscv_findmnt" -no SOURCE "$xcat_riscv_install_root/boot/efi" 2>/dev/null)" # lsblk pads its output, and efibootmgr wants bare values - disk="$(lsblk -no PKNAME "$espdev" 2>/dev/null | head -1 | tr -d '[:space:]')" - part="$(lsblk -no PARTN "$espdev" 2>/dev/null | head -1 | tr -d '[:space:]')" + disk="$("$xcat_riscv_lsblk" -no PKNAME "$espdev" 2>/dev/null | head -1 | tr -d '[:space:]')" + part="$("$xcat_riscv_lsblk" -no PARTN "$espdev" 2>/dev/null | head -1 | tr -d '[:space:]')" if [ -n "$disk" ] && [ -n "$part" ]; then # drop the x86 entries anaconda created for this install, and any # riscv64 entry a previous provisioning of this node left behind, so # reinstalling does not fill the firmware variable store with copies - for bootnum in $(efibootmgr -v 2>/dev/null | awk '/\\EFI\\[^\\]+\\(shimx64|grubx64|grubriscv64)\.efi/ { sub(/^Boot/, "", $1); sub(/\*$/, "", $1); print $1 }'); do - efibootmgr -q -b "$bootnum" -B || true + for bootnum in $("$xcat_riscv_efibootmgr" -v 2>/dev/null | awk '/\\EFI\\[^\\]+\\(shimx64|grubx64|grubriscv64)\.efi/ { sub(/^Boot/, "", $1); sub(/\*$/, "", $1); print $1 }'); do + "$xcat_riscv_efibootmgr" -q -b "$bootnum" -B || true done - label="$(. /etc/os-release 2>/dev/null; echo "${NAME:-Linux}")" - if efibootmgr -q -c -d "/dev/$disk" -p "$part" -L "$label" -l "\\EFI\\$vendordir\\grubriscv64.efi"; then - echo "riscv64: UEFI boot entry \"$label\" -> \\EFI\\$vendordir\\grubriscv64.efi on /dev/$disk part $part" + label="$(. "$xcat_riscv_install_root/etc/os-release" 2>/dev/null; echo "${NAME:-Linux}")" + if "$xcat_riscv_efibootmgr" -q -c -d "/dev/$disk" -p "$part" -L "$label" -l "\\EFI\\$vendordir\\grubriscv64.efi"; then + printf '%s\n' "riscv64: UEFI boot entry \"$label\" -> \\EFI\\$vendordir\\grubriscv64.efi on /dev/$disk part $part" else - echo "riscv64: efibootmgr could not create the boot entry; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" + printf '%s\n' "riscv64: efibootmgr could not create the boot entry; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" fi else - echo "riscv64: could not determine the ESP disk/partition; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" + printf '%s\n' "riscv64: could not determine the ESP disk/partition; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" fi else - echo "riscv64: efibootmgr unavailable; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" + printf '%s\n' "riscv64: efibootmgr unavailable; the firmware will use \\EFI\\BOOT\\BOOTRISCV64.EFI" fi else - echo "riscv64: no \\EFI\\*\\grubriscv64.efi on the ESP; is grub2-efi-riscv64 in the package list?" + printf '%s\n' "riscv64: no \\EFI\\*\\grubriscv64.efi on the ESP; is grub2-efi-riscv64 in the package list?" fi fi From 6671f258cde1c99163716573308187c39df8994c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:01 -0300 Subject: [PATCH 46/54] test(install): exercise RISC-V EFI fix-up --- xCAT-test/unit/install_profile_riscv64.t | 148 +++++++++++++++++------ 1 file changed, 113 insertions(+), 35 deletions(-) diff --git a/xCAT-test/unit/install_profile_riscv64.t b/xCAT-test/unit/install_profile_riscv64.t index 4a61ccae6..117110baf 100644 --- a/xCAT-test/unit/install_profile_riscv64.t +++ b/xCAT-test/unit/install_profile_riscv64.t @@ -2,7 +2,10 @@ use strict; use warnings; +use File::Path qw(make_path); +use File::Slurper qw(read_text write_text); use File::Spec; +use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; use Test::More; @@ -58,43 +61,118 @@ for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { } } -# The kickstart %post is a single shell script: xCAT splices every #INCLUDE: inline, so a -# top-level "exit" in an earlier included script ends the whole section. The riscv64 boot -# entry fix-up must run, so it has to come before any script that exits. -for my $family ( [ 'rocky', 'rocky10' ], [ 'rh', 'rhels10' ] ) { - my ( $dir, $osbase ) = @$family; - for my $profile (qw(compute service)) { - my $t = slurp_repo_file( - File::Spec->catfile( $install, $dir, "$profile.$osbase.riscv64.tmpl" ) - ); - my ($section) = $t =~ /^(%post\b.*?)^%end/ms; - ok( $section, "$dir/$profile.$osbase riscv64 template has a %post section" ) - or next; - my @scripts = $section =~ m{^#INCLUDE:\#ENV:XCATROOT\#/share/xcat/install/scripts/(\S+?)\#$}mg; - my ($index) = grep { $scripts[$_] eq 'post.rhels10.riscv64' } 0 .. $#scripts; - ok( defined $index, "$dir/$profile.$osbase riscv64 template includes the fix-up in the %post section" ) - or next; - for my $earlier ( @scripts[ 0 .. $index - 1 ] ) { - my $body = slurp_repo_file( - File::Spec->catfile( $install, 'scripts', $earlier ) - ); - unlike( $body, qr/^\s*exit\b/m, - "$dir/$profile.$osbase runs $earlier before the riscv64 fix-up, and $earlier does not end the %post" ); - } - } -} - my $post = File::Spec->catfile( $install, 'scripts', 'post.rhels10.riscv64' ); my $post_path = repo_path($post); ok( -r $post_path, 'post.rhels10.riscv64 exists' ); -my $bash = `bash -n $post_path 2>&1`; -is( $bash, '', 'post.rhels10.riscv64 parses as bash' ); -my $s = slurp_repo_file($post); -like( $s, qr/\[ "\$\(uname -m\)" = "riscv64" \]/, 'the fix-up only acts on riscv64' ); -like( $s, qr{/boot/efi/EFI/\*/grubriscv64\.efi}, 'the fix-up locates the distro grub2 UEFI image on the ESP' ); -like( $s, qr{cp -f "\$grubefi" /boot/efi/EFI/BOOT/BOOTRISCV64\.EFI}, 'the fix-up installs the removable-media fallback loader' ); -like( $s, qr/shimx64\|grubx64\|grubriscv64/, 'the fix-up removes the x86 boot entries anaconda registered and the riscv64 entry of an earlier install' ); -like( $s, qr/efibootmgr -q -c -d "\/dev\/\$disk" -p "\$part" -L "\$label" -l "\\\\EFI\\\\\$vendordir\\\\grubriscv64\.efi"/, 'the fix-up registers grubriscv64.efi as the boot entry' ); -unlike( $s, qr/set -e/, 'the fix-up never aborts the kickstart %post' ); + +is(system('sh', '-n', $post_path), 0, 'post.rhels10.riscv64 parses as POSIX shell'); + +my $tools = tempdir(CLEANUP => 1); +my $efi_log = File::Spec->catfile($tools, 'efibootmgr.log'); + +sub fake_command { + my ($name, $body) = @_; + my $path = File::Spec->catfile($tools, $name); + write_text($path, "#!/bin/sh\n$body"); + chmod 0755, $path or die "Unable to make $path executable: $!"; + return $path; +} + +my $uname = fake_command('uname', <<'SH'); +printf '%s\n' "${XCAT_TEST_ARCH:-riscv64}" +SH +my $findmnt = fake_command('findmnt', <<'SH'); +printf '/dev/vda1\n' +SH +my $lsblk = fake_command('lsblk', <<'SH'); +case "$*" in + *PKNAME*) printf "vda\n" ;; + *PARTN*) printf "1\n" ;; +esac +SH +my $efibootmgr = fake_command('efibootmgr', <<'SH'); +printf '%s\n' "$*" >> "$XCAT_EFI_LOG" +case "$*" in + '') exit 0 ;; + '-v') + printf 'Boot0001* Rocky HD(1,GPT,...)/File(\\EFI\\rocky\\shimx64.efi)\n' + printf 'Boot0002* OldRiscv HD(1,GPT,...)/File(\\EFI\\rocky\\grubriscv64.efi)\n' + printf 'Boot0003* Network PXE\n' + exit 0 + ;; + *'-c'*) exit "${XCAT_EFI_CREATE_RC:-0}" ;; +esac +exit 0 +SH + +sub installed_root { + my ($with_loader) = @_; + my $root = tempdir(CLEANUP => 1); + make_path(File::Spec->catdir($root, 'boot', 'efi', 'EFI', 'rocky')); + make_path(File::Spec->catdir($root, 'etc')); + write_text(File::Spec->catfile($root, 'etc', 'os-release'), "NAME=\"Rocky Linux\"\n"); + if ($with_loader) { + write_text( + File::Spec->catfile($root, 'boot', 'efi', 'EFI', 'rocky', 'grubriscv64.efi'), + "riscv loader\n", + ); + } + return $root; +} + +sub run_post { + my ($root, %extra) = @_; + local %ENV = ( + %ENV, + XCAT_INSTALL_ROOT => $root, + XCAT_UNAME => $uname, + XCAT_EFIBOOTMGR => $efibootmgr, + XCAT_FINDMNT => $findmnt, + XCAT_LSBLK => $lsblk, + XCAT_EFI_LOG => $efi_log, + XCAT_TEST_ARCH => 'riscv64', + %extra, + ); + open(my $fh, '-|', 'sh', $post_path) or die "Unable to run $post_path: $!"; + my $output = do { local $/; <$fh> }; + close($fh); + return ($? >> 8, $output); +} + +my $root = installed_root(1); +my ($status, $output) = run_post($root); +is($status, 0, 'the RISC-V fix-up completes successfully'); +is( + read_text(File::Spec->catfile($root, 'boot', 'efi', 'EFI', 'BOOT', 'BOOTRISCV64.EFI')), + "riscv loader\n", + 'the distro loader is copied to the removable-media fallback path', +); +like($output, qr/UEFI boot entry "Rocky Linux"/, 'the created UEFI entry is reported'); +my $efi_calls = read_text($efi_log); +like($efi_calls, qr/^-q -b 0001 -B$/m, 'the invalid x86 boot entry is removed'); +like($efi_calls, qr/^-q -b 0002 -B$/m, 'the prior RISC-V boot entry is removed'); +unlike($efi_calls, qr/0003/, 'unrelated firmware entries are retained'); +like( + $efi_calls, + qr/^-q -c -d \/dev\/vda -p 1 -L Rocky Linux -l \\EFI\\rocky\\grubriscv64\.efi$/m, + 'the new entry points at the distro RISC-V loader on the ESP disk', +); + +unlink($efi_log); +my $x86_root = installed_root(1); +($status, $output) = run_post($x86_root, XCAT_TEST_ARCH => 'x86_64'); +is($status, 0, 'the post-install script is a no-op on x86_64'); +is($output, '', 'the x86_64 no-op reports nothing'); +ok(!-e $efi_log, 'the x86_64 no-op never invokes efibootmgr'); + +my $bare_root = installed_root(0); +($status, $output) = run_post($bare_root); +is($status, 0, 'a missing distro loader remains nonfatal'); +like($output, qr/no \\EFI\\\*\\grubriscv64\.efi/, 'a missing distro loader is diagnosed'); + +unlink($efi_log); +($status, $output) = run_post($root, XCAT_EFI_CREATE_RC => 1); +is($status, 0, 'an efibootmgr create failure remains nonfatal'); +like($output, qr/firmware will use \\EFI\\BOOT\\BOOTRISCV64\.EFI/, 'the fallback path is reported after an efibootmgr failure'); done_testing(); From 63db0a82ddf00df8c0618f48bfe53b1597076952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:01 -0300 Subject: [PATCH 47/54] refactor(build): share RPM architecture sets --- build-utils/rpm-architectures.sh | 6 ++++++ buildcore.sh | 3 ++- buildlocal.sh | 33 +++++++++++++++----------------- 3 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 build-utils/rpm-architectures.sh diff --git a/build-utils/rpm-architectures.sh b/build-utils/rpm-architectures.sh new file mode 100644 index 000000000..80c215796 --- /dev/null +++ b/build-utils/rpm-architectures.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# Architecture sets shared by the RPM build entry points and their tests. +XCAT_CORE_RPM_ARCHES="x86_64 ppc64 ppc64le s390x aarch64 riscv64" +XCAT_LOCAL_RPM_ARCHES="x86_64 ppc64 s390x aarch64 riscv64" +XCAT_LOCAL_COLLECT_ARCHES="noarch x86_64 ppc64 riscv64" diff --git a/buildcore.sh b/buildcore.sh index fbe85a7bf..fb0ebe503 100755 --- a/buildcore.sh +++ b/buildcore.sh @@ -37,6 +37,7 @@ SCRIPT=$(readlink -f $0) SCRIPTPATH=`dirname $SCRIPT` +. "$SCRIPTPATH/build-utils/rpm-architectures.sh" . "$SCRIPTPATH/build-utils/source-only.sh" . "$SCRIPTPATH/build-utils/buildcore-source-only.sh" @@ -392,7 +393,7 @@ for rpmname in xCAT xCATsn; do ./makerpm $rpmname "$EMBED" if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname"; fi else - for arch in $(xcat_rpm_build_arches x86_64 ppc64 ppc64le s390x aarch64 riscv64); do + for arch in $(xcat_rpm_build_arches $XCAT_CORE_RPM_ARCHES); do if [ "$rpmname" = "xCAT-OpenStack" -a "$arch" != "x86_64" ] || [ "$rpmname" = "xCAT-OpenStack-baremetal" -a "$arch" != "x86_64" ] ; then continue; fi # only bld openstack for x86_64 for now ./makerpm $rpmname $arch "$EMBED" if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname-$arch"; fi diff --git a/buildlocal.sh b/buildlocal.sh index c5cc13f78..f8e94e8a5 100755 --- a/buildlocal.sh +++ b/buildlocal.sh @@ -20,6 +20,8 @@ if [ -z "$CURDIR" ]; then CURDIR=$(pwd) fi +. "$CURDIR/build-utils/rpm-architectures.sh" + echo "CURDIR is $CURDIR" echo "OSNAME is $OSNAME!" echo "NAMEALL is $NAMEALL" @@ -87,15 +89,13 @@ else echo "This is an $OSNAME system" if [ "$OS" = "SUSE" ]; then - rm -rf /usr/src/packages/RPMS/noarch/* - rm -rf /usr/src/packages/RPMS/x86_64/* - rm -rf /usr/src/packages/RPMS/ppc64/* - rm -rf /usr/src/packages/RPMS/riscv64/* + for arch in $XCAT_LOCAL_COLLECT_ARCHES; do + rm -rf /usr/src/packages/RPMS/$arch/* + done else - rm -rf /root/rpmbuild/RPMS/noarch/* - rm -rf /root/rpmbuild/RPMS/x86_64/* - rm -rf /root/rpmbuild/RPMS/ppc64/* - rm -rf /root/rpmbuild/RPMS/riscv64/* + for arch in $XCAT_LOCAL_COLLECT_ARCHES; do + rm -rf /root/rpmbuild/RPMS/$arch/* + done fi mkdir -p $CURDIR/build/ @@ -125,7 +125,7 @@ echo "This is an $OSNAME system" $CURDIR/makerpm $rpmname if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname"; fi else - for arch in x86_64 ppc64 s390x aarch64 riscv64; do + for arch in $XCAT_LOCAL_RPM_ARCHES; do $CURDIR/makerpm $rpmname $arch if [ $? -ne 0 ]; then FAILEDRPMS="$FAILEDRPMS $rpmname-$arch"; fi done @@ -133,15 +133,13 @@ echo "This is an $OSNAME system" done if [ "$OS" = "SUSE" ]; then - cp /usr/src/packages/RPMS/noarch/* $CURDIR/build/ - cp /usr/src/packages/RPMS/x86_64/* $CURDIR/build/ - cp /usr/src/packages/RPMS/ppc64/* $CURDIR/build/ - cp /usr/src/packages/RPMS/riscv64/* $CURDIR/build/ + for arch in $XCAT_LOCAL_COLLECT_ARCHES; do + cp /usr/src/packages/RPMS/$arch/* $CURDIR/build/ + done else - cp /root/rpmbuild/RPMS/noarch/* $CURDIR/build/ - cp /root/rpmbuild/RPMS/x86_64/* $CURDIR/build/ - cp /root/rpmbuild/RPMS/ppc64/* $CURDIR/build/ - cp /root/rpmbuild/RPMS/riscv64/* $CURDIR/build/ + for arch in $XCAT_LOCAL_COLLECT_ARCHES; do + cp /root/rpmbuild/RPMS/$arch/* $CURDIR/build/ + done fi #begin to create repo for redhat platform @@ -165,4 +163,3 @@ EOF fi - From 36c5d6d4340323093462ae1ac970282b18890f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:25:01 -0300 Subject: [PATCH 48/54] test(build): exercise RPM architecture sets --- xCAT-test/unit/riscv64_packaging.t | 41 ++++++++++++++++++------------ 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t index aed324d63..af6b40838 100644 --- a/xCAT-test/unit/riscv64_packaging.t +++ b/xCAT-test/unit/riscv64_packaging.t @@ -64,24 +64,33 @@ like( like( $server, qr/^Requires: perl-Net-Telnet perl-Net-DNS perl-Crypt-CBC perl-Crypt-Rijndael$/m, 'xCAT-server.spec keeps the other EL perl requires' ); -my $buildcore = slurp_repo_file('buildcore.sh'); -like( $buildcore, qr/^\s*for arch in x86_64 ppc64 ppc64le s390x aarch64 riscv64; do$/m, 'buildcore.sh builds xCAT and xCATsn for riscv64' ); +my $architectures = repo_path('build-utils/rpm-architectures.sh'); +is(system('sh', '-n', $architectures), 0, 'the shared build architecture data parses as POSIX shell'); +open( + my $arch_fh, + '-|', + 'sh', '-c', + '. "$1"; printf "%s\n%s\n%s\n" "$XCAT_CORE_RPM_ARCHES" "$XCAT_LOCAL_RPM_ARCHES" "$XCAT_LOCAL_COLLECT_ARCHES"', + 'sh', $architectures, +) or BAIL_OUT("unable to load $architectures: $!"); +my @architecture_sets = <$arch_fh>; +close($arch_fh) or BAIL_OUT("unable to read architecture data from $architectures"); +chomp @architecture_sets; -my $buildlocal = slurp_repo_file('buildlocal.sh'); -like( $buildlocal, qr/^\s*for arch in x86_64 ppc64 s390x aarch64 riscv64; do$/m, 'buildlocal.sh builds xCAT and xCATsn for riscv64' ); -like( $buildlocal, qr{^\s*cp /root/rpmbuild/RPMS/riscv64/\* \$CURDIR/build/$}m, 'buildlocal.sh collects riscv64 rpms' ); - -my $buildrpms = slurp_repo_file('buildrpms.pl'); -like( $buildrpms, qr/forcearch/, 'buildrpms.pl documents the forcearch mock configuration for riscv64 builds' ); -unlike( - $buildrpms, - qr/^use XCAT::BuildUtils\b/m, - 'the dependency bootstrap does not treat the repository build utility as an RPM dependency' +is( + $architecture_sets[0], + 'x86_64 ppc64 ppc64le s390x aarch64 riscv64', + 'the release build includes riscv64 without changing its existing architecture set', ); -like( - $buildrpms, - qr{require "\$Bin/build-utils/lib/XCAT/BuildUtils\.pm";}, - 'buildrpms loads the repository build utility by path at runtime' +is( + $architecture_sets[1], + 'x86_64 ppc64 s390x aarch64 riscv64', + 'the local build includes riscv64 without changing its existing architecture set', +); +is( + $architecture_sets[2], + 'noarch x86_64 ppc64 riscv64', + 'the local build collects the riscv64 packages it produces', ); # buildrpms.pl derives the rpm architecture from the mock target name through From 7d3cd9bd6dffa3d072d65fb8d23a0682e9e06061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:41:28 -0300 Subject: [PATCH 49/54] fix(netboot): use the HTTP tftp alias --- perl-xCAT/xCAT/DHCP/BootPolicy.pm | 6 +++--- xCAT-server/lib/xcat/plugins/mknb.pm | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/perl-xCAT/xCAT/DHCP/BootPolicy.pm b/perl-xCAT/xCAT/DHCP/BootPolicy.pm index ba33b89b6..3c6c976ef 100644 --- a/perl-xCAT/xCAT/DHCP/BootPolicy.pm +++ b/perl-xCAT/xCAT/DHCP/BootPolicy.pm @@ -76,6 +76,7 @@ sub kea_httpboot_network_classes { my $portsuffix = ( $httpport eq '80' ) ? '' : ":$httpport"; my $tftpdir = $opts{tftpdir} || '/tftpboot'; $tftpdir =~ s{/+$}{}; + my $http_tftp_root = '/tftpboot'; my $present = $opts{loader_present}; my @classes; @@ -88,7 +89,7 @@ sub kea_httpboot_network_classes { name => $name, test => "option[93].hex == $spec->{arch_id}", additional_only => 1, - 'boot-file-name' => "http://$opts{next_server}$portsuffix$tftpdir/$spec->{loader}", + 'boot-file-name' => "http://$opts{next_server}$portsuffix$http_tftp_root/$spec->{loader}", 'option-data' => [ { name => 'vendor-class-identifier', @@ -107,7 +108,6 @@ sub isc_client_architecture_lines { my $tftp = $opts{next_server} // ''; my $portsuffix = $opts{portsuffix} // ''; - my $tftpdir = $opts{tftpdir} // '/tftpboot'; my $net = $opts{net} // ''; my $maskbits = $opts{prefix} // ''; @@ -135,7 +135,7 @@ sub isc_client_architecture_lines { " filename \"boot/grub2/grub2.riscv64\";\n", " } else if option client-architecture = 00:1c { #riscv64 uefi http boot\n ", " option vendor-class-identifier \"HTTPClient\";\n", - " filename \"http://$tftp$portsuffix$tftpdir/boot/grub2/grub2.riscv64\";\n", + " filename \"http://$tftp$portsuffix/tftpboot/boot/grub2/grub2.riscv64\";\n", " } else if option client-architecture = 00:0e { #OPAL-v3\n ", " option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/${net}_${maskbits}\";\n", " } else if substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { #for onie on cumulus switch\n", diff --git a/xCAT-server/lib/xcat/plugins/mknb.pm b/xCAT-server/lib/xcat/plugins/mknb.pm index c4b77db11..891310e2d 100644 --- a/xCAT-server/lib/xcat/plugins/mknb.pm +++ b/xCAT-server/lib/xcat/plugins/mknb.pm @@ -899,6 +899,7 @@ sub _write_grub2_discovery_config { # node that does not serve the TFTP root over HTTP. my $httpport = $args{httpport} || '80'; my $httproot = 'http,' . $args{xcatd_address} . ($httpport eq '80' ? '' : ":$httpport"); + my $http_tftp_root = '/tftpboot'; my $tftproot = 'tftp,' . $args{xcatd_address}; my $content = "# xCAT Genesis discovery for network $hexnet - generated by mknb, do not edit\n"; $content .= "set default=0\n"; @@ -914,8 +915,8 @@ sub _write_grub2_discovery_config { $content .= " insmod http\n"; $content .= " insmod tftp\n"; $content .= " set root=$httproot\n"; - $content .= " linux $tftpdir/$kernel $cmdline BOOTIF=\$net_default_mac\n"; - $content .= " initrd $tftpdir/$initrd\n"; + $content .= " linux $http_tftp_root/$kernel $cmdline BOOTIF=\$net_default_mac\n"; + $content .= " initrd $http_tftp_root/$initrd\n"; $content .= "}\n"; $content .= "menuentry \"xCAT Genesis $arch (TFTP)\" {\n"; $content .= " insmod tftp\n"; From 4d0c96a78c94c8b52993c97fbfbfc5a267cdbf24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:44:20 -0300 Subject: [PATCH 50/54] test(netboot): cover the HTTP tftp alias --- xCAT-test/unit/dhcp_boot_policy.t | 4 ++-- xCAT-test/unit/dhcp_isc_client_arch.t | 2 +- xCAT-test/unit/mknb_xcatd_address.t | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/xCAT-test/unit/dhcp_boot_policy.t b/xCAT-test/unit/dhcp_boot_policy.t index f73f78d91..e06aafdda 100644 --- a/xCAT-test/unit/dhcp_boot_policy.t +++ b/xCAT-test/unit/dhcp_boot_policy.t @@ -182,8 +182,8 @@ my $httpboot_port = xCAT::DHCP::BootPolicy->kea_httpboot_network_classes( ); is( $httpboot_port->[0]{'boot-file-name'}, - 'http://10.0.0.1:8080/srv/tftpboot/boot/grub2/grub2.riscv64', - 'the HTTP boot URL follows the configured HTTP port and TFTP root' + 'http://10.0.0.1:8080/tftpboot/boot/grub2/grub2.riscv64', + 'the HTTP boot URL follows the configured HTTP port and web alias' ); is_deeply( diff --git a/xCAT-test/unit/dhcp_isc_client_arch.t b/xCAT-test/unit/dhcp_isc_client_arch.t index b2bd38d00..8cec79091 100644 --- a/xCAT-test/unit/dhcp_isc_client_arch.t +++ b/xCAT-test/unit/dhcp_isc_client_arch.t @@ -28,7 +28,7 @@ like( ); like( $rendered, - qr/client-architecture = 00:1c \{ #riscv64 uefi http boot\n\s+option vendor-class-identifier "HTTPClient";\n\s+filename "http:\/\/192\.0\.2\.10:8080\/srv\/tftp\/boot\/grub2\/grub2\.riscv64";/, + qr/client-architecture = 00:1c \{ #riscv64 uefi http boot\n\s+option vendor-class-identifier "HTTPClient";\n\s+filename "http:\/\/192\.0\.2\.10:8080\/tftpboot\/boot\/grub2\/grub2\.riscv64";/, 'the ISC policy renders the riscv64 HTTP boot branch with the subnet URL', ); like( diff --git a/xCAT-test/unit/mknb_xcatd_address.t b/xCAT-test/unit/mknb_xcatd_address.t index df8167dc8..aa2c77d4b 100644 --- a/xCAT-test/unit/mknb_xcatd_address.t +++ b/xCAT-test/unit/mknb_xcatd_address.t @@ -364,12 +364,12 @@ like( like($grub_cfg, qr/^menuentry "xCAT Genesis riscv64" \{$/m, 'the menu entry names the architecture'); like( $grub_cfg, - qr/^ linux \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3001 BOOTIF=\$net_default_mac$/m, + qr{^ linux /tftpboot/xcat/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3001 BOOTIF=\$net_default_mac$}m, 'the kernel line loads the Genesis kernel with the xcatd endpoint and the booting MAC', ); like( $grub_cfg, - qr/^ initrd \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.fs\.riscv64\.gz$/m, + qr{^ initrd /tftpboot/xcat/genesis\.fs\.riscv64\.gz$}m, 'the initrd line loads the Genesis initramfs', ); @@ -412,7 +412,7 @@ $responses = run_mknb('riscv64'); generation_succeeded($responses, 'riscv64 configuration generation succeeds with a serial console'); like( read_config($grub_cfg_path), - qr/^ linux \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3002 console=tty0 console=ttyS0,115200n8r BOOTIF=\$net_default_mac$/m, + qr{^ linux /tftpboot/xcat/genesis\.kernel\.riscv64 xcatd=192\.168\.148\.10:3002 console=tty0 console=ttyS0,115200n8r BOOTIF=\$net_default_mac$}m, 'the serial console and a non-default xcatd port are carried into the kernel line', ); %xCAT::TableUtils::site_extra = (); @@ -441,7 +441,7 @@ $responses = run_mknb('riscv64'); generation_succeeded($responses, 'riscv64 configuration generation succeeds with an lzma initramfs'); like( read_config($grub_cfg_path), - qr/^ initrd \Q$xCAT::TableUtils::tftpdir\E\/xcat\/genesis\.fs\.riscv64\.lzma$/m, + qr{^ initrd /tftpboot/xcat/genesis\.fs\.riscv64\.lzma$}m, 'the lzma Genesis initramfs is preferred over the gzip one', ); unlink("$xCAT::TableUtils::tftpdir/xcat/genesis.fs.riscv64.lzma"); From f41d5e6cd16455727d62a5af20f5fc990da8b1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:31:52 -0300 Subject: [PATCH 51/54] fix(dhcp): normalize all Kea option flags Render the boolean fields in global and subnet option-data through the same policy already used for client classes, for both DHCPv4 and DHCPv6. --- perl-xCAT/xCAT/DHCP/Backend/Kea.pm | 48 +++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm index b6a48cc06..d4a54ba9d 100644 --- a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm +++ b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm @@ -89,7 +89,8 @@ sub render_dhcp4_config { $dhcp4{'hooks-libraries'} = $intent->{'hooks-libraries'} if $intent->{'hooks-libraries'}; $dhcp4{'option-def'} = $intent->{'option-def'} if $intent->{'option-def'}; $dhcp4{'client-classes'} = [ map { $self->_render_client_class($_) } @{ $intent->{'client-classes'} } ] if $intent->{'client-classes'}; - $dhcp4{'option-data'} = $intent->{'option-data'} if $intent->{'option-data'}; + $dhcp4{'option-data'} = _render_option_data( $intent->{'option-data'} ) + if $intent->{'option-data'}; $dhcp4{'dhcp-ddns'} = $intent->{'dhcp-ddns'} if $intent->{'dhcp-ddns'}; foreach my $field (qw/ddns-send-updates ddns-override-no-update ddns-override-client-update ddns-qualifying-suffix ddns-update-on-renew/) { $dhcp4{$field} = $intent->{$field} if exists $intent->{$field}; @@ -119,7 +120,8 @@ sub render_dhcp6_config { $dhcp6{'control-socket'} = $intent->{'control-socket'} if $intent->{'control-socket'}; $dhcp6{'hooks-libraries'} = $intent->{'hooks-libraries'} if $intent->{'hooks-libraries'}; - $dhcp6{'option-data'} = $intent->{'option-data'} if $intent->{'option-data'}; + $dhcp6{'option-data'} = _render_option_data( $intent->{'option-data'} ) + if $intent->{'option-data'}; $dhcp6{'dhcp-ddns'} = $intent->{'dhcp-ddns'} if $intent->{'dhcp-ddns'}; foreach my $field (qw/ddns-send-updates ddns-override-no-update ddns-override-client-update ddns-qualifying-suffix ddns-update-on-renew/) { $dhcp6{$field} = $intent->{$field} if exists $intent->{$field}; @@ -696,11 +698,11 @@ sub live_upsert_reservations { my $delete = $self->_live_delete_reservation($reservation, service => $service, ignore_not_found => 1); return $delete if $delete->{error}; - my %stored = %$reservation; + my ($stored) = @{ _render_entry_option_data( [$reservation] ) }; my $result = $self->control_agent_command( 'reservation-add', { - reservation => \%stored, + reservation => $stored, 'operation-target' => 'memory', }, service => $service, @@ -767,7 +769,9 @@ sub _render_subnet4 { $rendered{interface} = $subnet->{interface} if defined $subnet->{interface}; $rendered{'next-server'} = _first_defined( $subnet->{'next-server'}, $subnet->{next_server} ); $rendered{'boot-file-name'} = _first_defined( $subnet->{'boot-file-name'}, $subnet->{boot_file_name} ); - $rendered{'option-data'} = _first_defined( $subnet->{'option-data'}, $subnet->{option_data} ); + $rendered{'option-data'} = _render_option_data( + _first_defined( $subnet->{'option-data'}, $subnet->{option_data} ) + ); my $additional_classes = _first_defined( $subnet->{additional_client_classes}, $subnet->{'evaluate-additional-classes'}, @@ -776,13 +780,14 @@ sub _render_subnet4 { $subnet->{require_client_classes}, ); $rendered{ $self->_additional_class_list_field() } = $additional_classes if defined $additional_classes; - $rendered{reservations} = $subnet->{reservations} if $subnet->{reservations}; + $rendered{reservations} = _render_entry_option_data( $subnet->{reservations} ) + if $subnet->{reservations}; delete $rendered{'next-server'} unless defined $rendered{'next-server'}; delete $rendered{'boot-file-name'} unless defined $rendered{'boot-file-name'}; delete $rendered{'option-data'} unless defined $rendered{'option-data'}; if ( $subnet->{pools} ) { - $rendered{pools} = $subnet->{pools}; + $rendered{pools} = _render_entry_option_data( $subnet->{pools} ); } elsif ( $subnet->{dynamicrange} ) { $rendered{pools} = [ xCAT::DHCP::Range->kea_pools( $subnet->{dynamicrange} ) ]; } else { @@ -838,6 +843,26 @@ sub _render_option_data { return \@rendered; } +sub _render_entry_option_data { + my ($entries) = @_; + + return $entries unless ref($entries) eq 'ARRAY'; + + my @rendered; + foreach my $entry (@$entries) { + if ( ref($entry) ne 'HASH' ) { + push @rendered, $entry; + next; + } + my %copy = %$entry; + $copy{'option-data'} = _render_option_data( $copy{'option-data'} ) + if $copy{'option-data'}; + push @rendered, \%copy; + } + + return \@rendered; +} + sub _render_subnet6 { my ( $self, $subnet ) = @_; @@ -847,12 +872,15 @@ sub _render_subnet6 { ); $rendered{interface} = $subnet->{interface} if defined $subnet->{interface}; - $rendered{'option-data'} = _first_defined( $subnet->{'option-data'}, $subnet->{option_data} ); - $rendered{reservations} = $subnet->{reservations} if $subnet->{reservations}; + $rendered{'option-data'} = _render_option_data( + _first_defined( $subnet->{'option-data'}, $subnet->{option_data} ) + ); + $rendered{reservations} = _render_entry_option_data( $subnet->{reservations} ) + if $subnet->{reservations}; delete $rendered{'option-data'} unless defined $rendered{'option-data'}; if ( $subnet->{pools} ) { - $rendered{pools} = $subnet->{pools}; + $rendered{pools} = _render_entry_option_data( $subnet->{pools} ); } elsif ( $subnet->{dynamicrange} ) { $rendered{pools} = [ xCAT::DHCP::Range->kea_pools( $subnet->{dynamicrange} ) ]; } else { From 7f1d9695a486c9f5c722c5af9d0415a9781207aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:32:22 -0300 Subject: [PATCH 52/54] test(dhcp): cover Kea option flags in every scope --- xCAT-test/unit/dhcp_kea_option_flags.t | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 xCAT-test/unit/dhcp_kea_option_flags.t diff --git a/xCAT-test/unit/dhcp_kea_option_flags.t b/xCAT-test/unit/dhcp_kea_option_flags.t new file mode 100644 index 000000000..9cb7b34d7 --- /dev/null +++ b/xCAT-test/unit/dhcp_kea_option_flags.t @@ -0,0 +1,139 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use JSON qw(decode_json); +use Test::More; + +use xCAT::DHCP::Backend::Kea; + +my $backend = xCAT::DHCP::Backend::Kea->new( kea_version => '2.4.1' ); + +sub option_flags { + return { + name => 'domain-name', + data => 'example.test', + 'always-send' => 1, + 'csv-format' => 0, + 'never-send' => 0, + }; +} + +sub assert_boolean_flags { + my ( $option, $scope ) = @_; + + foreach my $flag (qw/always-send csv-format never-send/) { + is( + ref( $option->{$flag} ), + 'JSON::PP::Boolean', + "$scope renders $flag as a JSON boolean", + ); + } + ok( $option->{'always-send'}, "$scope preserves a true flag" ); + ok( !$option->{'csv-format'}, "$scope preserves a false flag" ); + ok( !$option->{'never-send'}, "$scope preserves a second false flag" ); +} + +my $dhcp4 = decode_json( + $backend->render_dhcp4_config( + { + 'option-data' => [ option_flags() ], + subnet4 => [ + { + id => 1, + subnet => '192.0.2.0/24', + 'option-data' => [ option_flags() ], + reservations => [ + { + 'hw-address' => '00:11:22:33:44:55', + 'ip-address' => '192.0.2.10', + 'option-data' => [ option_flags() ], + }, + ], + pools => [ + { + pool => '192.0.2.100 - 192.0.2.110', + 'option-data' => [ option_flags() ], + }, + ], + }, + ], + } + ) +)->{Dhcp4}; + +assert_boolean_flags( $dhcp4->{'option-data'}[0], 'DHCPv4 global option-data' ); +assert_boolean_flags( $dhcp4->{subnet4}[0]{'option-data'}[0], 'DHCPv4 subnet option-data' ); +assert_boolean_flags( $dhcp4->{subnet4}[0]{reservations}[0]{'option-data'}[0], + 'DHCPv4 reservation option-data' ); +assert_boolean_flags( $dhcp4->{subnet4}[0]{pools}[0]{'option-data'}[0], + 'DHCPv4 pool option-data' ); + +my $dhcp6 = decode_json( + $backend->render_dhcp6_config( + { + 'option-data' => [ option_flags() ], + subnet6 => [ + { + id => 2, + subnet => '2001:db8::/64', + 'option-data' => [ option_flags() ], + reservations => [ + { + duid => '00:01:00:01:de:ad:be:ef', + 'ip-addresses' => ['2001:db8::10'], + 'option-data' => [ option_flags() ], + }, + ], + pools => [ + { + pool => '2001:db8::100 - 2001:db8::110', + 'option-data' => [ option_flags() ], + }, + ], + }, + ], + } + ) +)->{Dhcp6}; + +assert_boolean_flags( $dhcp6->{'option-data'}[0], 'DHCPv6 global option-data' ); +assert_boolean_flags( $dhcp6->{subnet6}[0]{'option-data'}[0], 'DHCPv6 subnet option-data' ); +assert_boolean_flags( $dhcp6->{subnet6}[0]{reservations}[0]{'option-data'}[0], + 'DHCPv6 reservation option-data' ); +assert_boolean_flags( $dhcp6->{subnet6}[0]{pools}[0]{'option-data'}[0], + 'DHCPv6 pool option-data' ); + +my @control_agent_payloads; +my $live_backend = xCAT::DHCP::Backend::Kea->new( + kea_version => '2.4.1', + control_agent_handler => sub { + my ($payload) = @_; + push @control_agent_payloads, $payload; + return { ok => 1 }; + }, +); +is_deeply( + $live_backend->live_upsert_reservations( + [ + { + 'hw-address' => '00:11:22:33:44:55', + 'ip-address' => '192.0.2.10', + 'option-data' => [ option_flags() ], + }, + ], + ), + { ok => 1 }, + 'live reservation update succeeds', +); +is( $control_agent_payloads[0]{command}, 'reservation-add', + 'live reservation update reaches the Kea add command' ); +assert_boolean_flags( + $control_agent_payloads[0]{arguments}{reservation}{'option-data'}[0], + 'live reservation option-data', +); + +done_testing(); From f7fd1e1fd206d1f7dad85308f07579c4b43ff085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:32:30 -0300 Subject: [PATCH 53/54] build(rpm): recommend the RISC-V Genesis image --- xCAT/debian/control | 2 +- xCAT/xCAT.spec | 1 + xCATsn/debian/control | 2 +- xCATsn/xCATsn.spec | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/xCAT/debian/control b/xCAT/debian/control index 22cf3e165..0481919e6 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -10,7 +10,7 @@ Homepage: https://xcat.org/ Package: xcat Architecture: amd64 ppc64el 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, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) -Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), 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 +Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), 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 Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT setup xCAT is Extreme Cluster/Cloud Administration Toolkit. xCAT offers complete diff --git a/xCAT/xCAT.spec b/xCAT/xCAT.spec index f052ea36e..febac8836 100644 --- a/xCAT/xCAT.spec +++ b/xCAT/xCAT.spec @@ -62,6 +62,7 @@ Requires: xCAT-probe = 4:%{version}-%{release} %if 0%{?fedora} || 0%{?rhel} >= 8 || 0%{?suse_version} >= 1500 Recommends: xCAT-genesis-openembedded-x86_64 Recommends: xCAT-genesis-openembedded-ppc64le +Recommends: xCAT-genesis-openembedded-riscv64 %endif %endif diff --git a/xCATsn/debian/control b/xCATsn/debian/control index d66ea6c13..281c92066 100644 --- a/xCATsn/debian/control +++ b/xCATsn/debian/control @@ -9,7 +9,7 @@ Homepage: https://xcat.org/ Package: xcatsn Architecture: amd64 ppc64el 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, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) -Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), 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 +Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), 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 Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT service node setup xCATsn is a service node management package intended for at-scale diff --git a/xCATsn/xCATsn.spec b/xCATsn/xCATsn.spec index f048ef370..1f8d9f47d 100644 --- a/xCATsn/xCATsn.spec +++ b/xCATsn/xCATsn.spec @@ -25,6 +25,7 @@ Requires: xCAT-probe = 4:%{version}-%{release} %if 0%{?fedora} || 0%{?rhel} >= 8 || 0%{?suse_version} >= 1500 Recommends: xCAT-genesis-openembedded-x86_64 Recommends: xCAT-genesis-openembedded-ppc64le +Recommends: xCAT-genesis-openembedded-riscv64 %endif # Match xCAT-genesis-scripts package naming by build architecture. From abb3fb16337d30e4710d5b9658f9d67c831637e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:32:40 -0300 Subject: [PATCH 54/54] test(build): cover the RISC-V Genesis recommendation --- xCAT-test/unit/genesis_openembedded_activation.t | 11 +++++++++++ xCAT-test/unit/riscv64_packaging.t | 2 ++ 2 files changed, 13 insertions(+) diff --git a/xCAT-test/unit/genesis_openembedded_activation.t b/xCAT-test/unit/genesis_openembedded_activation.t index 8294c57df..7aeef2c5c 100644 --- a/xCAT-test/unit/genesis_openembedded_activation.t +++ b/xCAT-test/unit/genesis_openembedded_activation.t @@ -13,6 +13,7 @@ my $rpm_weak_dependencies = join( '%if 0%{?fedora} || 0%{?rhel} >= 8 || 0%{?suse_version} >= 1500', 'Recommends: xCAT-genesis-openembedded-x86_64', 'Recommends: xCAT-genesis-openembedded-ppc64le', + 'Recommends: xCAT-genesis-openembedded-riscv64', '%endif', ); @@ -39,6 +40,11 @@ like( qr/^Recommends:.*\bxcat-genesis-openembedded-ppc64le\b/m, 'DEB installations recommend the first-class ppc64le image', ); +like( + $deb_control, + qr/^Recommends:.*\bxcat-genesis-openembedded-riscv64\b/m, + 'DEB installations recommend the first-class riscv64 image', +); unlike( $deb_control, qr/^Depends:.*\bxcat-genesis-openembedded-/m, @@ -63,6 +69,11 @@ like( qr/^Recommends:.*\bxcat-genesis-openembedded-ppc64le\b/m, 'DEB service nodes recommend the first-class ppc64le image', ); +like( + $sn_deb_control, + qr/^Recommends:.*\bxcat-genesis-openembedded-riscv64\b/m, + 'DEB service nodes recommend the first-class riscv64 image', +); my $go_xcat = slurp_repo_file('xCAT-server/share/xcat/tools/go-xcat'); unlike( diff --git a/xCAT-test/unit/riscv64_packaging.t b/xCAT-test/unit/riscv64_packaging.t index af6b40838..f4f456481 100644 --- a/xCAT-test/unit/riscv64_packaging.t +++ b/xCAT-test/unit/riscv64_packaging.t @@ -23,6 +23,7 @@ unlike( $xcat_rv || '', qr/xnba-undi|syslinux-xcat|elilo-xcat/, 'xCAT.spec does # riscv64 has no legacy Genesis package: the requirement must disappear rather than # resolve to an unsatisfiable name, and the OpenEmbedded image is recommended instead. like( $xcat, qr/^%\{\?genesistarch:Requires: xCAT-genesis-scripts-%\{genesistarch\} = 1:%\{version\}-%\{release\}\}$/m, 'xCAT.spec asks for the legacy Genesis package only where the architecture has one' ); +like( $xcat, qr/^Recommends: xCAT-genesis-openembedded-riscv64$/m, 'xCAT.spec recommends the RISC-V OpenEmbedded Genesis image' ); SKIP: { my $rpmspec = qx(command -v rpmspec 2>/dev/null); @@ -43,6 +44,7 @@ SKIP: { my $xcatsn = slurp_repo_file('xCATsn/xCATsn.spec'); like( $xcatsn, qr/^%ifarch riscv64\nRequires: ipmitool-xcat >= 1\.8\.17-1\n%endif$/m, 'xCATsn.spec requires ipmitool-xcat on riscv64' ); +like( $xcatsn, qr/^Recommends: xCAT-genesis-openembedded-riscv64$/m, 'xCATsn.spec recommends the RISC-V OpenEmbedded Genesis image' ); my $server = slurp_repo_file('xCAT-server/xCAT-server.spec'); like( $server, qr/^Recommends: perl-DB_File$/m, 'xCAT-server.spec recommends perl-DB_File on EL10 (riscv64 has no EPEL to provide it)' );