2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-21 08:33:20 +00:00
Files
xcat-core/xCAT-server/lib/xcat/plugins/mknb.pm
T
Daniel Hilst 24e22671b4 Merge branch 'master' of https://github.com/xcat2/xcat-core into release/2.19-rc1
master moved 149 commits ahead of the branch point and four files needed a
decision.

xCAT/debian/control and xCATsn/debian/control: master moved nmap and
ipmitool-xcat into Depends, raised the ipmitool version and added the s390x
OpenEmbedded Genesis recommendation. The branch made the genesis-scripts
dependency per architecture. Both are kept, so the ppc64el metapackage depends
on xcat-genesis-scripts-ppc64el and no longer on the amd64 package.

build-utils/lib/XCAT/BuildUtils.pm and xCAT-test/unit/build_utils.t: master
replaced @DEB_ARCHES plus the branch's %NO_RISCV64 exception list with
%ARCH_PACKAGES, which carries the architecture list per package.
deb_package_arches returns the same answer for every package, so master's form
is kept and %NO_RISCV64 is dropped.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-11 15:59:58 -03:00

1107 lines
44 KiB
Perl

package xCAT_plugin::mknb;
use strict;
use Digest::SHA ();
use File::Temp qw(tempdir tempfile);
use xCAT::Utils;
use xCAT::TableUtils;
use xCAT::NodeRange;
use File::Path;
use File::Copy;
use English qw(-no_match_vars);
my $GENESIS_EXPORT_MANIFEST = 'xcat-genesis.manifest';
my %GENESIS_ARCHITECTURES = map { $_ => 1 }
qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64 s390x);
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",
);
if ($arch eq 's390x') {
my $config_directory = "$tftpdir/pxelinux.cfg/s390x";
if (opendir my $config_stream, $config_directory) {
foreach my $name (readdir $config_stream) {
if ($name =~ m{\A[.][.]?\z}xms) {
next;
}
my $path = "$config_directory/$name";
if (_is_generated_s390x_config($path)) {
push @artifacts, $path;
}
}
closedir $config_stream;
}
}
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 _is_generated_s390x_config {
my ($path) = @_;
if (-l $path || !-f $path) {
return 0;
}
open my $config, '<', $path or return 0;
my $header = <$config>;
if (!close $config) {
return 0;
}
return defined($header) && $header eq "# pxelinux.cfg xCAT Genesis s390x\n";
}
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;
}
#-------------------------------------------------------------------------------
=head3 stage_genesis_payload
Descriptions:
Copy the Genesis payload into place for mknb: for a legacy image the unpacked
root tree and then the kernel, for an exported image the nbroot tree.
Extracted so the outcome can be driven directly. The copies are the only
place mknb learns that an installed Genesis image is unusable, and a caller
cannot tell WHICH copy failed from a single exit status.
Arguments:
genesis_type, genesis_dir, tftpdir, arch, tempdir, and an optional run
coderef used in place of system() by the tests.
Returns:
(rc, source) -- rc is the exit status of the copy that failed, and source
names it, so the caller reports the file it could not read.
=cut
#-------------------------------------------------------------------------------
sub stage_genesis_payload {
my (%a) = @_;
my $run = $a{run} || sub { return system($_[0]); };
my $rc;
if (($a{genesis_type} // '') eq 'legacy') {
# Two copies, each able to fail on its own. Return on the first, so neither the exit
# status nor the name of the unreadable file is lost to the one that follows it.
$rc = $run->("shopt -s dotglob; GLOBIGNORE=\".:..\" cp -a $a{genesis_dir}/fs/* $a{tempdir}");
return ($rc, "$a{genesis_dir}/fs") if $rc;
$rc = $run->("cp -a $a{genesis_dir}/kernel $a{tftpdir}/xcat/genesis.kernel.$a{arch}");
return ($rc, "$a{genesis_dir}/kernel") if $rc;
return (0, undef);
}
$rc = $run->("cp -a $a{genesis_dir}/nbroot/* $a{tempdir}");
return ($rc, "$a{genesis_dir}/nbroot");
}
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, riscv64 or s390x)" }, { 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");
}
$invisibletouch = 1 if $genesis_type eq 'legacy';
my ($rc, $failed_src) = stage_genesis_payload(
genesis_type => $genesis_type, genesis_dir => $genesis_dir,
tftpdir => $tftpdir, arch => $arch, tempdir => $tempdir);
if ($rc) {
system("rm -rf $tempdir");
$callback->({ error => ["Failed to copy $failed_src 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 ($arch eq 's390x') {
$consolecmdline = 'console=ttysclp0';
} elsif (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");
}
if ($arch eq 's390x') {
mkpath "$tftpdir/pxelinux.cfg/s390x";
chmod 0755, "$tftpdir/pxelinux.cfg";
chmod 0755, "$tftpdir/pxelinux.cfg/s390x";
}
my $dopxe = 0;
my $s390x_config_error = 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})
|| ($arch eq 's390x' && defined($nobootnicips{$xcatd_address}))) {
if ($arch =~ /ppc/ and -r "$tftpdir/pxelinux.cfg/p/$net") {
unlink("$tftpdir/pxelinux.cfg/p/$net");
} elsif ($arch eq 's390x') {
my $path = "$tftpdir/pxelinux.cfg/s390x/$net";
if (_is_generated_s390x_config($path)) {
if (!unlink $path) {
$callback->({ error => ["Unable to remove s390x Genesis configuration: $path: $OS_ERROR"], errorcode => [1] });
$s390x_config_error = 1;
}
}
}
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);
} elsif ($arch eq 's390x') {
my (undef, $config_error) = _write_s390x_discovery_config(
tftpdir => $tftpdir,
network => $net,
xcatd_address => $xcatd_address,
xcatdport => $xcatdport,
consolecmdline => $consolecmdline,
kernel => $invisibletouch
? "xcat/genesis.kernel.$arch"
: "xcat/nbk.$arch",
initrd => $initrd_file,
);
if ($config_error) {
$callback->({ error => [$config_error], errorcode => [1] });
$s390x_config_error = 1;
}
}
}
$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}) {
# 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;
}
_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") {
# These configurations are only reachable through grub2.<arch>. copycd builds it from
# Ubuntu media; on EL it is supplied by grub2-xcat or copied from the media.
$callback->({ data => ["Note: $tftpdir/boot/grub2/grub2.$arch is missing; $arch nodes need it to reach these configurations (copycd builds it from Ubuntu media; on EL it is installed by grub2-xcat or copied from the $arch installation media)"] });
}
if ($configfileonly && !$s390x_config_error) {
$callback->({ data => ["Write netboot config file done"] });
}
}
sub _write_s390x_discovery_config {
my (%args) = @_;
my $tftpdir = $args{tftpdir};
my $initrd = $args{initrd};
$initrd =~ s{^\Q$tftpdir\E/?}{}xms;
my $cmdline = "xcatd=$args{xcatd_address}:$args{xcatdport} xcat.bootloader=s390-ccw";
if (defined $args{consolecmdline} and length $args{consolecmdline}) {
$cmdline .= " $args{consolecmdline}";
}
my $qemu_path = "$tftpdir/pxelinux.cfg/s390x/$args{network}";
my $qemu_config = "# pxelinux.cfg xCAT Genesis s390x\n"
. "DEFAULT xCAT\n"
. "LABEL xCAT\n"
. " KERNEL $args{kernel}\n"
. " INITRD $initrd\n"
. " APPEND $cmdline\n";
my $error = _write_s390x_config($qemu_path, $qemu_config);
return (undef, $error) if $error;
return ($qemu_path, undef);
}
sub _write_s390x_config {
my ($path, $contents) = @_;
if ((-e $path || -l $path) && !_is_generated_s390x_config($path)) {
return "Refusing to replace unmanaged s390x configuration: $path";
}
my $directory = $path;
$directory =~ s{/[^/]+\z}{}xms;
my ($config, $temporary);
my $created = eval {
($config, $temporary) = tempfile(
'.mknb-s390x-XXXXXX', DIR => $directory, UNLINK => 0
);
1;
};
if (!$created) {
my $create_error = $EVAL_ERROR || $OS_ERROR;
chomp $create_error;
return "Unable to write s390x Genesis configuration: $path: $create_error";
}
my $write_ok = print {$config} $contents;
my $write_error = $OS_ERROR;
my $close_ok = close $config;
my $close_error = $OS_ERROR;
if (!$write_ok) {
unlink $temporary;
return "Unable to write s390x Genesis configuration: $path: $write_error";
}
if (!$close_ok) {
unlink $temporary;
return "Unable to write s390x Genesis configuration: $path: $close_error";
}
if (!chmod 0644, $temporary) {
my $chmod_error = $OS_ERROR;
unlink $temporary;
return "Unable to set s390x Genesis configuration permissions: $path: $chmod_error";
}
if ((-e $path || -l $path) && !_is_generated_s390x_config($path)) {
unlink $temporary;
return "Refusing to replace unmanaged s390x configuration: $path";
}
if (!rename $temporary, $path) {
my $rename_error = $OS_ERROR;
unlink $temporary;
return "Unable to install s390x Genesis configuration: $path: $rename_error";
}
return;
}
# 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 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}";
}
# 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");
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";
# 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 $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";
$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;