2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-05 20:47:55 +00:00
Files
xcat-core/xCAT-server/lib/xcat/plugins/mknb.pm
T
Vinícius Ferrão f945775317 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.
2026-09-01 10:50:22 -03:00

944 lines
38 KiB
Perl

package xCAT_plugin::mknb;
use strict;
use Digest::SHA ();
use File::Temp qw(tempdir);
use xCAT::Utils;
use xCAT::TableUtils;
use xCAT::NodeRange;
use File::Path;
use File::Copy;
my $GENESIS_EXPORT_MANIFEST = 'xcat-genesis.manifest';
my %GENESIS_ARCHITECTURES = map { $_ => 1 }
qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64);
sub _canonical_genesis_arch {
my ($arch) = @_;
return unless defined($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',
};
}
sub _select_network_addresses {
my ($network_addresses, $preferred_addresses) = @_;
my %preferred = map { $_ => 1 } grep { defined($_) } @{$preferred_addresses};
my %legacy;
my %selected;
foreach my $network (keys %{$network_addresses}) {
my $addresses = $network_addresses->{$network};
next unless @{$addresses};
$legacy{$network} = $addresses->[-1];
$selected{$network} = $addresses->[0];
foreach my $address (@{$addresses}) {
if (defined($address) && $preferred{$address}) {
$selected{$network} = $address;
last;
}
}
}
return (\%legacy, \%selected);
}
sub _select_genesis_source {
my ($xcatroot, $requested_arch) = @_;
my $arch = _canonical_genesis_arch($requested_arch);
return unless defined($arch);
return unless $GENESIS_ARCHITECTURES{$arch};
my $netboot = "$xcatroot/share/xcat/netboot";
my $openembedded = "$netboot/genesis-openembedded/$arch";
return ($openembedded, $arch, 'openembedded')
if -d $openembedded;
my $legacy_arch = $arch eq 'ppc64le' ? 'ppc64' : $arch;
my $legacy = "$netboot/genesis/$legacy_arch";
return ($legacy, $legacy_arch, 'legacy') if -d $legacy;
my $classic = "$netboot/$legacy_arch";
return ($classic, $legacy_arch, 'classic') if -d $classic;
return;
}
sub _genesis_export_manifest_present {
my ($directory) = @_;
my $manifest = "$directory/$GENESIS_EXPORT_MANIFEST";
return -e $manifest || -l $manifest;
}
sub _prebuilt_genesis_requested {
my ($directory) = @_;
foreach my $name (
$GENESIS_EXPORT_MANIFEST,
'kernel',
'initramfs.cpio.gz',
'SHA256SUMS'
)
{
my $path = "$directory/$name";
return 0 unless -e $path || -l $path;
}
return 1;
}
sub _validate_prebuilt_genesis_manifest {
my ($directory, $arch) = @_;
my $manifest = "$directory/$GENESIS_EXPORT_MANIFEST";
return "Missing Genesis export manifest: $manifest"
unless -f $manifest && !-l $manifest;
open(my $manifest_fh, '<:raw', $manifest)
or return "Unable to read Genesis export manifest: $manifest";
my %expected = (
format => 'xcat-genesis',
version => '1',
architecture => $arch,
);
my %values;
while (my $line = <$manifest_fh>) {
chomp($line);
unless ($line =~ /^([a-z][a-z0-9_-]*)=([A-Za-z0-9][A-Za-z0-9._+-]*)$/) {
close($manifest_fh);
return "Invalid Genesis export manifest entry: $line";
}
my ($name, $value) = ($1, $2);
unless (exists($expected{$name})) {
close($manifest_fh);
return "Unknown Genesis export manifest entry: $name";
}
if (exists($values{$name})) {
close($manifest_fh);
return "Duplicate Genesis export manifest entry: $name";
}
$values{$name} = $value;
}
close($manifest_fh);
foreach my $name (qw(format version architecture)) {
return "Missing Genesis export manifest entry: $name"
unless exists($values{$name});
return "Unsupported Genesis export $name: $values{$name}"
unless $values{$name} eq $expected{$name};
}
return;
}
sub _read_prebuilt_genesis_checksums {
my ($directory) = @_;
my $checksum_file = "$directory/SHA256SUMS";
return (undef, "Missing Genesis checksum file: $checksum_file")
unless -f $checksum_file && !-l $checksum_file;
open(my $checksum_fh, '<:raw', $checksum_file)
or return (undef, "Unable to read Genesis checksum file: $checksum_file");
my %expected;
while (my $line = <$checksum_fh>) {
chomp($line);
unless ($line =~ /^([0-9a-f]{64}) ([A-Za-z0-9][A-Za-z0-9._-]*)$/) {
close($checksum_fh);
return (undef, "Invalid Genesis checksum entry: $line");
}
my ($digest, $name) = ($1, $2);
if (exists($expected{$name})) {
close($checksum_fh);
return (undef, "Duplicate Genesis checksum entry: $name");
}
$expected{$name} = $digest;
}
close($checksum_fh);
return (\%expected, undef);
}
sub _sha256_file {
my ($path) = @_;
open(my $artifact_fh, '<:raw', $path)
or return (undef, "Unable to read Genesis artifact: $path");
my $digest = Digest::SHA->new(256)->addfile($artifact_fh)->hexdigest;
close($artifact_fh);
return ($digest, undef);
}
sub _install_prebuilt_genesis {
my ($source, $tftpdir, $arch) = @_;
return (undef, "Invalid Genesis export directory: $source")
unless -d $source && !-l $source;
my $manifest_error =
_validate_prebuilt_genesis_manifest($source, $arch);
return (undef, $manifest_error) if $manifest_error;
my ($expected, $checksum_error) =
_read_prebuilt_genesis_checksums($source);
return (undef, $checksum_error) if $checksum_error;
unless (exists($expected->{$GENESIS_EXPORT_MANIFEST})) {
return (undef,
"Missing Genesis checksum entry: $GENESIS_EXPORT_MANIFEST");
}
my ($manifest_digest, $manifest_digest_error) =
_sha256_file("$source/$GENESIS_EXPORT_MANIFEST");
return (undef, $manifest_digest_error) if $manifest_digest_error;
unless ($manifest_digest eq $expected->{$GENESIS_EXPORT_MANIFEST}) {
return (undef,
"Genesis checksum mismatch: $source/$GENESIS_EXPORT_MANIFEST");
}
my $destination_dir = "$tftpdir/xcat";
eval { mkpath($destination_dir) unless -d $destination_dir; };
return (undef, "Unable to create Genesis destination: $destination_dir")
unless -d $destination_dir;
my $suffix = xCAT::Utils::genpassword(24);
my @artifacts = (
[ 'kernel', "$destination_dir/genesis.kernel.$arch" ],
[ 'initramfs.cpio.gz', "$destination_dir/genesis.fs.$arch.gz" ],
[ $GENESIS_EXPORT_MANIFEST,
"$destination_dir/genesis.exact-arch.$arch" ],
);
my @staged;
foreach my $artifact (@artifacts) {
my ($name, $destination) = @{$artifact};
my $source_path = "$source/$name";
unless (-f $source_path && !-l $source_path) {
unlink(@staged);
return (undef, "Missing Genesis artifact: $source_path");
}
unless (exists($expected->{$name})) {
unlink(@staged);
return (undef, "Missing Genesis checksum entry: $name");
}
my $temporary = "$destination.$suffix.new";
unless (copy($source_path, $temporary) && chmod(0644, $temporary)) {
unlink(@staged, $temporary);
return (undef, "Unable to stage Genesis artifact: $source_path");
}
push(@staged, $temporary);
my ($digest, $digest_error) = _sha256_file($temporary);
if ($digest_error || $digest ne $expected->{$name}) {
unlink(@staged);
return (undef, $digest_error || "Genesis checksum mismatch: $source_path");
}
}
my %backups;
foreach my $artifact (@artifacts) {
my $destination = $artifact->[1];
next unless -e $destination || -l $destination;
unless (-f $destination && !-l $destination) {
unlink(@staged, values(%backups));
return (undef, "Invalid Genesis destination: $destination");
}
my $backup = "$destination.$suffix.old";
unless (copy($destination, $backup)) {
unlink(@staged, values(%backups));
return (undef, "Unable to preserve Genesis artifact: $destination");
}
$backups{$destination} = $backup;
}
my @installed;
foreach my $index (0 .. $#artifacts) {
my $destination = $artifacts[$index]->[1];
unless (rename($staged[$index], $destination)) {
my @rollback_errors;
foreach my $installed (reverse(@installed)) {
if ($backups{$installed}) {
push(@rollback_errors, $installed)
unless rename($backups{$installed}, $installed);
} else {
push(@rollback_errors, $installed) unless unlink($installed);
}
}
unlink(@staged[$index .. $#staged], values(%backups));
my $error = "Unable to install Genesis artifact: $destination";
$error .= "; unable to restore: " . join(', ', @rollback_errors)
if @rollback_errors;
return (undef, $error);
}
push(@installed, $destination);
}
unlink(values(%backups));
unlink("$destination_dir/genesis.fs.$arch.lzma");
return ("$destination_dir/genesis.fs.$arch.gz", undef);
}
sub _remove_openembedded_genesis {
my ($tftpdir, $requested_arch) = @_;
my $arch = _canonical_genesis_arch($requested_arch);
return (0, 'Missing Genesis architecture') unless defined($arch);
return (0, "Unsupported Genesis architecture: $requested_arch")
unless $GENESIS_ARCHITECTURES{$arch};
my $directory = "$tftpdir/xcat";
my @artifacts = (
"$directory/genesis.kernel.$arch",
"$directory/genesis.fs.$arch.gz",
"$directory/genesis.fs.$arch.lzma",
"$directory/genesis.exact-arch.$arch",
);
my $removed = 0;
my @failed;
foreach my $artifact (@artifacts) {
next unless -e $artifact || -l $artifact;
unless (unlink($artifact)) {
push(@failed, $artifact);
next;
}
$removed++;
}
if (@failed == 1) {
return ($removed, "Unable to remove Genesis artifact: $failed[0]");
}
if (@failed) {
return ($removed,
'Unable to remove Genesis artifacts: ' . join(', ', @failed));
}
return ($removed, undef);
}
sub genesis_lzma_command {
my ($have_lzma, $have_xz) = @_;
return 'lzma -C crc32 -9' if $have_lzma;
return 'xz --format=lzma -C crc32 -9' if $have_xz;
return;
}
sub process_request {
my $request = shift;
my $callback = shift;
my $serialport;
my $serialspeed;
my $serialflow;
my %nobootnicips = ();
my $initrd_file = undef;
my $invisibletouch = 0;
my $xcatdport = 3001;
my @entries = xCAT::TableUtils->get_site_attribute("defserialport");
my $t_entry = $entries[0];
if (defined($t_entry)) {
$serialport = $t_entry;
}
@entries = xCAT::TableUtils->get_site_attribute("defserialspeed");
$t_entry = $entries[0];
if (defined($t_entry)) {
$serialspeed = $t_entry;
}
@entries = xCAT::TableUtils->get_site_attribute("defserialflow");
$t_entry = $entries[0];
if (defined($t_entry)) {
$serialflow = $t_entry;
}
@entries = xCAT::TableUtils->get_site_attribute("xcatdport");
$t_entry = $entries[0];
if (defined($t_entry)) {
$xcatdport = $t_entry;
}
my $httpport="80";
my @hports=xCAT::TableUtils->get_site_attribute("httpport");
if ($hports[0]){
$httpport=$hports[0];
}
my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport";
@entries = xCAT::TableUtils->get_site_attribute("dhcpinterfaces");
$t_entry = $entries[0];
if (defined($t_entry)) {
my %nobootnics = ();
foreach my $dhcpif (split /;/, $t_entry) {
if ($dhcpif =~ /\|/) {
my $isself = 0;
(my $ngroup, $dhcpif) = split /\|/, $dhcpif;
foreach my $host (noderange($ngroup)) {
unless(xCAT::NetworkUtils->thishostisnot($host)) {
$isself = 1;
}
}
unless(xCAT::NetworkUtils->thishostisnot($ngroup)) {
$isself = 1;
}
unless ($isself) {
next;
}
}
foreach (split /[,\s]+/, $dhcpif) {
my ($nicname, $flag) = split /:/;
if ($flag and $flag =~ /noboot/i) {
$nobootnics{$nicname} = 1;
}
}
}
my $nicips = xCAT::NetworkUtils->get_nic_ip();
foreach (keys %$nicips) {
# To support tagged vlan, create entries in the hash for the
# interface name removing the physical interface ending:
# 'enP1p12s0f0.2@enP1p12s0f0' => 'enP1p12s0f0.2'
if ($_ =~ "@") {
my $newkey = $_;
$newkey =~ s/\@.*//g;
$$nicips{$newkey} = ${nicips}->{$_};
}
}
foreach (keys %nobootnics) {
if (defined($nicips->{$_})) {
$nobootnicips{$nicips->{$_}} = 1;
}
}
}
my $tftpdir = xCAT::TableUtils->getTftpDir();
my $requested_arch = $request->{arg}->[0];
if (!$requested_arch) {
$callback->({ error => "Need to specify architecture (x86, x86_64, ppc64, ppc64le, armv7hf, aarch64 or riscv64)" }, { errorcode => [1] });
return;
}
my $canonical_arch = _canonical_genesis_arch($requested_arch);
if (($request->{arg}->[1] // '') eq '--remove-openembedded') {
unless ($GENESIS_ARCHITECTURES{$canonical_arch}) {
$callback->({ error => "Unsupported Genesis architecture: $requested_arch", errorcode => [1] });
return;
}
my $source = "$::XCATROOT/share/xcat/netboot/genesis-openembedded/$canonical_arch";
if (-d $source || -l $source) {
$callback->({ error => "Cannot remove boot artifacts while OpenEmbedded Genesis $canonical_arch is installed", errorcode => [1] });
return;
}
my ($removed, $remove_error) =
_remove_openembedded_genesis($tftpdir, $canonical_arch);
if ($remove_error) {
$callback->({ error => $remove_error, errorcode => [1] });
return;
}
$callback->({ data => "Removed $removed OpenEmbedded Genesis artifacts for $canonical_arch" });
return;
}
my ($genesis_dir, $arch, $genesis_type) =
_select_genesis_source($::XCATROOT, $requested_arch);
unless (defined($genesis_dir) && -d $genesis_dir) {
$callback->({ error => "Unable to find a Genesis image for architecture $requested_arch", errorcode => [1] });
return;
}
if ($canonical_arch eq 'ppc64le' && $arch eq 'ppc64') {
my $marker = "$tftpdir/xcat/genesis.exact-arch.ppc64";
if (-e $marker || -l $marker) {
$callback->({
error => 'Cannot use the legacy ppc64le fallback while a canonical ppc64 image is published',
errorcode => [1],
});
return;
}
}
if ($requested_arch eq 'ppc64el') {
$callback->({ data => 'Using the canonical architecture name ppc64le' });
}
if (($requested_arch eq 'ppc64le' || $requested_arch eq 'ppc64el')
&& $arch eq 'ppc64') {
$callback->({ data => 'OpenEmbedded ppc64le is not installed, using the legacy ppc64 image' });
}
$request->{arg}->[0] = $arch;
my $configfileonly = $request->{arg}->[1];
if ($configfileonly and $configfileonly ne "-c" and $configfileonly ne "--configfileonly") {
$callback->({ error => "The option $configfileonly is not supported", errorcode => [1] });
return;
} elsif ($configfileonly) {
goto CREAT_CONF_FILE;
}
if (_prebuilt_genesis_requested($genesis_dir)) {
my $image_name = $genesis_type eq 'openembedded'
? 'OpenEmbedded Genesis'
: 'exported Genesis';
$callback->({ data => ["Installing $image_name image for $arch"] });
my ($installed_initrd, $install_error) =
_install_prebuilt_genesis($genesis_dir, $tftpdir, $arch);
if ($install_error) {
$callback->({ error => [$install_error], errorcode => [1] });
return;
}
$initrd_file = $installed_initrd;
$invisibletouch = 1;
goto CREAT_CONF_FILE;
}
if ($genesis_type eq 'openembedded'
|| _genesis_export_manifest_present($genesis_dir)) {
$callback->({
error => ["Incomplete Genesis export: $genesis_dir"],
errorcode => [1],
});
return;
}
# Grab all the standard ssh public keys we can
my @ssh_pub_keys = ();
if (-r "/root/.ssh/id_rsa.pub") {
push(@ssh_pub_keys, 'id_rsa.pub');
}
if (-r "/root/.ssh/id_ed25519.pub") {
push(@ssh_pub_keys, 'id_ed25519.pub');
}
if (-r "/root/.ssh/id_ecdsa.pub") {
push(@ssh_pub_keys, 'id_ecdsa.pub');
}
if (scalar @ssh_pub_keys == 0) {
# We have no public keys.
# See if we have any private keys we can extract pubkeys from
if (-r "/root/.ssh/id_rsa") {
$callback->({ data => ["Extracting rsa ssh public key from private key"] });
my $rc = system('ssh-keygen -y -f /root/.ssh/id_rsa > /root/.ssh/id_rsa.pub');
if ($rc) {
$callback->({ error => ["Failure executing ssh-keygen for root when extracting rsa ssh public key from private key"], errorcode => [1] });
} else {
push(@ssh_pub_keys, 'id_rsa.pub');
}
} elsif (-r "/root/.ssh/id_ed25519") {
$callback->({ data => ["Extracting ed25519 ssh public key from private key"] });
my $rc = system('ssh-keygen -y -f /root/.ssh/id_ed25519 > /root/.ssh/id_ed25519.pub');
if ($rc) {
$callback->({ error => ["Failure executing ssh-keygen for root when extracting ed25519 ssh public key from private key"], errorcode => [1] });
} else {
push(@ssh_pub_keys, 'id_ed25519.pub');
}
} elsif (-r "/root/.ssh/id_ecdsa") {
$callback->({ data => ["Extracting ecdsa ssh public key from private key"] });
my $rc = system('ssh-keygen -y -f /root/.ssh/id_ecdsa > /root/.ssh/id_ecdsa.pub');
if ($rc) {
$callback->({ error => ["Failure executing ssh-keygen for root when extracting ecdsa ssh public key from private key"], errorcode => [1] });
} else {
push(@ssh_pub_keys, 'id_ecdsa.pub');
}
}
}
if (scalar @ssh_pub_keys == 0) {
# Looks like we didn't have any private keys either, so generate one
$callback->({ data => ["Generating rsa ssh private key for root"] });
my $rc = system('ssh-keygen -t rsa -q -b 2048 -N "" -f /root/.ssh/id_rsa');
if ($rc) {
$callback->({ error => ["Failure executing ssh-keygen for root when generating rsa ssh private key"], errorcode => [1] });
} else {
push(@ssh_pub_keys, 'id_rsa.pub');
}
}
my $tempdir = tempdir("mknb.$$.XXXXXX", TMPDIR => 1);
unless ($tempdir) {
$callback->({ error => ["Failed to create a temporary directory"], errorcode => [1] });
return;
}
unless (-e "$tftpdir/xcat") {
mkpath("$tftpdir/xcat");
}
my $rc;
if ($genesis_type eq 'legacy') {
$rc = system("shopt -s dotglob; GLOBIGNORE=\".:..\" cp -a $genesis_dir/fs/* $tempdir");
$rc = system("cp -a $genesis_dir/kernel $tftpdir/xcat/genesis.kernel.$arch");
$invisibletouch = 1;
} else {
$rc = system("cp -a $genesis_dir/nbroot/* $tempdir");
}
if ($rc) {
system("rm -rf $tempdir");
if ($invisibletouch) {
$callback->({ error => ["Failed to copy $genesis_dir/fs contents"], errorcode => [1] });
} else {
$callback->({ error => ["Failed to copy $genesis_dir/nbroot contents"], errorcode => [1] });
}
return;
}
my $sshdir;
if ($invisibletouch) {
$sshdir = "/.ssh";
} else {
$sshdir = "/root/.ssh";
}
mkpath($tempdir . "$sshdir");
chmod(0700, $tempdir . "$sshdir");
open(my $authkeys_fh, '>:raw', "$tempdir$sshdir/authorized_keys");
foreach my $keyfile (@ssh_pub_keys) {
open(my $pubkey_fh, '<:raw', "/root/.ssh/$keyfile");
while(my $line = <$pubkey_fh>) {
print($authkeys_fh $line);
}
close($pubkey_fh);
}
close($authkeys_fh);
chmod(0600, "$tempdir$sshdir/authorized_keys");
if (not $invisibletouch and -r "/etc/xcat/hostkeys/ssh_host_rsa_key") {
copy("/etc/xcat/hostkeys/ssh_host_rsa_key", "$tempdir/etc/ssh_host_rsa_key");
copy("/etc/xcat/hostkeys/ssh_host_dsa_key", "$tempdir/etc/ssh_host_dsa_key");
chmod(0600, <$tempdir/etc/ssh_*>);
}
unless ($invisibletouch or -r "$tempdir/etc/ssh_host_rsa_key") {
system("ssh-keygen -t rsa -f $tempdir/etc/ssh_host_rsa_key -C '' -N ''");
system("ssh-keygen -t dsa -f $tempdir/etc/ssh_host_dsa_key -C '' -N ''");
}
my $lzma_exit_value = 1;
if ($invisibletouch) {
my $done = 0;
# Build each image under a unique suffix and atomically rename it into
# place, so concurrent mknb runs sharing $tftpdir cannot read or clobber
# a half-written genesis.fs.
my $suffix = xCAT::Utils::genpassword(24);
my $lzma_command = genesis_lzma_command(-x "/usr/bin/lzma", -x "/usr/bin/xz");
if ($lzma_command) { #let's reclaim some of that size...
$callback->({ data => ["Creating genesis.fs.$arch.lzma in $tftpdir/xcat"] });
system("cd $tempdir; find . | cpio -o -H newc | $lzma_command > $tftpdir/xcat/genesis.fs.$arch.lzma.$suffix");
$lzma_exit_value = $? >> 8;
if ($lzma_exit_value) {
$callback->({ data => ["Creating genesis.fs.$arch.lzma in $tftpdir/xcat failed, falling back to gzip"] });
unlink("$tftpdir/xcat/genesis.fs.$arch.lzma.$suffix");
} else {
move("$tftpdir/xcat/genesis.fs.$arch.lzma.$suffix", "$tftpdir/xcat/genesis.fs.$arch.lzma");
$done = 1;
$initrd_file = "$tftpdir/xcat/genesis.fs.$arch.lzma";
}
}
if (not $done) {
$callback->({ data => ["Creating genesis.fs.$arch.gz in $tftpdir/xcat"] });
system("cd $tempdir; find . | cpio -o -H newc | gzip -9 > $tftpdir/xcat/genesis.fs.$arch.gz.$suffix");
move("$tftpdir/xcat/genesis.fs.$arch.gz.$suffix", "$tftpdir/xcat/genesis.fs.$arch.gz");
$initrd_file = "$tftpdir/xcat/genesis.fs.$arch.gz";
}
} else {
$callback->({ data => ["Creating nbfs.$arch.gz in $tftpdir/xcat"] });
system("cd $tempdir; find . | cpio -o -H newc | gzip -9 > $tftpdir/xcat/nbfs.$arch.gz");
$initrd_file = "$tftpdir/xcat/nbfs.$arch.gz";
}
system("rm -rf $tempdir");
unless ($initrd_file) {
$callback->({ data => ["Creating filesystem file in $tftpdir/xcat failed"] });
return;
}
my $exact_arch_marker = "$tftpdir/xcat/genesis.exact-arch.$arch";
if (($genesis_type eq 'legacy' || $genesis_type eq 'classic')
&& (-e $exact_arch_marker || -l $exact_arch_marker)
&& !unlink($exact_arch_marker)) {
$callback->({ error => ["Unable to remove Genesis architecture marker: $exact_arch_marker"], errorcode => [1] });
return;
}
CREAT_CONF_FILE:
if ($configfileonly) {
unless (-e "$tftpdir/xcat/genesis.kernel.$arch") {
$callback->({ error => ["No kernel file found in $tftpdir/xcat, pls run \"mknb $arch\" instead."], errorcode => [1] });
return;
}
if (-e "$tftpdir/xcat/genesis.fs.$arch.lzma") {
$initrd_file = "$tftpdir/xcat/genesis.fs.$arch.lzma";
$invisibletouch = 1;
} elsif (-e "$tftpdir/xcat/genesis.fs.$arch.gz") {
$initrd_file = "$tftpdir/xcat/genesis.fs.$arch.gz";
$invisibletouch = 1;
} elsif (-e "$tftpdir/xcat/nbfs.$arch.gz") {
$initrd_file = "$tftpdir/xcat/nbfs.$arch.gz";
} else {
$callback->({ error => ["No filesystem file found in $tftpdir/xcat, pls run \"mknb $arch\" instead."], errorcode => [1] });
return;
}
}
my $hexnet_addresses = xCAT::NetworkUtils->my_hexnets('all');
my $normnet_addresses = xCAT::NetworkUtils->my_nets('all');
my @masters = xCAT::TableUtils->get_site_attribute("master");
my @master_addresses;
if ($masters[0]) {
@master_addresses = xCAT::NetworkUtils->getipaddr(
$masters[0], OnlyV4 => 1, GetAllAddresses => 1
);
}
my ($hexnets, $xcatdhexnets) = _select_network_addresses(
$hexnet_addresses, \@master_addresses
);
my ($normnets, $xcatdnormnets) = _select_network_addresses(
$normnet_addresses, \@master_addresses
);
my $consolecmdline;
if (defined($serialport) and $serialspeed) {
if ($arch =~ /ppc/) {
$consolecmdline = "console=tty0 console=hvc$serialport,$serialspeed";
} else {
$consolecmdline = "console=tty0 console=ttyS$serialport,$serialspeed";
}
if ($serialflow =~ /cts/ or $serialflow =~ /hard/) {
$consolecmdline .= "n8r";
}
}
my $cfgfile;
if ($arch =~ /x86/) {
mkpath("$tftpdir/xcat/xnba/nets");
chmod(0755, "$tftpdir/xcat/xnba");
chmod(0755, "$tftpdir/xcat/xnba/nets");
mkpath("$tftpdir/pxelinux.cfg");
chmod(0755, "$tftpdir/pxelinux.cfg");
if (-r "/usr/lib/syslinux/pxelinux.0") {
copy("/usr/lib/syslinux/pxelinux.0", "$tftpdir/pxelinux.0");
} elsif (-r "/usr/share/syslinux/pxelinux.0") {
copy("/usr/share/syslinux/pxelinux.0", "$tftpdir/pxelinux.0");
} elsif ("/usr/lib/PXELINUX/pxelinux.0") {
copy("/usr/lib/PXELINUX/pxelinux.0", "$tftpdir/pxelinux.0");
} else {
copy("/opt/xcat/share/xcat/netboot/syslinux/pxelinux.0", "$tftpdir/pxelinux.0");
}
if (-r "$tftpdir/pxelinux.0") {
chmod(0644, "$tftpdir/pxelinux.0");
}
} 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}) {
my $net = $_;
my $nicip = $normnets->{$net};
my $xcatd_address = defined($xcatdnormnets->{$net}) ? $xcatdnormnets->{$net} : $nicip;
$net =~ s/\//_/;
if (defined($nobootnicips{$nicip})) {
if ($arch =~ /ppc/ and -r "$tftpdir/pxelinux.cfg/p/$net") {
unlink("$tftpdir/pxelinux.cfg/p/$net");
}
next;
}
$dopxe = 0;
if ($arch =~ /x86/) { #only do pxe if just x86 or x86_64 and no x86
if ($arch =~ /x86_64/ and not $invisibletouch) {
if (-r "$tftpdir/xcat/xnba/nets/$net") {
my $cfg;
my @contents;
open($cfg, "<", "$tftpdir/xcat/xnba/nets/$net");
@contents = <$cfg>;
close($cfg);
if (grep (/x86_64/, @contents)) {
$dopxe = 1;
}
} else {
$dopxe = 1;
}
} else {
$dopxe = 1;
}
}
if ($dopxe) {
my $cfg;
open($cfg, ">", "$tftpdir/xcat/xnba/nets/$net");
print $cfg "#!gpxe\n";
if ($invisibletouch) {
print $cfg 'imgfetch -n kernel http://${next-server}'.$portsuffix.'/tftpboot/xcat/genesis.kernel.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/machyp}' . "\n";
print $cfg 'imgfetch -n nbfs http://${next-server}'.$portsuffix . "$initrd_file\n";
} else {
print $cfg 'imgfetch -n kernel http://${next-server}'.$portsuffix.'/tftpboot/xcat/nbk.' . "$arch xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline\n";
print $cfg 'imgfetch -n nbfs http://${next-server}'.$portsuffix . "$initrd_file\n";
}
print $cfg "imgload kernel\n";
print $cfg "imgexec kernel\n";
close($cfg);
if ($invisibletouch and $arch =~ /x86_64/) { #UEFI time
open($cfg, ">", "$tftpdir/xcat/xnba/nets/$net.elilo");
print $cfg "default=\"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfg " delay=5\n";
print $cfg ' image=/tftpboot/xcat/genesis.kernel.' . "$arch\n";
print $cfg " label=\"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfg " initrd=$initrd_file\n";
print $cfg " append=\"xcatd=" . $xcatd_address . ":$xcatdport destiny=discover $consolecmdline BOOTIF=%B\"\n";
close($cfg);
open($cfg, ">", "$tftpdir/xcat/xnba/nets/$net.uefi");
print $cfg "#!gpxe\n";
print $cfg 'imgfetch -n kernel http://${next-server}'.$portsuffix.'/tftpboot/xcat/genesis.kernel.' . "$arch\nimgload kernel\n";
print $cfg "imgargs kernel xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline BOOTIF=01-" . '${netX/mac:hexhyp}' . " destiny=discover initrd=initrd\n";
print $cfg 'imgfetch -n initrd http://${next-server}'.$portsuffix . "$initrd_file\nimgexec kernel\n";
close($cfg);
}
} elsif ($arch =~ /ppc/) {
open($cfgfile, ">", "$tftpdir/pxelinux.cfg/p/$net");
print $cfgfile "default \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " delay=10\n";
print $cfgfile " label \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " kernel http://" . $xcatd_address . "$portsuffix/$tftpdir/xcat/genesis.kernel.$arch\n";
print $cfgfile " initrd http://" . $xcatd_address . "$portsuffix/$initrd_file\n";
print $cfgfile ' append "xcatd=' . $xcatd_address . ":$xcatdport $consolecmdline\"\n";
close($cfgfile);
}
}
$dopxe = 0;
foreach (keys %{$hexnets}) {
my $xcatd_address = defined($xcatdhexnets->{$_}) ? $xcatdhexnets->{$_} : $hexnets->{$_};
$dopxe = 0;
if ($arch =~ /x86/) { #only do pxe if just x86 or x86_64 and no x86
if ($arch =~ /x86_64/) {
if (-r "$tftpdir/pxelinux.cfg/" . uc($_)) {
my $pcfg;
open($pcfg, "<", "$tftpdir/pxelinux.cfg/" . uc($_));
my @pcfgcontents = <$pcfg>;
close($pcfg);
if (grep (/x86_64/, @pcfgcontents)) {
$dopxe = 1;
}
} else {
$dopxe = 1;
}
} else {
$dopxe = 1;
}
}
if ($dopxe) {
my $tftp_initrd = $initrd_file;
$tftp_initrd =~ s{^\Q$tftpdir\E/?}{};
my $kernel_file = $invisibletouch ? "genesis.kernel.$arch" : "nbk.$arch";
open($cfgfile, ">", "$tftpdir/pxelinux.cfg/" . uc($_));
print $cfgfile "DEFAULT xCAT\n";
print $cfgfile " LABEL xCAT\n";
print $cfgfile " KERNEL xcat/$kernel_file\n";
print $cfgfile " APPEND initrd=$tftp_initrd xcatd=" . $xcatd_address . ":$xcatdport $consolecmdline\n";
close($cfgfile);
} elsif ($arch =~ /ppc/) {
open($cfgfile, ">", "$tftpdir/etc/" . lc($_));
print $cfgfile "default \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " delay=10\n";
print $cfgfile " label \"xCAT Genesis (" . $normnets->{$_} . ")\"\n";
print $cfgfile " kernel http://" . $xcatd_address . "$portsuffix/$tftpdir/xcat/genesis.kernel.$arch\n";
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,
httpport => $httpport,
consolecmdline => $consolecmdline,
);
}
}
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)"] });
}
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.<arch>, net booted from $tftpdir/boot/grub2, looks for grub.cfg-01-<mac>,
# 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
# $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}";
}
# 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";
$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);
open(my $cfg, ">", $cfgpath) or return;
print $cfg $content;
close($cfg);
chmod(0644, $cfgpath);
return $cfgpath;
}
1;