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 83c2c0216..657b54b6c 100644 --- a/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst +++ b/docs/source/developers/guides/code/dhcp_backend_validation_matrix.rst @@ -351,6 +351,62 @@ backend scope described above. ``ALLOC_FAIL_NO_POOLS``; DHCP/TFTP/GRUB handoff passed and the node reached Genesis with the temporary EL9 ppc64le payload workaround. +Ubuntu LTS KVM Validation Snapshot +---------------------------------- + +As of April 29, 2026, the Ubuntu LTS KVM validation for the Ubuntu +provisioning restoration work has the following result: + +.. list-table:: + :header-rows: 1 + :widths: 14 10 10 12 12 42 + + * - Platform + - Arch + - Backend + - BIOS + - UEFI + - Notes + * - Ubuntu 18.04 LTS + - ``x86_64`` + - ``ISC`` + - Pass + - Pass + - Stateless and stateful compute boots passed against an Ubuntu 18.04 + headnode. The 18.04 debian-installer initrd did not include + ``virtio_blk`` in this KVM environment, so stateful VMs used + ``virtio-scsi`` disks. The legacy preseed disables installer + update/security services so stateful installs and ``apt-get update`` + use only the local xCAT install media. Persistent interface naming used + ``R::net.ifnames=0 R::biosdevname=0``. + * - Ubuntu 20.04 LTS + - ``x86_64`` + - ``ISC`` + - Pass + - Pass + - Stateless and stateful compute boots passed. Stateful validation used a + manual static reservation workaround for the known forced-ISC OMAPI + issue. + * - Ubuntu 22.04 LTS + - ``x86_64`` + - ``Kea`` + - Pass + - Pass + - Stateless and stateful compute boots passed. Kea 2.0.2 configuration + validation with ``kea-dhcp4 -t`` passed on the Ubuntu 22.04 headnode. + ``makedns -n`` starts ``bind9`` successfully, and the DHCP section of + ``xcatprobe xcatmn`` passed on consecutive runs. + * - Ubuntu 24.04 LTS + - ``x86_64`` + - ``Kea`` + - Pass + - Pass + - Stateless and stateful compute boots passed. Kea 2.4.1 configuration + validation with ``kea-dhcp4 -t`` passed on the Ubuntu 24.04 headnode. + ``makedns -n`` starts ``bind9`` successfully, and the DHCP section of + ``xcatprobe xcatmn`` passed on consecutive runs. UEFI validation used + OVMF Secure Boot disabled. + Skipped Rows ------------ diff --git a/docs/source/developers/guides/code/kea_dhcp_backend_plan.rst b/docs/source/developers/guides/code/kea_dhcp_backend_plan.rst index b73ae8fb7..16c6e0ce6 100644 --- a/docs/source/developers/guides/code/kea_dhcp_backend_plan.rst +++ b/docs/source/developers/guides/code/kea_dhcp_backend_plan.rst @@ -290,7 +290,7 @@ Unit tests: * host reservation formatting * subnet and pool mapping * Kea reservation policy flags for subnet reservations and out-of-pool-only - overrides + overrides, plus DHCPv4 ``match-client-id`` behavior * backend selection and override behavior Configuration validation tests: @@ -319,6 +319,8 @@ Integration matrix: * EL9 plus ISC * EL9 plus forced Kea * EL10 plus Kea +* Ubuntu 18.04 plus ISC +* Ubuntu 20.04 plus ISC * Ubuntu 22.04 plus Kea * Ubuntu 24.04 plus Kea @@ -353,7 +355,8 @@ ordinary containers. Open test infrastructure details to confirm: * SSH access method and user for ``rome01.local.versatushpc.com.br`` -* available base images for EL9, EL10, Ubuntu 22.04, and Ubuntu 24.04 +* available base images for EL9, EL10, Ubuntu 18.04, Ubuntu 20.04, Ubuntu + 22.04, and Ubuntu 24.04 * libvirt network names and whether isolated DHCP test networks are already available * whether nested or privileged test guests can run DHCP client and PXE tests diff --git a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm index f9a5aa8fe..79c98cc91 100644 --- a/perl-xCAT/xCAT/DHCP/Backend/Kea.pm +++ b/perl-xCAT/xCAT/DHCP/Backend/Kea.pm @@ -63,10 +63,11 @@ sub render_dhcp4_config { type => 'memfile', name => '/var/lib/kea/kea-leases4.csv', }, - 'valid-lifetime' => _integer( _first_defined( $intent->{'valid-lifetime'}, $intent->{valid_lifetime}, 43200 ) ), - 'reservations-in-subnet' => _json_bool( _first_defined( $intent->{'reservations-in-subnet'}, $intent->{reservations_in_subnet}, 1 ) ), - 'reservations-out-of-pool' => _json_bool( _first_defined( $intent->{'reservations-out-of-pool'}, $intent->{reservations_out_of_pool}, 1 ) ), - subnet4 => [ map { $self->_render_subnet4($_) } @{ _first_defined( $intent->{subnet4}, $intent->{subnets}, [] ) } ], + 'valid-lifetime' => _integer( _first_defined( $intent->{'valid-lifetime'}, $intent->{valid_lifetime}, 43200 ) ), + 'reservations-in-subnet' => _json_bool( _first_defined( $intent->{'reservations-in-subnet'}, $intent->{reservations_in_subnet}, 1 ) ), + 'reservations-out-of-pool' => _json_bool( _first_defined( $intent->{'reservations-out-of-pool'}, $intent->{reservations_out_of_pool}, 1 ) ), + 'match-client-id' => _json_bool( _first_defined( $intent->{'match-client-id'}, $intent->{match_client_id}, 0 ) ), + subnet4 => [ map { $self->_render_subnet4($_) } @{ _first_defined( $intent->{subnet4}, $intent->{subnets}, [] ) } ], ); $dhcp4{'control-socket'} = $intent->{'control-socket'} if $intent->{'control-socket'}; diff --git a/xCAT-probe/lib/perl/probe_utils.pm b/xCAT-probe/lib/perl/probe_utils.pm index fa31e84e7..218678f34 100644 --- a/xCAT-probe/lib/perl/probe_utils.pm +++ b/xCAT-probe/lib/perl/probe_utils.pm @@ -158,6 +158,66 @@ sub get_os { return $os; } +sub _netplan_get { + my $key = shift; + open(my $fh, '-|', 'netplan', 'get', $key) or return undef; + my $val = <$fh>; + close $fh; + return undef if $?; + chomp $val if defined $val; + return $val; +} + +sub _command_available { + my $cmd = shift; + for my $dir (split /:/, $ENV{PATH} || '') { + next unless $dir; + return 1 if -x "$dir/$cmd"; + } + return 0; +} + +sub _netplan_has_static_ip { + my ($nic, $ip) = @_; + + return 0 unless _command_available('netplan'); + + (my $escaped_nic = $nic) =~ s/\./\\./g; + for my $devtype (qw(ethernets bonds bridges vlans)) { + my $dev_obj = _netplan_get("$devtype.$escaped_nic"); + next unless defined $dev_obj; + next if ($dev_obj eq 'null' || $dev_obj eq ''); + + my $addresses = _netplan_get("$devtype.$escaped_nic.addresses"); + next unless defined $addresses; + next if ($addresses eq 'null' || $addresses eq ''); + next unless $addresses =~ /(?:^|[\s\[,])\Q$ip\E(?:\/\d+)?(?:$|[\s\],])/m; + + my $dhcp_val = _netplan_get("$devtype.$escaped_nic.dhcp4"); + return 0 if defined $dhcp_val && $dhcp_val =~ /true/i; + return 1; + } + + return 0; +} + +sub dhcp_query_reply_mac { + my ($reply, $node, $ip) = @_; + + return unless defined $reply && defined $node && defined $ip; + return $1 if $reply =~ /\Q$node\E:\s+ip-address\s*=\s*\Q$ip\E,?\s+hardware-address\s*=\s*([0-9a-f:]+)/i; + return; +} + +sub dhcp_query_reply_matches { + my ($reply, $node, $ip, $mac) = @_; + + return 0 unless defined $mac; + my $reply_mac = dhcp_query_reply_mac($reply, $node, $ip); + return 0 unless defined $reply_mac; + return lc($reply_mac) eq lc($mac); +} + #------------------------------------------ =head3 @@ -206,8 +266,9 @@ sub is_static_ip { my $output2 = `cat /etc/sysconfig/network/ifcfg-$nic 2>&1 |grep -i BOOTPROTO`; $rst = 1 if (($output1 =~ /$ip/) && ($output2 =~ /static/i)); } elsif ($os =~ /ubuntu/) { - my $output = `cat /etc/network/interfaces 2>&1|grep -E "iface\\s+$nic"`; - $rst = 1 if ($output =~ /static/i); + return 1 if _netplan_has_static_ip($nic, $ip); + my $output = `cat /etc/network/interfaces 2>&1`; + $rst = 1 if (($output =~ /iface\s+\Q$nic\E\s+inet\s+static/i) && ($output =~ /\baddress\s+\Q$ip\E(?:\/\d+)?\b/i)); } return $rst; } diff --git a/xCAT-probe/subcmds/xcatmn b/xCAT-probe/subcmds/xcatmn index 9677af5eb..f2e3f074a 100755 --- a/xCAT-probe/subcmds/xcatmn +++ b/xCAT-probe/subcmds/xcatmn @@ -1093,10 +1093,9 @@ sub check_dhcp_service { my $snip = xCAT::NetworkUtils->getipaddr($sntmp); my $snmac = `lsdef $sntmp -i mac -c 2>&1| awk -F'=' '{print \$2}'`; chomp($snmac); - my $tmpmac; - if ($tmp =~ /$sntmp: ip-address = $snip, hardware-address = (.+)/) { - $tmpmac = $1; - if ($tmpmac !~ $snmac) { + my $tmpmac = probe_utils::dhcp_query_reply_mac($tmp, $sntmp, $snip); + if (defined($tmpmac)) { + if (lc($tmpmac) ne lc($snmac)) { push @$error_ref, "DHCP server reply is wrong"; returncmdoutput($tmp, $error_ref) if ($verbose); $rc = 1; @@ -1108,6 +1107,10 @@ sub check_dhcp_service { } } else { + # Clean up any leftover test node from a previous failed run + `makedhcp -d xcatmntest 2>&1`; + `rmdef xcatmntest 2>&1`; + my $tmp = `chdef xcatmntest groups=all ip=$serverip mac=aa:aa:aa:aa:aa:aa 2>&1`; if ($?) { push @$error_ref, "Node simulation by 'chdef' has failed"; @@ -1121,7 +1124,7 @@ sub check_dhcp_service { `cp /etc/hosts /etc/hosts.bak.probe > /dev/null 2>&1`; open HOSTFILE, ">> /etc/hosts"; - print HOSTFILE "$serverip xcatmntest xcatmntest.$domain"; + print HOSTFILE "$serverip xcatmntest xcatmntest.$domain\n"; close HOSTFILE; probe_utils->send_msg($outputtarget, "d", "To do 'makedhcp xcatmntest'") if ($verbose); @@ -1130,40 +1133,45 @@ sub check_dhcp_service { push @$error_ref, "makedhcp xcatmntest failed"; returncmdoutput($tmp, $error_ref) if ($verbose); $rc = 1; + `makedhcp -d xcatmntest 2>&1`; `rmdef xcatmntest 2>&1`; + if (-f "/etc/hosts.bak.probe") { + unlink "/etc/hosts"; + move("/etc/hosts.bak.probe", "/etc/hosts"); + } last; } + # Allow DHCP server time to commit the OMAPI-added entry + sleep 1; + $tmp = `makedhcp -q xcatmntest 2>&1`; if ($?) { push @$error_ref, "makedhcp -q xcatmntest failed"; returncmdoutput($tmp, $error_ref) if ($verbose); $rc = 1; - `makedhcp -d xcatmntest 2>&1 && rmdef xcatmntest 2>&1`; - last; } - chomp($tmp); - if (length($tmp) == 0) { - # makedhcp -q did not return anything, and RC was 0 + chomp($tmp) if defined($tmp); + if (!$rc && length($tmp) == 0) { push @$error_ref, "DHCP server returned no data for 'makedhcp -q xcatmntest'"; returncmdoutput($tmp, $error_ref) if ($verbose); $rc = 1; - last; } - if ($tmp !~ /xcatmntest: ip-address = $serverip, hardware-address = aa:aa:aa:aa:aa:aa/) { + if (!$rc && !probe_utils::dhcp_query_reply_matches($tmp, 'xcatmntest', $serverip, 'aa:aa:aa:aa:aa:aa')) { push @$error_ref, "DHCP server reply does not match expected IP and MAC"; returncmdoutput($tmp, $error_ref) if ($verbose); $rc = 1; - `makedhcp -d xcatmntest 2>&1 && rmdef xcatmntest 2>&1`; - last; } + # Always clean up the test node regardless of success or failure push @$error_ref, "Start clearing simulation information for dhcp test" if ($verbose); - $tmp = `makedhcp -d xcatmntest 2>&1 && rmdef xcatmntest 2>&1`; + $tmp = `makedhcp -d xcatmntest 2>&1 && rmdef xcatmntest 2>&1`; returncmdoutput($tmp, $error_ref) if ($verbose); - unlink "/etc/hosts"; - move("/etc/hosts.bak.probe", "/etc/hosts"); + if (-f "/etc/hosts.bak.probe") { + unlink "/etc/hosts"; + move("/etc/hosts.bak.probe", "/etc/hosts"); + } } } if ($rc) { @@ -1492,5 +1500,3 @@ exit $rst; - - diff --git a/xCAT-server/lib/perl/xCAT/SvrUtils.pm b/xCAT-server/lib/perl/xCAT/SvrUtils.pm index 40c17982f..2b581ee01 100644 --- a/xCAT-server/lib/perl/xCAT/SvrUtils.pm +++ b/xCAT-server/lib/perl/xCAT/SvrUtils.pm @@ -1590,28 +1590,38 @@ sub setupNFSTree { shift @entries; if (grep /\Q$nfsdirectory\E/, @entries) { $callback->({ data => ["$nfsdirectory has been exported already!"] }); - - # nothing to do } else { - $cmd = "/usr/sbin/exportfs :$nfsdirectory"; + $cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure"; xCAT::Utils->runcmd($cmd, 0); - - # exportfs can export this directory immediately $callback->({ data => ["now $nfsdirectory is exported!"] }); - $cmd = "cat /etc/exports"; - @entries = xCAT::Utils->runcmd($cmd, 0); - unless (my $entry = grep /\Q$nfsdirectory\E/, @entries) { + } - #if there's no entry in /etc/exports, one with default options will be added - $cmd = qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check)" >> /etc/exports}; - xCAT::Utils->runcmd($cmd, 0); - $callback->({ data => ["$nfsdirectory is added to /etc/exports with default option"] }); - } + if (ensure_nfs_export_option($nfsdirectory, 'insecure')) { + xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0); + $callback->({ data => ["added insecure to existing $nfsdirectory export"] }); + } elsif (!nfs_export_exists($nfsdirectory)) { + $cmd = qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >> /etc/exports}; + xCAT::Utils->runcmd($cmd, 0); + $callback->({ data => ["$nfsdirectory is added to /etc/exports with default option"] }); } } } } +sub nfs_export_exists { + my ( $dir, %opts ) = _nfs_method_args(@_); + my @files = _nfs_export_files(%opts); + for my $file (@files) { + open(my $fh, '<', $file) or next; + while (<$fh>) { + next if /^\s*#/; + return 1 if /^\s*\Q$dir\E\s/; + } + close $fh; + } + return 0; +} + sub setupStatemnt { my $sip = shift; if (($sip) && ($sip =~ /xCAT::SvrUtils/)) @@ -1639,26 +1649,120 @@ sub setupStatemnt { if (grep /\Q$nfsdirectory\E/, @entries) { $callback->({ data => ["$nfsdirectory has been exported already!"] }); } else { - $cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check"; + $cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure"; xCAT::Utils->runcmd($cmd, 0); $callback->({ data => ["now $nfsdirectory is exported!"] }); + } - # add the directory into /etc/exports if not exist - $cmd = "cat /etc/exports"; - @entries = xCAT::Utils->runcmd($cmd, 0); - if (my $entry = grep /\Q$nfsdirectory\E/, @entries) { - unless ($entry =~ m/rw/) { - $callback->({ data => ["The $nfsdirectory should be with rw option in /etc/exports"] }); - } - } else { - xCAT::Utils->runcmd(qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check)" >>/etc/exports}, 0); - $callback->({ data => ["$nfsdirectory is added into /etc/exports with default options"] }); - } + if (ensure_nfs_export_option($nfsdirectory, 'insecure')) { + xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0); + $callback->({ data => ["added insecure to existing $nfsdirectory export"] }); + } elsif (!nfs_export_exists($nfsdirectory)) { + xCAT::Utils->runcmd(qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >>/etc/exports}, 0); + $callback->({ data => ["$nfsdirectory is added into /etc/exports with default options"] }); } } } +sub ensure_nfs_export_option { + my ( $dir, @args ) = _nfs_method_args(@_); + local @_ = @args; + my $option = shift || 'insecure'; + my %opts = @_; + my @files = _nfs_export_files(%opts); + my $changed = 0; + for my $file (@files) { + open(my $fh, '<', $file) or next; + my @raw = <$fh>; + close $fh; + + my @logical; + my $buf = ''; + for my $r (@raw) { + chomp $r; + if ($r =~ s/\s*\\$//) { + $buf .= "$r "; + next; + } + $buf .= $r; + push @logical, $buf; + $buf = ''; + } + push @logical, $buf if $buf ne ''; + + my $file_changed = 0; + for my $line (@logical) { + next if $line =~ /^\s*#/; + next unless $line =~ /^\s*\Q$dir\E\s/; + my $line_changed = 0; + my $comment = ''; + $comment = " $1" if $line =~ s/\s+(#.*)$//; + my $tail = $line; + $tail =~ s/^\s*\Q$dir\E\s+//; + my @tokens; + while ($tail =~ /(\S+(?:\([^)]*\))?)/g) { + push @tokens, $1; + } + my @out; + for my $tok (@tokens) { + if ($tok =~ /^-(.+)/) { + my $opts = $1; + unless (_option_list_has($opts, $option)) { + $tok = "-$opts,$option"; + $line_changed = 1; + } + } elsif ($tok =~ /^([^(]+)\(([^)]*)\)$/) { + my ($client, $opts) = ($1, $2); + unless (_option_list_has($opts, $option)) { + $tok = "$client($opts,$option)"; + $line_changed = 1; + } + } else { + $tok = "$tok($option)"; + $line_changed = 1; + } + push @out, $tok; + } + if ($line_changed) { + $line = "$dir " . join(" ", @out) . "$comment"; + $file_changed = 1; + } + } + if ($file_changed) { + open(my $wfh, '>', $file) or next; + print $wfh map { "$_\n" } @logical; + close $wfh; + $changed = 1; + } + } + return $changed; +} + +sub _nfs_method_args { + my @args = @_; + shift @args if @args && (ref($args[0]) || $args[0] eq __PACKAGE__); + return @args; +} + +sub _nfs_export_files { + my %opts = @_; + return @{ $opts{files} } if ref($opts{files}) eq 'ARRAY'; + + my @files = ('/etc/exports'); + push @files, glob('/etc/exports.d/*.exports') if -d '/etc/exports.d'; + return @files; +} + +sub _option_list_has { + my ($list, $option) = @_; + foreach my $item (split /,/, $list) { + $item =~ s/^\s+|\s+$//g; + return 1 if $item eq $option; + } + return 0; +} + #------------------------------------------------------------------------------------------- # Common method to send info back to the client # The last two args are optional, though $allerrornodes will unlikely be there without $node diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index d133db725..a9bb1d62d 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -1132,17 +1132,23 @@ sub mirrorspec { if (!$pkgdir) { $pkgdir = $_; } else { - my $osuurl = "http://" . $masternode.':'.$ENV{httpport} . $_ . " ./"; + my $httpport = $ENV{HTTPPORT} || $ENV{httpport} || '80'; + my $osuurl = "http://" . $masternode . ':' . $httpport . $_ . " ./"; push @mirrors, $osuurl; } } } if ($pkgdir) { + my $httpport = $ENV{HTTPPORT} || $ENV{httpport} || '80'; + my $security_host = $masternode; + $security_host .= ':' . $httpport if $httpport ne '80'; $line .= " d-i mirror/country string manual\n d-i mirror/protocol string http\n d-i mirror/http/directory string $pkgdir\n +d-i apt-setup/security_host string $security_host\n +d-i apt-setup/security_path string $pkgdir\n d-i mirror/http/proxy string\n"; } diff --git a/xCAT-server/lib/xcat/plugins/AAsn.pm b/xCAT-server/lib/xcat/plugins/AAsn.pm index 1d46696c5..201167ecf 100644 --- a/xCAT-server/lib/xcat/plugins/AAsn.pm +++ b/xCAT-server/lib/xcat/plugins/AAsn.pm @@ -12,6 +12,7 @@ use strict; use xCAT::Table; use xCAT::Utils; +use xCAT::SvrUtils; use xCAT::TableUtils; use xCAT::NetworkUtils; use xCAT::DHCP::Backend; @@ -398,15 +399,11 @@ sub setupInstallloc # add /install to /etc/exports - if needed # - my $cmd = "/bin/cat /etc/exports | grep '$installdir'"; - my $outref = xCAT::Utils->runcmd("$cmd", -1); my $changed_exports; - if ($::RUNCMD_RC != 0) + if (!xCAT::SvrUtils->nfs_export_exists($installdir)) { - - # ok - then add this entry my $cmd = -"/bin/echo '$installdir *(rw,no_root_squash,sync,no_subtree_check)' >> /etc/exports"; +"/bin/echo '$installdir *(rw,no_root_squash,sync,no_subtree_check,insecure)' >> /etc/exports"; my $outref = xCAT::Utils->runcmd("$cmd", 0); if ($::RUNCMD_RC != 0) { @@ -418,11 +415,16 @@ sub setupInstallloc $changed_exports++; } } + else + { + if (xCAT::SvrUtils->ensure_nfs_export_option($installdir, 'insecure')) + { + $changed_exports++; + } + } if ($changed_exports) { - - # restart nfs &setup_NFS($nodename); my $cmd = "/usr/sbin/exportfs -a"; @@ -431,7 +433,6 @@ sub setupInstallloc { xCAT::MsgUtils->message('S', "Error with $cmd."); } - } } } diff --git a/xCAT-server/lib/xcat/plugins/ddns.pm b/xCAT-server/lib/xcat/plugins/ddns.pm index f5f0ce5d1..df17749bf 100644 --- a/xCAT-server/lib/xcat/plugins/ddns.pm +++ b/xCAT-server/lib/xcat/plugins/ddns.pm @@ -27,18 +27,34 @@ my $ddns_key_path = "/etc/xcat/ddns.key"; # Net::DNS >= 1.36 removed support for sign_tsig($keyname, $secret) and now # expects a keyfile. Keep the keyfile in sync with the xcat_key secret. +sub ddns_tsig_algorithm { + my ($ctx) = @_; + + # Older Net::DNS releases used by Ubuntu 22.04 cannot sign updates with the + # keyfile API, so keep the generated xCAT key on the legacy algorithm that + # both old and new Net::DNS can use. + return "hmac-md5" if (Net::DNS->VERSION < 1.36); + return $ctx->{tsig_algorithm} || "hmac-md5"; +} + +sub ddns_key_contents { + my ($ctx) = @_; + + my $algorithm = ddns_tsig_algorithm($ctx); + return + "key \"xcat_key\" {\n" + . "\talgorithm $algorithm;\n" + . "\tsecret \"" . $ctx->{privkey} . "\";\n" + . "};\n\n"; +} + sub ensure_ddns_key_file { my ($ctx) = @_; return unless (Net::DNS->VERSION >= 1.36); return unless ($ctx && $ctx->{privkey}); - my $algorithm = $ctx->{tsig_algorithm} || "hmac-sha256"; - my $contents = - "key \"xcat_key\" {\n" - . "\talgorithm $algorithm;\n" - . "\tsecret \"" . $ctx->{privkey} . "\";\n" - . "};\n\n"; + my $contents = ddns_key_contents($ctx); if (open(my $existing_fh, "<", $ddns_key_path)) { local $/; @@ -1278,18 +1294,22 @@ sub update_namedconf { $gotkey = 1; my $algorithmnow; if ($ctx->{privkey}) { - - #for now, assume the field is correct - #push @newnamed,"key xcat_key {\n","\talgorithm hmac-md5;\n","\tsecret \"".$ctx->{privkey}."\";\n","};\n\n"; - push @newnamed, $line; + my @keyblock = ($line); do { $i++; $line = $currnamed[$i]; if ($line =~ /^\s*algorithm\s+([^;\s]+)\s*;/) { $algorithmnow = $1; } - push @newnamed, $line; + push @keyblock, $line; } while ($line !~ /^\};/); + if ($algorithmnow && Net::DNS->VERSION < 1.36 && lc($algorithmnow) ne "hmac-md5") { + $ctx->{tsig_algorithm} = "hmac-md5"; + push @newnamed, ddns_key_contents($ctx); + $ctx->{restartneeded} = 1; + } else { + push @newnamed, @keyblock; + } } else { push @newnamed, $line; while ($line !~ /^\};/) { #skip the old file zone @@ -1406,8 +1426,8 @@ sub update_namedconf { $ctx->{privkey} = encode_base64(genpassword(32)); chomp($ctx->{privkey}); } - $ctx->{tsig_algorithm} ||= "hmac-sha256"; - my $contents = "key \"xcat_key\" {\n" . "\talgorithm " . $ctx->{tsig_algorithm} . ";\n" . "\tsecret \"" . $ctx->{privkey} . "\";\n" . "};\n\n"; + $ctx->{tsig_algorithm} ||= ddns_tsig_algorithm($ctx); + my $contents = ddns_key_contents($ctx); push @newnamed, $contents; ensure_ddns_key_file($ctx); $ctx->{restartneeded} = 1; diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index b44e5b4be..8eb948a30 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -418,8 +418,27 @@ sub copycd my @line = split(" ", `ls -lh $installroot/$distname/$arch/dists/ | grep dr`); my $dist = $line[ @line - 1 ]; - # touches the Packages file so that deb packaging works - system("touch $installroot/$distname/$arch/dists/$dist/restricted/binary-$debarch/Packages"); + # Ensure the uncompressed Packages file exists. The ISO ships only + # Packages.gz; an empty Packages file causes apt hash-sum failures + # during autoinstall because apt reads the zero-length file instead + # of falling back to the compressed variant. + my $pkgdir = "$installroot/$distname/$arch/dists/$dist/restricted/binary-$debarch"; + if (-d $pkgdir) { + my $pkgfile = "$pkgdir/Packages"; + my $pkggz = "$pkgfile.gz"; + if (-f $pkggz && (! -f $pkgfile || -z $pkgfile)) { + if (open(my $in, '-|', 'gzip', '-dc', $pkggz)) { + if (open(my $out, '>', $pkgfile)) { + copy($in, $out); + close($out); + } + close($in); + } + } elsif (! -f $pkgfile) { + open(my $fh, '>', $pkgfile); + close($fh) if $fh; + } + } # removes the links unstable and testing, otherwise the repository does not work for debian system("rm -f $installroot/$distname/$arch/dists/unstable"); @@ -753,9 +772,12 @@ sub mkinstall { } mkpath($autoinstfile); - # create empty meta-data file + # create empty meta-data and vendor-data files open(my $fh, ">", $autoinstfile . "/meta-data"); close($fh); + open($fh, ">", $autoinstfile . "/vendor-data"); + print $fh "{}\n"; + close($fh); # point the template output at the /user-data file $autoinstfile = "$autoinstfile/user-data"; @@ -937,6 +959,7 @@ sub mkinstall { if (using_subiquity($os,$tmplfile)) { $kcmdline .= " autoinstall ip=dhcp netboot=nfs nfsroot=${instserver}:${pkgdir}"; $kcmdline .= " ds=nocloud-net;s=http://${instserver}:${httpport}/install/autoinst/${node}/"; + $kcmdline .= " ---"; } else { $kcmdline .= " url=http://${instserver}:$httpport/install/autoinst/$node"; $kcmdline .= " mirror/http/hostname=${instserver}:$httpport"; @@ -1766,20 +1789,18 @@ sub setupNFSTree { if (grep /\Q$nfsdirectory\E/, @entries) { $callback->({ data => ["$nfsdirectory has been exported already!"] }); } else { - $cmd = "/usr/sbin/exportfs :$nfsdirectory"; + $cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure"; xCAT::Utils->runcmd($cmd, 0); - - # exportfs can export this directory immediately $callback->({ data => ["now $nfsdirectory is exported!"] }); - $cmd = "cat /etc/exports"; - @entries = xCAT::Utils->runcmd($cmd, 0); - unless (my $entry = grep /\Q$nfsdirectory\E/, @entries) { + } - # if no entry in /etc/exports, one entry with default options will be added - $cmd = qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check)" >> /etc/exports}; - xCAT::Utils->runcmd($cmd, 0); - $callback->({ data => ["$nfsdirectory is added to /etc/exports with default option"] }); - } + if (xCAT::SvrUtils->ensure_nfs_export_option($nfsdirectory, 'insecure')) { + xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0); + $callback->({ data => ["added insecure to existing $nfsdirectory export"] }); + } elsif (!xCAT::SvrUtils->nfs_export_exists($nfsdirectory)) { + $cmd = qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >> /etc/exports}; + xCAT::Utils->runcmd($cmd, 0); + $callback->({ data => ["$nfsdirectory is added to /etc/exports with default option"] }); } } } @@ -1808,21 +1829,17 @@ sub setupStatemnt { if (grep /\Q$nfsdirectory\E/, @entries) { $callback->({ data => ["$nfsdirectory has been exported already!"] }); } else { - $cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check"; + $cmd = "/usr/sbin/exportfs :$nfsdirectory -o rw,no_root_squash,sync,no_subtree_check,insecure"; xCAT::Utils->runcmd($cmd, 0); $callback->({ data => ["now $nfsdirectory is exported!"] }); + } - # add the directory into /etc/exports if not exist - $cmd = "cat /etc/exports"; - @entries = xCAT::Utils->runcmd($cmd, 0); - if (my $entry = grep /\Q$nfsdirectory\E/, @entries) { - unless ($entry =~ m/rw/) { - $callback->({ data => ["The $nfsdirectory should be with rw option in /etc/exports"] }); - } - } else { - xCAT::Utils->runcmd(qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check)" >> /etc/exports}, 0); - $callback->({ data => ["$nfsdirectory is added into /etc/exports with default options"] }); - } + if (xCAT::SvrUtils->ensure_nfs_export_option($nfsdirectory, 'insecure')) { + xCAT::Utils->runcmd("/usr/sbin/exportfs -r", 0); + $callback->({ data => ["added insecure to existing $nfsdirectory export"] }); + } elsif (!xCAT::SvrUtils->nfs_export_exists($nfsdirectory)) { + xCAT::Utils->runcmd(qq{echo "$nfsdirectory *(rw,no_root_squash,sync,no_subtree_check,insecure)" >> /etc/exports}, 0); + $callback->({ data => ["$nfsdirectory is added into /etc/exports with default options"] }); } } } diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index 2ec8df1c3..5812a2190 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -9,6 +9,10 @@ use lib "$::XCATROOT/lib/perl"; use strict; use IPC::Open2; +use IPC::Open3; +use IO::Select; +use Symbol qw/gensym/; +use POSIX qw/WNOHANG/; use xCAT::Table; #use Data::Dumper; @@ -149,6 +153,97 @@ sub handled_commands return { makedhcp => "dhcp", }; } +###################################################### +# Run omshell to query a host and parse the output. +# Uses a heredoc pipe instead of open2 to avoid deadlock +# when omshell produces output before all input is consumed. +###################################################### +sub _omshell_query_host +{ + my $node = shift; + my $omapiuser = shift; + my $omapikey = shift; + my $port = shift; + + my $port_cmd = defined($port) ? "port $port\n" : ""; + my $omcmds = "${port_cmd}key $omapiuser \"$omapikey\"\nconnect\nnew host\nset name = \"$node\"\nopen\nclose\n"; + + my @output = _run_omshell($omcmds); + + return _parse_omshell_host_output($node, @output); +} + +sub _run_omshell +{ + my $omcmds = shift; + + my ($in, $out); + my $err = gensym; + my $pid = eval { open3($in, $out, $err, '/usr/bin/omshell') }; + return () if $@ || !$pid; + + print $in $omcmds; + close($in); + + my @output; + my $selector = IO::Select->new($out, $err); + my $deadline = time + 10; + while ($selector->count) { + my $remaining = $deadline - time; + if ($remaining <= 0) { + kill 'TERM', $pid; + last; + } + + foreach my $fh ($selector->can_read($remaining)) { + my $line = <$fh>; + if (defined $line) { + push @output, $line if fileno($fh) == fileno($out); + } else { + $selector->remove($fh); + close($fh); + } + } + } + + for (1 .. 10) { + last if waitpid($pid, WNOHANG) == $pid; + select(undef, undef, undef, 0.1); + } + if (waitpid($pid, WNOHANG) == 0) { + kill 'KILL', $pid; + waitpid($pid, 0); + } + + return @output; +} + +sub _parse_omshell_host_output +{ + my ($node, @output) = @_; + + my ($nname, $ipaddr, $hwaddr); + foreach my $line (@output) { + chomp $line; + if ($line =~ /^\s*name\s*=\s*"?(.*?)"?\s*$/) { + $nname = $1 if $1 eq $node; + } elsif ($line =~ /^\s*hardware-address\s*=/) { + $hwaddr = $line; + } elsif ($line =~ /^\s*ip-address\s*=\s*(.+?)\s*$/) { + my $ip = $1; + if ($ip =~ /^(?:[[:xdigit:]]{1,2}:){3}[[:xdigit:]]{1,2}$/) { + my @parts = split(/\:/, $ip); + $ipaddr = "ip-address = " . join(".", map { hex($_) } @parts); + } else { + $ipaddr = "ip-address = $ip"; + } + } + } + + $nname = $node if !$nname && $ipaddr; + return ($nname, $ipaddr, $hwaddr); +} + ###################################################### # List nodes in DHCP for both IPv4 and IPv6 ###################################################### @@ -156,154 +251,41 @@ sub listnode { my $node = shift; my $callback = shift; - my $lines; - my $ipaddr = ""; - my $hwaddr; - my $nname; my $rsp; - my ($OMOUT, $OMIN, $OMOUT6, $OMIN6); - my $usingipv6; my $omapiuser; my $omapikey; - # Collect the omapi user and key from the passwd table my $pwtab = xCAT::Table->new("passwd"); my @pws = $pwtab->getAllAttribs('key', 'username', 'password', 'cryptmethod', 'authdomain', 'comments', 'disable'); foreach (@pws) { - - # Look for the opapi entry in the passwd table - if ($_->{key} =~ "omapi") { #omapi key - # save username and password for omapi connection + if ($_->{key} =~ "omapi") { $omapiuser = $_->{username}; $omapikey = $_->{password}; } } - # Look through the networks table for networks with IPv6 format for address + my $usingipv6; my $nettab = xCAT::Table->new("networks"); my @vnets = $nettab->getAllAttribs('net', 'mgtifname', 'mask', 'dynamicrange', 'nameservers', 'ddnsdomain', 'domain'); foreach (@vnets) { - if ($_->{net} =~ /:/) { #IPv6 detected + if ($_->{net} =~ /:/) { $usingipv6 = 1; } } - # open ipv4 omshell file handles - $OMOUT will contain the response - open2($OMOUT, $OMIN, "/usr/bin/omshell "); - - # setup omapi for the connection and check for the node requested - print $OMIN "key " - . $omapiuser . " \"" - . $omapikey . "\"\n"; - print $OMIN "connect\n"; - print $OMIN "new host\n"; - - # specify which node we are looking up - print $OMIN "set name = \"$node\"\n"; - print $OMIN "open\n"; - - # the close will put the data into $OMOUT - print $OMIN "close\n"; - close($OMIN); - my $name = 0; - - # Process the output - while (<$OMOUT>) { # now read the output of sort(1) - chomp $_; - - # if this line contains the node name - if ($_ =~ $node) { - - # save the name returned - if ($name) { - $nname = $_; - $nname =~ s/name = //; - $nname =~ s/"//g; - } - $name = 1; - } - - # if this line is the hardware-address line - if ($_ =~ 'hardware-address') { - - # save the hardware address as it is with the hardware-address label - $hwaddr = $_; - } - - # if this line is the ip-address line - elsif ($_ =~ 'ip-address') { - - # convert the hex IP address to a dotted decimal address for readability - my ($ipname, $ip) = split /= /, $_; - chomp($ip); - my ($p1, $p2, $p3, $p4) = split(/\:/, $ip); - my $dp1 = hex($p1); - my $dp2 = hex($p2); - my $dp3 = hex($p3); - my $dp4 = hex($p4); - $ipaddr = "ip-address = $dp1.$dp2.$dp3.$dp4"; - } - } - - # if we collected the ip address then print out the information for this node + my ($nname, $ipaddr, $hwaddr) = _omshell_query_host($node, $omapiuser, $omapikey, undef); if ($ipaddr) { push @{ $rsp->{data} }, "$nname: $ipaddr, $hwaddr"; xCAT::MsgUtils->message("I", $rsp, $callback); } - close($OMOUT); - # if using IPv6 addresses check using omshell IPv6 port if ($usingipv6) { - open2($OMOUT6, $OMIN6, "/usr/bin/omshell "); - print $OMOUT6 "port 7912\n"; - print $OMOUT6 "connect\n"; - print $OMIN6 "key " - . $omapiuser . " \"" - . $omapikey . "\"\n"; - print $OMIN6 "connect\n"; - print $OMIN6 "new host\n"; - - # check for the node specified - print $OMIN6 "set name = \"$node\"\n"; - print $OMIN6 "open\n"; - print $OMIN6 "close\n"; - close($OMIN6); - $name = 0; - $ipaddr = ""; - while (<$OMOUT6>) { # now read the output - chomp $_; - if ($_ =~ $node) { - - # save the name - if ($name) { - $nname = $_; - $nname =~ s/name = //; - $nname =~ s/"//g; - } - $name = 1; - } - if ($_ =~ 'hardware-address') { - - # save the hardware-address - $hwaddr = $_; - } - elsif ($_ =~ 'ip-address') { - - #save the ip address - my ($ipname, $ipaddr) = split /= /, $_; - chomp($ipaddr); - } - } - - # print the information if the ip address is found - if ($ipaddr) { - push @{ $rsp->{data} }, "$nname: $ipaddr, $hwaddr"; + my ($nname6, $ipaddr6, $hwaddr6) = _omshell_query_host($node, $omapiuser, $omapikey, 7912); + if ($ipaddr6) { + push @{ $rsp->{data} }, "$nname6: $ipaddr6, $hwaddr6"; xCAT::MsgUtils->message("I", $rsp, $callback); } - - # close the IPv6 output file handle - close($OMOUT6); } } diff --git a/xCAT-server/lib/xcat/plugins/xnba.pm b/xCAT-server/lib/xcat/plugins/xnba.pm index a72b90df6..6092543c7 100644 --- a/xCAT-server/lib/xcat/plugins/xnba.pm +++ b/xCAT-server/lib/xcat/plugins/xnba.pm @@ -265,6 +265,7 @@ sub setstate { print $pcfg "exit\n"; } close($pcfg); + _write_uefi_exit_script($bootloader_root, $node, $cref->{currstate}); } elsif ($kern and $kern->{kernel}) { if ($kern->{kernel} =~ /!/) { #TODO: deprecate this, do stateless Xen like stateless ESXi my $hypervisor; @@ -360,10 +361,21 @@ sub setstate { } else { #TODO: actually, should possibly default to xCAT image? print $pcfg "LOCALBOOT 0\n"; close($pcfg); + _write_uefi_exit_script($bootloader_root, $node, $cref->{currstate}); } return (0, ""); } +sub _write_uefi_exit_script { + my ($bootloader_root, $node, $state) = @_; + + open(my $ucfg, '>', "$bootloader_root/$node.uefi"); + print $ucfg "#!gpxe\n"; + print $ucfg "#$state\n" if $state; + print $ucfg "exit\n"; + close($ucfg); +} + my $errored = 0; diff --git a/xCAT-server/sbin/xcatconfig b/xCAT-server/sbin/xcatconfig index a409f1437..e60dcbf8a 100755 --- a/xCAT-server/sbin/xcatconfig +++ b/xCAT-server/sbin/xcatconfig @@ -23,6 +23,7 @@ BEGIN use lib "$::XCATROOT/lib/perl"; use strict; use xCAT::Utils; +use xCAT::SvrUtils; use xCAT::DHCP::Backend; use xCAT::NetworkUtils; use Getopt::Long; @@ -1881,52 +1882,30 @@ sub setupLinuxexports # add tftpboot to /etc/exports - if needed # - my $cmd = "/bin/cat /etc/exports | grep '$::TFTPDIR'"; - my $outref = xCAT::Utils->runcmd("$cmd", -1); - if ($::RUNCMD_RC != 0) - { - - # ok - then add this entry - #SECURITY: this has potential for sharing private host/user keys - my $cmd = -"/bin/echo '$::TFTPDIR *(rw,no_root_squash,sync,no_subtree_check)' >> /etc/exports"; - - my $outref = xCAT::Utils->runcmd("$cmd", 0); - if ($::RUNCMD_RC != 0) + foreach my $exportdir ($::TFTPDIR, $::INSTALLDIR) { + if (!xCAT::SvrUtils->nfs_export_exists($exportdir)) { - xCAT::MsgUtils->message('E', - "Could not update the /etc/exports file."); + my $cmd = +"/bin/echo '$exportdir *(rw,no_root_squash,sync,no_subtree_check,insecure)' >> /etc/exports"; + xCAT::Utils->runcmd("$cmd", 0); + if ($::RUNCMD_RC != 0) + { + xCAT::MsgUtils->message('E', + "Could not update the /etc/exports file."); + } + else + { + verbose("Added $exportdir to the /etc/exports file."); + $changed_exports++; + } } else { - verbose("Added $::TFTPDIR to the /etc/exports file."); - $changed_exports++; - } - } - - # - # add /install to /etc/exports - if needed - # - - $cmd = "/bin/cat /etc/exports | grep '$::INSTALLDIR'"; - $outref = xCAT::Utils->runcmd("$cmd", -1); - if ($::RUNCMD_RC != 0) - { - - # ok - then add this entry - #SECURITY: this has potential for sharing private host/user keys - my $cmd = -"/bin/echo '$::INSTALLDIR *(rw,no_root_squash,sync,no_subtree_check)' >> /etc/exports"; - my $outref = xCAT::Utils->runcmd("$cmd", 0); - if ($::RUNCMD_RC != 0) - { - xCAT::MsgUtils->message('E', - "Could not update the /etc/exports file."); - } - else - { - verbose("Added $::INSTALLDIR to the /etc/exports file."); - $changed_exports++; + if (xCAT::SvrUtils->ensure_nfs_export_option($exportdir, 'insecure')) + { + verbose("Added insecure to existing $exportdir export."); + $changed_exports++; + } } } @@ -1973,8 +1952,8 @@ sub setupLinuxexports { verbose("NFS has been enabled."); } - $cmd = "/usr/sbin/exportfs -a"; - $outref = xCAT::Utils->runcmd("$cmd", 0); + my $cmd = "/usr/sbin/exportfs -a"; + my $outref = xCAT::Utils->runcmd("$cmd", 0); if ($::RUNCMD_RC != 0) { xCAT::MsgUtils->message('E', "Error with $cmd."); diff --git a/xCAT-server/share/xcat/install/scripts/pre.ubuntu.subiquity b/xCAT-server/share/xcat/install/scripts/pre.ubuntu.subiquity index d66b59280..fe5206525 100644 --- a/xCAT-server/share/xcat/install/scripts/pre.ubuntu.subiquity +++ b/xCAT-server/share/xcat/install/scripts/pre.ubuntu.subiquity @@ -205,9 +205,9 @@ base64decode() # must be the last partition on the disk - this forces us to put swap before # the main root partition, and forces us to have a fixed size swap partition # -# Also, the autoinstall.yaml file that this needs to be appended to is a -# modified cloud-init.conf file, with the top level elided - hence the -# storage block below needs to start with no indentation. +# Subiquity re-serializes /autoinstall.yaml after the first load, +# stripping the autoinstall: wrapper. The storage block is appended +# to this unwrapped file and must start at column 0. logger -t $log_label -p "info" "Generate partition file..." if [ -e /tmp/xcat.install_disk ]; then INSTALL_DISK=$(cat /tmp/xcat.install_disk) @@ -220,42 +220,113 @@ fi if [ -d /sys/firmware/efi ]; then cat </tmp/partitionfile storage: + version: 1 config: - - {id: disk-detected, ptable: gpt, path: $INSTALL_DISK, wipe: superblock, - preserve: false, name: '', grub_device: true, type: disk} - - {id: efi-part, device: disk-detected, size: 512MB, flag: boot, - type: partition, preserve: false} - - {id: swap-part, device: disk-detected, size: 2GB, flag: swap, - type: partition, preserve: false} - - {id: root-part, device: disk-detected, size: -1, type: partition, - preserve: false} - - {id: efi-part-fs, volume: efi-part, type: format, fstype: fat, label: efi} - - {id: swap-part-fs, volume: swap-part, type: format, fstype: swap} - - {id: root-part-fs, volume: root-part, type: format, fstype: ext4, - label: root} - - {id: efi-part-mount, device: efi-part-fs, type: mount, path: /boot/efi} - - {id: swap-part-mount, device: swap-part-fs, type: mount, path: none} - - {id: root-part-mount, device: root-part-fs, type: mount, path: /} + - id: disk-detected + type: disk + ptable: gpt + path: $INSTALL_DISK + wipe: superblock-recursive + preserve: false + - id: efi-part + type: partition + device: disk-detected + size: 512M + flag: boot + number: 1 + preserve: false + grub_device: true + - id: swap-part + type: partition + device: disk-detected + size: 2G + number: 2 + preserve: false + - id: root-part + type: partition + device: disk-detected + size: -1 + wipe: superblock + number: 3 + preserve: false + - id: efi-part-fs + type: format + fstype: fat32 + volume: efi-part + preserve: false + - id: swap-part-fs + type: format + fstype: swap + volume: swap-part + preserve: false + - id: root-part-fs + type: format + fstype: ext4 + volume: root-part + preserve: false + - id: efi-part-mount + type: mount + path: /boot/efi + device: efi-part-fs + - id: swap-part-mount + type: mount + path: none + device: swap-part-fs + - id: root-part-mount + type: mount + path: / + device: root-part-fs EOF else cat </tmp/partitionfile storage: + version: 1 config: - - {id: disk-detected, ptable: msdos, path: $INSTALL_DISK, wipe: superblock, - preserve: false, name: '', grub_device: true, type: disk} - - {id: boot-part, device: disk-detected, size: 512MB, flag: boot, - type: partition, preserve: false} - - {id: swap-part, device: disk-detected, size: 2GB, flag: swap, - type: partition, preserve: false} - - {id: root-part, device: disk-detected, size: -1, type: partition, - preserve: false} - - {id: boot-part-fs, volume: boot-part, type: format, fstype: ext4, label: boot} - - {id: swap-part-fs, volume: swap-part, type: format, fstype: swap} - - {id: root-part-fs, volume: root-part, type: format, fstype: ext4, - label: root} - - {id: boot-part-mount, device: boot-part-fs, type: mount, path: /boot} - - {id: swap-part-mount, device: swap-part-fs, type: mount, path: none} - - {id: root-part-mount, device: root-part-fs, type: mount, path: /} + - id: disk-detected + type: disk + ptable: gpt + path: $INSTALL_DISK + wipe: superblock-recursive + preserve: false + grub_device: true + - id: bios-grub + type: partition + device: disk-detected + size: 1M + flag: bios_grub + number: 1 + preserve: false + - id: swap-part + type: partition + device: disk-detected + size: 2G + number: 2 + preserve: false + - id: root-part + type: partition + device: disk-detected + size: -1 + wipe: superblock + number: 3 + preserve: false + - id: swap-part-fs + type: format + fstype: swap + volume: swap-part + preserve: false + - id: root-part-fs + type: format + fstype: ext4 + volume: root-part + preserve: false + - id: swap-part-mount + type: mount + path: none + device: swap-part-fs + - id: root-part-mount + type: mount + path: / + device: root-part-fs EOF fi @@ -265,6 +336,5 @@ fi exit 0 if [ "$XCATDEBUGMODE" = "1" ] || [ "$XCATDEBUGMODE" = "2" ]; then -then set +x fi diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index f2f45fe83..5ed8d4327 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -13,65 +13,81 @@ autoinstall: authorized-keys: [] install-server: true apt: - primary: - - arches: [amd64, i386] - uri: https://mirror.pnl.gov/ubuntu/ + fallback: offline-install + geoip: false + disable_suites: + - updates + - backports + - security + kernel: + package: linux-generic user-data: hostname: #HOSTNAME# disable_root: false - package_update: true - package_upgrade: true + package_update: false + package_upgrade: false timezone: #TABLE:site:key=timezone:value# chpasswd: - list: - - root:#CRYPT:passwd:key=system,username=root:password# + list: | + root:#CRYPT:passwd:key=system,username=root:password# + expire: false packages: - - bash - - nfs-common - - openssl - - isc-dhcp-client - - libc-bin - openssh-server - openssh-client - wget - - vim - - rsync - - busybox-static - - gawk - - dnsutils - - chrony - - gpg early-commands: - - '{ - echo "Running install disk detection..."; - wget http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/getinstdisk; - chmod u+x ./getinstdisk; - ./getinstdisk; - echo "Running early_command Installation script..."; - wget http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.pre; - chmod u+x #HOSTNAME#.pre; - ./#HOSTNAME#.pre; - echo "Updating storage config..."; - cat /autoinstall.yaml |head -n -1 > /tmp/autoinstall.yaml; - cat /tmp/partitionfile >> /tmp/autoinstall.yaml; - cat /tmp/autoinstall.yaml >/autoinstall.yaml; - echo "Disabling systemd-resolved..."; - rm -f /etc/resolv.conf; - echo "nameserver #TABLE:noderes:$NODE:xcatmaster#" >/etc/resolv.conf; - echo "domain #TABLE:site:key=domain:value#" >>/etc/resolv.conf; - } >>/tmp/pre-install.log' + - | + exec >/tmp/pre-install.log 2>&1 + set -eux + echo "=== xCAT early-commands ===" + echo "=== network ===" + ip addr show + ip route show + echo "=== disk detection ===" + wget -T 30 -O /tmp/getinstdisk http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/getinstdisk + test -s /tmp/getinstdisk + chmod u+x /tmp/getinstdisk + /tmp/getinstdisk + echo "=== pre-install script ===" + wget -T 30 -O /tmp/pre.sh http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.pre + test -s /tmp/pre.sh + chmod u+x /tmp/pre.sh + /tmp/pre.sh + echo "=== storage injection ===" + test -s /tmp/partitionfile + sed -i '/^\.\.\.$/d' /autoinstall.yaml + cat /tmp/partitionfile >> /autoinstall.yaml + echo "=== DNS setup ===" + rm -f /etc/resolv.conf + echo "nameserver #TABLE:noderes:$NODE:xcatmaster#" >/etc/resolv.conf + echo "domain #TABLE:site:key=domain:value#" >>/etc/resolv.conf + echo "=== early-commands complete ===" late-commands: - mkdir -p /target/var/log/xcat/ - '{ cat /tmp/pre-install.log >> /target/var/log/xcat/xcat.log; + installnic="#TABLE:noderes:$NODE:installnic#"; + installmac="#TABLE:mac:$NODE:mac#"; + installmac="$(printf ''%s'' "${installmac}" | tr ''A-F'' ''a-f'')"; + mkdir -p /target/etc/netplan; + printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " set-name: ${installnic}" " dhcp4: true" >/target/etc/netplan/00-xcat-install.yaml; + chmod 600 /target/etc/netplan/00-xcat-install.yaml; + printf ''%s\n'' ''#HOSTNAME#'' >/target/etc/hostname; + if grep -q ''^127\.0\.1\.1'' /target/etc/hosts; then + sed -i ''s/^127\.0\.1\.1.*/127.0.1.1 #HOSTNAME#/'' /target/etc/hosts; + else + printf ''%s\n'' ''127.0.1.1 #HOSTNAME#'' >>/target/etc/hosts; + fi; + mkdir -p /target/etc/cloud; + touch /target/etc/cloud/cloud-init.disabled; echo "Updating kernel command line..."; - echo "GRUB_CMDLINE_LINUX=\"#TABLE:bootparams:$NODE:kcmdline#\"" >>/target/etc/default/grub; + printf ''%s\n'' ''GRUB_CMDLINE_LINUX="#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#"'' >>/target/etc/default/grub; curtin in-target --target /target update-grub2; echo "Running late_command Installation script..."; - wget http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.post; + wget -T 30 http://#XCATVAR:XCATMASTER##COLONHTTPPORT#/install/autoinst/#HOSTNAME#.post; chmod u+x #HOSTNAME#.post; cp ./#HOSTNAME#.post /target/root/post.script; curtin in-target --target /target /root/post.script; } >>/target/var/log/xcat/xcat.log 2>&1' error-commands: - - tar -c --transform='s/^/#HOSTNAME#-logs\//' /var/crash /var/logs/installer |nc -l 8080 + - tar -c --transform='s/^/#HOSTNAME#-logs\//' /var/crash /var/log/installer /tmp/pre-install.log /autoinstall.yaml 2>/dev/null |nc -l 8080 diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.tmpl index 086b1c455..8379c61b8 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.tmpl @@ -87,6 +87,8 @@ d-i apt-setup/multiverse boolean false d-i apt-setup/universe boolean false d-i apt-setup/backports boolean false d-i apt-setup/updates boolean false +# Keep legacy d-i installs on the local mirror/content only. +d-i apt-setup/services-select multiselect ### Boot loader installation @@ -143,8 +145,10 @@ d-i preseed/late_command string \ mount -o bind /dev/pts /target/dev/pts -t devpts; \ mount -o bind /sys /target/sys; \ chroot /target /root/post.script; \ + if [ -f /target/etc/apt/sources.list ]; then \ + sed -i -e '/^[[:space:]]*deb[[:space:]].*[[:space:]][^[:space:]]*-\(security\|updates\|backports\)[[:space:]]/s/^/# /' -e '/^[[:space:]]*deb-src[[:space:]].*[[:space:]][^[:space:]]*-\(security\|updates\|backports\)[[:space:]]/s/^/# /' /target/etc/apt/sources.list; \ + fi; \ if [ -f /target/etc/network/interfaces ]; then \ cp /target/etc/network/interfaces /etc/network/interfaces; \ fi; \ } >>/target/var/log/xcat/xcat.log 2>&1 - diff --git a/xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh b/xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh index 26a21f6f3..f9525c8dc 100755 --- a/xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh +++ b/xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh @@ -40,7 +40,10 @@ then sed -i 's/^X11Forwarding .*$/X11Forwarding yes/' $ROOTDIR/etc/ssh/sshd_config sed -i 's/^KeyRegenerationInterval .*$/KeyRegenerationInterval 0/' $ROOTDIR/etc/ssh/sshd_config sed -i 's/\(.*MaxStartups.*\)/#\1/' $ROOTDIR/etc/ssh/sshd_config - echo "MaxStartups 1024" >>$ROOTDIR/etc/ssh/sshd_config + # Keep the unauthenticated connection limit below systemd's default + # 1024 soft open-file limit. Ubuntu 22.04 OpenSSH can otherwise spin + # in ppoll() before sending the SSH banner. + echo "MaxStartups 100:30:200" >>$ROOTDIR/etc/ssh/sshd_config fi if [ -r $ROOTDIR/etc/ssh/sshd_config ] @@ -77,4 +80,3 @@ chmod 600 $ROOTDIR/root/.ssh/config #chroot $ROOTDIR ssh-keygen -y -f /root/.ssh/id_rsa >/root/.ssh/id_rsa.pub - diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu20.04.x86_64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu20.04.x86_64.pkglist index dcdfb53a1..5be4be874 100644 --- a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu20.04.x86_64.pkglist +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu20.04.x86_64.pkglist @@ -16,3 +16,5 @@ gzip xz-utils cpio chrony +dracut +dracut-network diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu22.04.x86_64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu22.04.x86_64.pkglist new file mode 100644 index 000000000..baa088bac --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu22.04.x86_64.pkglist @@ -0,0 +1,21 @@ +bash +nfs-common +openssl +isc-dhcp-client +libc-bin +linux-image-generic +openssh-server +openssh-client +wget +rsync +busybox-static +gawk +bind9-dnsutils +tar +gzip +xz-utils +cpio +chrony +iproute2 +dracut +dracut-network diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist new file mode 100644 index 000000000..baa088bac --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist @@ -0,0 +1,21 @@ +bash +nfs-common +openssl +isc-dhcp-client +libc-bin +linux-image-generic +openssh-server +openssh-client +wget +rsync +busybox-static +gawk +bind9-dnsutils +tar +gzip +xz-utils +cpio +chrony +iproute2 +dracut +dracut-network diff --git a/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.netboot b/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.netboot index 2e24279a1..ba7f4313d 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.netboot +++ b/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.netboot @@ -1,7 +1,11 @@ #!/bin/sh echo $drivers dracut_install wget tar cpio gzip dash modprobe touch echo cut wc xz -dracut_install grep ifconfig hostname awk egrep grep dirname expr +dracut_install grep hostname awk egrep dirname expr ip sed tr cat dracut_install mount.nfs inst "$moddir/xcatroot" "/sbin/xcatroot" inst_hook cmdline 10 "$moddir/xcat-cmdline.sh" +# install netroot dispatcher if available (dracut 051+ with network-legacy) +if [ -f "$moddir/../40network/netroot.sh" ]; then + inst "$moddir/../40network/netroot.sh" "/sbin/netroot" +fi diff --git a/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.statelite b/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.statelite index 19e3f95b3..6f42859b2 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.statelite +++ b/xCAT-server/share/xcat/netboot/ubuntu/dracut/install.statelite @@ -1,5 +1,5 @@ #!/bin/sh echo $drivers -dracut_install wget cpio gzip dash modprobe wc touch echo cut -dracut_install grep ifconfig hostname awk egrep grep dirname expr +dracut_install wget cpio gzip dash modprobe wc touch echo cut +dracut_install grep hostname awk egrep dirname expr inst_hook pre-pivot 5 "$moddir/xcat-prepivot.sh" diff --git a/xCAT-server/share/xcat/netboot/ubuntu/dracut/module-setup.sh b/xCAT-server/share/xcat/netboot/ubuntu/dracut/module-setup.sh new file mode 100644 index 000000000..f304d7e7e --- /dev/null +++ b/xCAT-server/share/xcat/netboot/ubuntu/dracut/module-setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +check() { + return 0 +} + +depends() { + echo network + return 0 +} + +install() { + . "$moddir/install" +} diff --git a/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-cmdline.sh b/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-cmdline.sh index d445cf585..937d74cf5 100644 --- a/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-cmdline.sh +++ b/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-cmdline.sh @@ -1,4 +1,7 @@ root=1 rootok=1 netroot=xcat -echo '[ -e $NEWROOT/proc ]' > /initqueue-finished/xcatroot.sh +for HDIR in "" "/lib/dracut/hooks"; do + [ -d "${HDIR}/initqueue-finished" ] || continue + echo '[ -e $NEWROOT/proc ]' > ${HDIR}/initqueue-finished/xcatroot.sh +done diff --git a/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-prepivot.sh b/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-prepivot.sh index 5c6256df6..1127193b9 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-prepivot.sh +++ b/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcat-prepivot.sh @@ -83,7 +83,10 @@ if [ -d "$NEWROOT/etc/sysconfig" -a ! -e "$NEWROOT/etc/sysconfig/selinux" ]; the echo "SELINUX=disabled" >> "$NEWROOT/etc/sysconfig/selinux" fi -# inject new exit_if_exists -echo 'settle_exit_if_exists="--exit-if-exists=/dev/root"; rm "$job"' > /initqueue/xcat.sh -# force udevsettle to break -> /initqueue/work +# signal dracut to proceed with switch_root +[ -e /dev/root ] || ln -s null /dev/root +for HDIR in "" "/lib/dracut/hooks"; do + [ -d "${HDIR}/initqueue" ] || continue + echo 'settle_exit_if_exists="--exit-if-exists=/dev/root"; rm -f -- "$job"' > ${HDIR}/initqueue/xcat.sh + > ${HDIR}/initqueue/work +done diff --git a/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot b/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot index 46575db55..c48c46d2d 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot +++ b/xCAT-server/share/xcat/netboot/ubuntu/dracut/xcatroot @@ -181,9 +181,14 @@ cd / if [ -z $STATEMNT ]; then for lf in /tmp/dhclient.*.lease; do + [ -e "$lf" ] || continue netif=${lf#*.} netif=${netif%.*} - cp $lf "$NEWROOT/var/lib/dhclient/dhclient-$netif.leases" + if [ -d "$NEWROOT/var/lib/dhclient" ]; then + cp $lf "$NEWROOT/var/lib/dhclient/dhclient-$netif.leases" + elif [ -d "$NEWROOT/var/lib/dhcp" ]; then + cp $lf "$NEWROOT/var/lib/dhcp/dhclient.$netif.leases" + fi done if [ ! -z "$ifname" ]; then @@ -191,31 +196,58 @@ if [ -z $STATEMNT ]; then ETHX=${ifname%:$MACX*} elif [ ! -z "$netdev" ]; then ETHX=$netdev - MACX=`ip link show $netdev | grep ether | awk '{print $2}'` + if [ -e "/sys/class/net/$netdev/address" ]; then + MACX=$(cat /sys/class/net/$netdev/address) + else + MACX=$(ip link show $netdev 2>/dev/null | grep ether | awk '{print $2}') + fi elif [ ! -z "$BOOTIF" ]; then - MACX=$BOOTIF - #ETHX=`ifconfig |grep -i $BOOTIF | awk '{print $1}'` - ETHX=` ip -oneline link show |grep -i $BOOTIF|awk -F ':' '{print $2}'|grep -o "[^ ]\+\( \+[^ ]\+\)*"` + MACX=$(echo $BOOTIF | sed 's/^01-//;s/-/:/g' | tr 'A-Z' 'a-z') + for devpath in /sys/class/net/*; do + [ -e "$devpath/address" ] || continue + if [ "$(cat $devpath/address)" = "$MACX" ]; then + ETHX=${devpath##*/} + break + fi + done + [ -z "$ETHX" ] && ETHX=$(ip -oneline link show | grep -i "$MACX" | awk -F ':' '{print $2}' | grep -o "[^ ]\+\( \+[^ ]\+\)*") fi if [ ! -z "$MACX" ] && [ ! -z "$ETHX" ]; then - if [ ! -e $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX ]; then - touch $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX + if [ -x $NEWROOT/usr/sbin/netplan ]; then + mkdir -p $NEWROOT/etc/netplan + cat > $NEWROOT/etc/netplan/90-xcat.yaml < $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX + echo "BOOTPROTO=dhcp" >> $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX + echo "HWADDR=$MACX" >> $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX + echo "ONBOOT=yes" >> $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX + elif [ -d $NEWROOT/etc/network ]; then + echo "auto $ETHX" >> $NEWROOT/etc/network/interfaces + echo "iface $ETHX inet dhcp" >> $NEWROOT/etc/network/interfaces fi - echo "DEVICE=$ETHX" > $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX - echo "BOOTPROTO=dhcp" >> $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX - echo "HWADDR=$MACX" >> $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX - echo "ONBOOT=yes" >> $NEWROOT/etc/sysconfig/network-scripts/ifcfg-$ETHX fi fi cp /etc/resolv.conf "$NEWROOT/etc/" + if [ -d "$NEWROOT/etc/sysconfig" -a ! -e "$NEWROOT/etc/sysconfig/selinux" ]; then echo "SELINUX=disabled" >> "$NEWROOT/etc/sysconfig/selinux" fi -# inject new exit_if_exists -echo 'settle_exit_if_exists="--exit-if-exists=/dev/root"; rm "$job"' > /initqueue/xcat.sh -# force udevsettle to break -> /initqueue/work +# signal dracut to proceed with switch_root +[ -e /dev/root ] || ln -s null /dev/root +for HDIR in "" "/lib/dracut/hooks"; do + [ -d "${HDIR}/initqueue" ] || continue + echo 'settle_exit_if_exists="--exit-if-exists=/dev/root"; rm -f -- "$job"' > ${HDIR}/initqueue/xcat.sh + > ${HDIR}/initqueue/work +done diff --git a/xCAT-server/share/xcat/netboot/ubuntu/genimage b/xCAT-server/share/xcat/netboot/ubuntu/genimage index 98b8fe146..bbc3986ab 100755 --- a/xCAT-server/share/xcat/netboot/ubuntu/genimage +++ b/xCAT-server/share/xcat/netboot/ubuntu/genimage @@ -357,7 +357,7 @@ unless ($onlyinitrd) { open($aptconfig, ">", "$rootimg_dir/etc/apt/sources.list"); if ($srcdir) { - print $aptconfig "deb http://$masternode:$httpport$srcdir $dist main\n"; + print $aptconfig "deb http://$masternode:$httpport$srcdir $dist main restricted universe\n"; } foreach (@pkgdir_internet) { @@ -369,9 +369,19 @@ unless ($onlyinitrd) { close($aptconfig); + # Prevent package maintainer scripts from starting services while the + # image root is mounted as a chroot. + fake_policy_rc_d(); + + $rc = system("$aptgetcmd"); + if ($rc) { + xdie "Failed to update package metadata\n"; + } + # run apt-get upgrade to update any installed debs - my $aptgetcmd_update = $aptgetcmd . "&&" . $aptgetcmdby . " upgrade "; - $rc = system("$aptgetcmd_update"); + if (!$noupdate) { + $rc = system("$aptgetcmdby upgrade"); + } # Start to install pkgs in pkglist unless ($imagename) { @@ -463,6 +473,9 @@ unless ($onlyinitrd) { } my $aptgetcmd_install = $aptgetcmdby . " install --no-install-recommends " . $kernelimage; $rc = system("$aptgetcmd_install"); + if ($rc) { + xdie "Failed to install kernel package $kernelimage\n"; + } } } @@ -713,7 +726,9 @@ my @moddeps = <$moddeps>; my @checkdeps = @ndrivers; while (scalar @checkdeps) { my $driver = pop @checkdeps; - my @lines = grep /\/$driver:/, @moddeps; + my $driver_match = quotemeta($driver); + $driver_match =~ s/\\\.ko$/\\.ko(?:\\.(?:gz|xz|zst))?/; + my @lines = grep /\/$driver_match:/, @moddeps; foreach (@lines) { chomp; s/.*://; @@ -722,6 +737,7 @@ while (scalar @checkdeps) { my $dep; foreach $dep (@deps) { $dep =~ s/.*\///; + $dep =~ s/\.(?:gz|xz|zst)$//; unless (grep { $_ eq $dep } @ndrivers) { #only add if not added unshift(@checkdeps, $dep); #recursively check dependencies unshift(@ndrivers, $dep); @@ -731,18 +747,12 @@ while (scalar @checkdeps) { } } close($moddeps); -if (-d "$rootimg_dir/usr/share/dracut") { +if (-d "$rootimg_dir/usr/share/dracut" or -d "$rootimg_dir/usr/lib/dracut") { $dracutmode = 1; - # get dracut version - my $dracutver = `chroot $rootimg_dir dpkg-query -W dracut | awk '{print \$2}'| aek -F- {print \$1}'`; - if ($dracutver =~ /^\d\d\d$/) { - if ($dracutver >= "009") { - $dracutdir = "dracut_009"; - } else { - $dracutdir = "dracut"; # The default directory - } - } + my $dracutver = `chroot $rootimg_dir dpkg-query -W dracut 2>/dev/null | awk '{print \$2}'`; + chomp($dracutver); + $dracutdir = "dracut"; print "Enter the dracut mode. Dracut version: $dracutver. Dracut directory: $dracutdir.\n"; } @@ -896,7 +906,7 @@ sub getlibs { foreach (@libs) { # vdso is a shared library that's embedded in the kernel # automatically loaded whenever a new process is exec-ed - if ( $_ =~ /linux-vdso64/ ) { + if ( $_ =~ /linux-vdso/ ) { next; } if (/statically linked/ or /not a dynamic executable/) { @@ -922,12 +932,22 @@ sub getlibs { sub mkinitrd_dracut { my ($mode) = @_; # the mode is for statelite or stateless - my $dracutmpath = "$rootimg_dir/usr/share/dracut/modules.d/97xcat"; + my $dracutmpath; + if (-d "$rootimg_dir/usr/lib/dracut/modules.d") { + $dracutmpath = "$rootimg_dir/usr/lib/dracut/modules.d/97xcat"; + } else { + $dracutmpath = "$rootimg_dir/usr/share/dracut/modules.d/97xcat"; + } mkpath($dracutmpath); my $perm = (stat("$fullpath/$dracutdir/check"))[2]; cp("$fullpath/$dracutdir/check", $dracutmpath); chmod($perm & 07777, "$dracutmpath/check"); + if (-f "$fullpath/$dracutdir/module-setup.sh") { + $perm = (stat("$fullpath/$dracutdir/module-setup.sh"))[2]; + cp("$fullpath/$dracutdir/module-setup.sh", $dracutmpath); + chmod($perm & 07777, "$dracutmpath/module-setup.sh"); + } foreach (@ndrivers) { s/\.ko$//; } @@ -945,9 +965,13 @@ sub mkinitrd_dracut { $perm = (stat("$fullpath/$dracutdir/xcat-prepivot.sh"))[2]; chmod($perm & 07777, "$dracutmpath/xcat-prepivot.sh"); - # update etc/dracut.conf + # update etc/dracut.conf - use network-legacy if 40network requires wicked + my $netmod = "network"; + if (-d "$dracutmpath/../35network-legacy" && ! -x "$rootimg_dir/usr/sbin/wicked") { + $netmod = "network-legacy"; + } open($DRACUTCONF, '>', "$rootimg_dir/etc/dracut.conf"); - print $DRACUTCONF qq{dracutmodules+=" xcat nfs base network kernel-modules "\n}; + print $DRACUTCONF qq{dracutmodules+=" xcat nfs base $netmod kernel-modules "\n}; print $DRACUTCONF qq{add_drivers+=" $add_drivers "\n}; print $DRACUTCONF qq{filesystems+=" nfs "\n}; close $DRACUTCONF; @@ -975,9 +999,13 @@ sub mkinitrd_dracut { $perm = (stat("$fullpath/$dracutdir/installkernel"))[2]; chmod($perm & 07777, "$dracutmpath/installkernel"); - # update etc/dracut.conf + # update etc/dracut.conf - use network-legacy if 40network requires wicked + my $netmod = "network"; + if (-d "$dracutmpath/../35network-legacy" && ! -x "$rootimg_dir/usr/sbin/wicked") { + $netmod = "network-legacy"; + } open($DRACUTCONF, '>', "$rootimg_dir/etc/dracut.conf"); - print $DRACUTCONF qq{dracutmodules+=" xcat nfs base network kernel-modules "\n}; + print $DRACUTCONF qq{dracutmodules+=" xcat nfs base $netmod kernel-modules "\n}; print $DRACUTCONF qq{add_drivers+=" $add_drivers "\n}; close $DRACUTCONF; } else { @@ -1152,7 +1180,7 @@ EOS1 #print $inifile "mknod /dev/hvc7 c 229 7\n"; foreach (@ndrivers) { - print $inifile "insmod /lib/$_\n"; + print $inifile "[ -f /lib/$_ ] && insmod /lib/$_\n"; } @@ -1188,13 +1216,24 @@ PRINIC=$prinic NODESTATUS='y' XCATIPORT="3002" +iface_exists() { + [ -n "\$1" ] && [ -d "/sys/class/net/\$1" ] +} + for i in `cat /proc/cmdline`; do KEY=`echo \$i |awk -F= '{print \$1}'` if [ "\$KEY" == 'netdev' ]; then NETDEV=`echo \$i |awk -F= '{print \$2}'` elif [ "\$KEY" == 'BOOTIF' ]; then - VALUE=`echo \$i |awk -F= '{print \$2}'|sed -e s/^01-// -e s/-/:/g` - BOOTIF=`ifconfig -a|grep -i "hwaddr \$VALUE"|awk '{print \$1}'` + VALUE=`echo \$i |awk -F= '{print \$2}'|sed -e s/^01-// -e s/-/:/g|tr 'A-Z' 'a-z'` + for devpath in /sys/class/net/*; do + [ -e "\$devpath/address" ] || continue + if [ "\`cat \$devpath/address\`" = "\$VALUE" ]; then + BOOTIF=\${devpath##*/} + break + fi + done + [ -z "\$BOOTIF" ] && BOOTIF=`ifconfig -a|grep -i "hwaddr \$VALUE"|awk '{print \$1}'` elif [ "\$KEY" == 'XCAT' ]; then VALUE=`echo \$i |awk -F= '{print \$2}'` # format: XCAT=xcatmaster:3001 @@ -1215,16 +1254,23 @@ for i in `cat /proc/cmdline`; do done if [ -z "\$IFACE" ]; then - if [ ! -z "\$NETDEV" ]; then - IFACE=\$NETDEV - elif [ ! -z "\$BOOTIF" ]; then + if iface_exists "\$BOOTIF"; then IFACE=\$BOOTIF - elif [ ! -z "\$PRINIC" ]; then + elif iface_exists "\$NETDEV"; then + IFACE=\$NETDEV + elif iface_exists "\$PRINIC"; then IFACE=\$PRINIC else - echo "\${RED}Couldn't find the proper booting device, switch to shell...\${RESET}" - shell - exit + for devpath in /sys/class/net/*; do + IFACE=\${devpath##*/} + [ "\$IFACE" != "lo" ] && break + IFACE= + done + if [ -z "\$IFACE" ]; then + echo "\${RED}Couldn't find the proper booting device, switch to shell...\${RESET}" + shell + exit + fi fi fi @@ -1666,9 +1712,10 @@ EOMS } # add extra commands for initrd - foreach ("usr/bin/dig", "bin/busybox", "bin/bash", "sbin/mount.nfs", "usr/bin/rsync", "sbin/insmod", "sbin/udevadm", "sbin/modprobe", "sbin/blkid", "sbin/depmod", "usr/bin/wget", "usr/bin/xz", "bin/gzip", "bin/tar") { - getlibs($_); - push @filestoadd, $_; + foreach my $dest ("usr/bin/dig", "bin/busybox", "bin/bash", "sbin/mount.nfs", "usr/bin/rsync", "sbin/insmod", "sbin/udevadm", "sbin/modprobe", "sbin/blkid", "sbin/depmod", "usr/bin/wget", "usr/bin/xz", "bin/gzip", "bin/tar") { + my $src = find_rootimg_file($dest); + getlibs($src); + push @filestoadd, [ $src, $dest ]; } # Workaround for Ubuntu 18.04 busybox bug @@ -1691,16 +1738,16 @@ EOMS if ($arch =~ /x86_64/) { - push @filestoadd, "lib64/libnss_dns.so.2"; - #the path of libnss_dns.so.2 for ubuntu 14.4+ - push @filestoadd, "lib/x86_64-linux-gnu/libnss_dns.so.2"; - push @filestoadd, "lib64/libresolv.so.2"; + foreach ("lib64/libnss_dns.so.2", "lib/x86_64-linux-gnu/libnss_dns.so.2", "lib64/libresolv.so.2") { + push @filestoadd, $_ if (-e "$rootimg_dir/$_"); + } } elsif ($arch =~ /ppc64el/) { - push @filestoadd, "lib/powerpc64le-linux-gnu/libnss_files.so.2"; - push @filestoadd, "lib/powerpc64le-linux-gnu/libnss_dns.so.2"; + foreach ("lib/powerpc64le-linux-gnu/libnss_files.so.2", "lib/powerpc64le-linux-gnu/libnss_dns.so.2") { + push @filestoadd, $_ if (-e "$rootimg_dir/$_"); + } } else { - push @filestoadd, "lib/libnss_dns.so.2"; + push @filestoadd, "lib/libnss_dns.so.2" if (-e "$rootimg_dir/lib/libnss_dns.so.2"); } push @filestoadd, keys %libhash; @@ -1716,9 +1763,7 @@ EOMS } elsif (-f "$pathtofiles/" . $_->[0]) { $srcpath = "$pathtofiles/" . $_->[0]; } - mkpath(dirname("/tmp/xcatinitrd.$$/" . $_->[1])); - copy($srcpath, "/tmp/xcatinitrd.$$/" . $_->[1]); - chmod 0755, "/tmp/xcatinitrd.$$/" . $_->[1]; + copy_initrd_file($srcpath, "/tmp/xcatinitrd.$$/" . $_->[1]); } else { #print "$_\n"; @@ -1728,9 +1773,7 @@ EOMS } elsif (-f "$pathtofiles/$_") { $srcpath = "$pathtofiles/$_"; } - mkpath(dirname("/tmp/xcatinitrd.$$/$_")); - copy("$srcpath", "/tmp/xcatinitrd.$$/$_"); - chmod 0755, "/tmp/xcatinitrd.$$/" . $_; + copy_initrd_file($srcpath, "/tmp/xcatinitrd.$$/$_"); } } @@ -1743,7 +1786,7 @@ EOMS system("cp $rootimg_dir/lib/modules/$kernelver/modules.order /tmp/xcatinitrd.$$/lib/modules/$kernelver/modules.order"); } - system("chroot /tmp/xcatinitrd.$$/ depmod $kernelver"); + system("chroot /tmp/xcatinitrd.$$/ /sbin/depmod $kernelver"); # Copy udev and network scripts into initrd for s390x, which also works for other platforms # udev @@ -1789,7 +1832,9 @@ sub isaptdir { sub isnetdriver { foreach (@ndrivers) { - if ($File::Find::name =~ /\/$_/) { + my $driver_match = quotemeta($_); + $driver_match =~ s/\\\.ko$/\\.ko(?:\\.(?:gz|xz|zst))?/; + if ($File::Find::name =~ /\/$driver_match$/) { my $filetoadd = $File::Find::name; $filetoadd =~ s!$rootimg_dir/!!; push @filestoadd, [ $filetoadd, "lib/$_" ]; @@ -1797,6 +1842,41 @@ sub isnetdriver { } } +sub find_rootimg_file { + my ($file) = @_; + + return $file if (-e "$rootimg_dir/$file"); + + my $base = basename($file); + foreach my $candidate ("usr/bin/$base", "bin/$base", "usr/sbin/$base", "sbin/$base") { + return $candidate if (-e "$rootimg_dir/$candidate"); + } + + xdie "Failed to find $file in $rootimg_dir\n"; +} + +sub copy_initrd_file { + my ( $src, $dest ) = @_; + + mkpath(dirname($dest)); + if ( $src =~ /\.ko\.(gz|xz|zst)$/ ) { + my %decompressor = ( + gz => [ 'gzip', '-dc' ], + xz => [ 'xz', '-dc' ], + zst => [ 'zstd', '-dc' ], + ); + my @cmd = ( @{ $decompressor{$1} }, $src ); + open( my $in, '-|', @cmd ) || xdie "Failed to decompress $src\n"; + open( my $out, '>', $dest ) || xdie "Failed to open $dest\n"; + copy( $in, $out ) || xdie "Failed to copy $src to $dest\n"; + close($out); + close($in) || xdie "Failed to decompress $src\n"; + } else { + copy( $src, $dest ) || xdie "Failed to copy $src to $dest\n"; + } + chmod 0755, $dest; +} + sub postscripts { generic_post(); @@ -2187,5 +2267,3 @@ sub usage { return 0; } - - diff --git a/xCAT-test/unit/copycds_packages_integrity.t b/xCAT-test/unit/copycds_packages_integrity.t new file mode 100644 index 000000000..f0ea902d7 --- /dev/null +++ b/xCAT-test/unit/copycds_packages_integrity.t @@ -0,0 +1,42 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; +use File::Find; + +# This test verifies that copycds does not leave empty Packages files +# alongside valid Packages.gz files. An empty Packages file causes +# apt's cdrom handler to fail with a hash mismatch during autoinstall. + +my $installdir = $ENV{INSTALLDIR} || '/install'; + +plan skip_all => "$installdir not found" unless -d $installdir; + +my @empty_packages; +my @ok_packages; + +find(sub { + return unless $_ eq 'Packages'; + return unless -f $File::Find::name; + + my $gz = "$File::Find::name.gz"; + if (-s $File::Find::name == 0 && -f $gz && -s $gz > 0) { + push @empty_packages, $File::Find::name; + } elsif (-s $File::Find::name > 0) { + push @ok_packages, $File::Find::name; + } +}, "$installdir"); + +if (@empty_packages || @ok_packages) { + is(scalar @empty_packages, 0, + 'no empty Packages files alongside valid Packages.gz') + or diag("Empty Packages files found:\n " . join("\n ", @empty_packages)); + + cmp_ok(scalar @ok_packages, '>', 0, + 'at least one non-empty Packages file exists') + if @ok_packages; +} else { + plan skip_all => "no apt repo trees found under $installdir"; +} + +done_testing(); diff --git a/xCAT-test/unit/dhcp_backend_selection.t b/xCAT-test/unit/dhcp_backend_selection.t index 75b25e57e..8363a851d 100644 --- a/xCAT-test/unit/dhcp_backend_selection.t +++ b/xCAT-test/unit/dhcp_backend_selection.t @@ -49,6 +49,12 @@ is( 'Ubuntu 22.04 defaults to Kea' ); +is( + xCAT::DHCP::Backend->default_backend( platform => '', os => 'ubuntu22.04.5', os_name => 'ubuntu', version => '22.04.5' ), + 'kea', + 'Ubuntu 22.04 point releases default to Kea' +); + is( xCAT::DHCP::Backend->default_backend( platform => '', os => 'ubuntu20.04', os_name => 'ubuntu', version => '20.04' ), 'isc', diff --git a/xCAT-test/unit/dhcp_kea_renderer.t b/xCAT-test/unit/dhcp_kea_renderer.t index 365f0e0a4..c3a5de13e 100644 --- a/xCAT-test/unit/dhcp_kea_renderer.t +++ b/xCAT-test/unit/dhcp_kea_renderer.t @@ -76,7 +76,8 @@ is_deeply( $config->{Dhcp4}{'interfaces-config'}{interfaces}, ['eth0'], 'interfa is( $config->{Dhcp4}{'valid-lifetime'}, 600, 'valid lifetime is rendered' ); is( $config->{Dhcp4}{'lease-database'}{type}, 'memfile', 'memfile lease backend is the default' ); is( $config->{Dhcp4}{'reservations-in-subnet'}, JSON::true, 'subnet host reservations are enabled by default' ); -is( $config->{Dhcp4}{'reservations-out-of-pool'}, JSON::true, 'out-of-pool host reservations are enabled by default' ); +is( $config->{Dhcp4}{'reservations-out-of-pool'}, JSON::true, 'out-of-pool host reservations are enabled for xCAT static addresses' ); +is( $config->{Dhcp4}{'match-client-id'}, JSON::false, 'DHCPv4 leases match MAC reservations when client-id changes across boot stages' ); my $subnet = $config->{Dhcp4}{subnet4}[0]; is( $subnet->{id}, 1, 'subnet id is rendered' ); @@ -191,8 +192,9 @@ is( $empty_boot_subnet->{'boot-file-name'}, '', 'empty boot-file-name is preserv my $reservation_policy_json = $backend->render_dhcp4_config( { - 'reservations-in-subnet' => 1, - 'reservations-out-of-pool' => 1, + 'reservations-in-subnet' => 0, + 'reservations-out-of-pool' => 0, + 'match-client-id' => 1, subnets => [ { id => 9, @@ -203,8 +205,9 @@ my $reservation_policy_json = $backend->render_dhcp4_config( } ); my $reservation_policy_config = decode_json($reservation_policy_json); -is( $reservation_policy_config->{Dhcp4}{'reservations-in-subnet'}, JSON::true, 'reservation in-subnet policy can be overridden' ); -is( $reservation_policy_config->{Dhcp4}{'reservations-out-of-pool'}, JSON::true, 'reservation out-of-pool policy can be overridden' ); +is( $reservation_policy_config->{Dhcp4}{'reservations-in-subnet'}, JSON::false, 'reservation in-subnet policy can be overridden' ); +is( $reservation_policy_config->{Dhcp4}{'reservations-out-of-pool'}, JSON::false, 'reservation out-of-pool policy can be overridden' ); +is( $reservation_policy_config->{Dhcp4}{'match-client-id'}, JSON::true, 'client-id lease matching policy can be overridden' ); my $comment_dir = tempdir(CLEANUP => 1); my $commented_config = "$comment_dir/kea-dhcp4.conf"; diff --git a/xCAT-test/unit/dhcp_omshell_parse.t b/xCAT-test/unit/dhcp_omshell_parse.t new file mode 100644 index 000000000..3315531ef --- /dev/null +++ b/xCAT-test/unit/dhcp_omshell_parse.t @@ -0,0 +1,43 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../xCAT-server/lib"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use Test::More; + +$ENV{XCATCFG} ||= 'SQLite:/tmp'; + +my $source_dhcp_plugin = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/dhcp.pm"; +if ( -f $source_dhcp_plugin ) { + require $source_dhcp_plugin; +} else { + require xCAT_plugin::dhcp; +} + +my ($name, $ip, $mac) = xCAT_plugin::dhcp::_parse_omshell_host_output( + 'node01', + 'name = "node01"', + 'ip-address = 0a:00:00:05', + 'hardware-address = 00:11:22:33:44:55', +); + +is($name, 'node01', 'host name is parsed'); +is($ip, 'ip-address = 10.0.0.5', 'IPv4 OMAPI hex address is converted to dotted decimal'); +is($mac, 'hardware-address = 00:11:22:33:44:55', 'hardware address is preserved'); + +($name, $ip, $mac) = xCAT_plugin::dhcp::_parse_omshell_host_output( + 'nodev6', + 'name = "nodev6"', + 'ip-address = 2001:db8::50', + 'hardware-address = 00:aa:bb:cc:dd:ee', +); + +is($name, 'nodev6', 'IPv6 host name is parsed'); +is($ip, 'ip-address = 2001:db8::50', 'IPv6 address is preserved'); +is($mac, 'hardware-address = 00:aa:bb:cc:dd:ee', 'IPv6 hardware address is preserved'); + +done_testing(); diff --git a/xCAT-test/unit/nfs_export_options.t b/xCAT-test/unit/nfs_export_options.t new file mode 100644 index 000000000..7575aab3b --- /dev/null +++ b/xCAT-test/unit/nfs_export_options.t @@ -0,0 +1,50 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use File::Temp qw/tempdir/; +use Test::More; + +use xCAT::SvrUtils; + +my $dir = tempdir(CLEANUP => 1); +my $exports = "$dir/exports"; + +open(my $fh, '>', $exports) or die "Unable to write $exports: $!"; +print $fh <<'EOF'; +# comment +/install *(rw,no_root_squash,sync,no_subtree_check) # install tree +/tftpboot -rw,no_root_squash +/other host1(rw) host2(ro,insecure) +EOF +close($fh); + +ok(xCAT::SvrUtils->nfs_export_exists('/install', files => [$exports]), 'target export is detected'); +ok(!xCAT::SvrUtils->nfs_export_exists('/missing', files => [$exports]), 'missing export is not detected'); + +ok( + xCAT::SvrUtils->ensure_nfs_export_option('/install', 'insecure', files => [$exports]), + 'missing option is added to target export' +); +my $content = do { local $/; open(my $rfh, '<', $exports) or die $!; <$rfh> }; +like($content, qr{^/install \*\(rw,no_root_squash,sync,no_subtree_check,insecure\) # install tree$}m, 'target export gains insecure and keeps comment'); +like($content, qr{^/tftpboot -rw,no_root_squash$}m, 'non-target export is unchanged'); +like($content, qr{^/other host1\(rw\) host2\(ro,insecure\)$}m, 'unrelated multi-client export is unchanged'); + +ok( + !xCAT::SvrUtils->ensure_nfs_export_option('/install', 'insecure', files => [$exports]), + 'existing option is not duplicated' +); + +ok( + xCAT::SvrUtils->ensure_nfs_export_option('/tftpboot', 'insecure', files => [$exports]), + 'dash-style export gains option' +); +$content = do { local $/; open(my $rfh, '<', $exports) or die $!; <$rfh> }; +like($content, qr{^/tftpboot -rw,no_root_squash,insecure$}m, 'dash-style export is updated correctly'); + +done_testing(); diff --git a/xCAT-test/unit/probe_utils_dhcp_reply.t b/xCAT-test/unit/probe_utils_dhcp_reply.t new file mode 100644 index 000000000..4d432a0d4 --- /dev/null +++ b/xCAT-test/unit/probe_utils_dhcp_reply.t @@ -0,0 +1,52 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; + +use Test::More; + +require probe_utils; + +ok( + probe_utils::dhcp_query_reply_matches( + 'xcatmntest: ip-address = 10.10.0.254, hardware-address = aa:aa:aa:aa:aa:aa', + 'xcatmntest', + '10.10.0.254', + 'aa:aa:aa:aa:aa:aa', + ), + 'ISC-style makedhcp query output with comma matches' +); + +ok( + probe_utils::dhcp_query_reply_matches( + 'xcatmntest: ip-address = 10.10.0.254 hardware-address = aa:aa:aa:aa:aa:aa', + 'xcatmntest', + '10.10.0.254', + 'aa:aa:aa:aa:aa:aa', + ), + 'Kea-style makedhcp query output without comma matches' +); + +is( + probe_utils::dhcp_query_reply_mac( + 'node01: ip-address = 192.0.2.10 hardware-address = AA:BB:CC:DD:EE:FF', + 'node01', + '192.0.2.10', + ), + 'AA:BB:CC:DD:EE:FF', + 'DHCP query MAC is extracted without normalizing display case' +); + +ok( + !probe_utils::dhcp_query_reply_matches( + 'xcatmntest: ip-address = 10.10.0.99 hardware-address = aa:aa:aa:aa:aa:aa', + 'xcatmntest', + '10.10.0.254', + 'aa:aa:aa:aa:aa:aa', + ), + 'mismatched IP does not match' +); + +done_testing(); diff --git a/xCAT-test/unit/probe_utils_netplan.t b/xCAT-test/unit/probe_utils_netplan.t new file mode 100644 index 000000000..1f1a8c1e2 --- /dev/null +++ b/xCAT-test/unit/probe_utils_netplan.t @@ -0,0 +1,34 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; + +use Test::More; + +require probe_utils; + +my %netplan = ( + 'ethernets.eth0' => 'renderer: networkd', + 'ethernets.eth0.addresses' => "- 10.0.0.2/24\n", + 'ethernets.eth0.dhcp4' => 'false', + 'ethernets.eth1' => 'renderer: networkd', + 'ethernets.eth1.addresses' => "- 10.0.0.3/24\n", + 'ethernets.eth1.dhcp4' => 'true', + 'vlans.bond0\.123' => 'renderer: networkd', + 'vlans.bond0\.123.addresses' => "- 10.0.123.5/24\n", +); + +{ + no warnings 'redefine'; + local *probe_utils::_command_available = sub { return $_[0] eq 'netplan' ? 1 : 0; }; + local *probe_utils::_netplan_get = sub { return $netplan{ $_[0] }; }; + + ok(probe_utils::_netplan_has_static_ip('eth0', '10.0.0.2'), 'static netplan address is detected'); + ok(!probe_utils::_netplan_has_static_ip('eth0', '10.0.0.99'), 'wrong address is not treated as static'); + ok(!probe_utils::_netplan_has_static_ip('eth1', '10.0.0.3'), 'dhcp4 true is not treated as static'); + ok(probe_utils::_netplan_has_static_ip('bond0.123', '10.0.123.5'), 'dotted VLAN interface is escaped for netplan get'); +} + +done_testing(); diff --git a/xCAT-test/unit/statelite_add_ssh.t b/xCAT-test/unit/statelite_add_ssh.t new file mode 100644 index 000000000..b3dc6ddbe --- /dev/null +++ b/xCAT-test/unit/statelite_add_ssh.t @@ -0,0 +1,17 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +my $script_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/netboot/add-on/statelite/add_ssh" : ''; +$script_path = "xCAT-server/share/xcat/netboot/add-on/statelite/add_ssh" + unless -f $script_path; + +plan skip_all => "add_ssh not found" unless -f $script_path; + +my $script = do { local $/; open my $fh, '<', $script_path or die $!; <$fh> }; + +like($script, qr/MaxStartups 100:30:200/, 'add_ssh caps MaxStartups below systemd soft nofile limit'); +unlike($script, qr/echo\s+"MaxStartups 1024"/, 'add_ssh does not force MaxStartups 1024'); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_preseed_template.t b/xCAT-test/unit/ubuntu_preseed_template.t new file mode 100644 index 000000000..3f0e41dde --- /dev/null +++ b/xCAT-test/unit/ubuntu_preseed_template.t @@ -0,0 +1,38 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +my $tmpl_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/install/ubuntu/compute.tmpl" : ''; +$tmpl_path = "xCAT-server/share/xcat/install/ubuntu/compute.tmpl" + unless -f $tmpl_path; + +plan skip_all => "compute.tmpl not found" unless -f $tmpl_path; + +my $tmpl = do { local $/; open my $fh, '<', $tmpl_path or die $!; <$fh> }; +my $template_pm_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/lib/perl/xCAT/Template.pm" : ''; +$template_pm_path = "xCAT-server/lib/perl/xCAT/Template.pm" + unless -f $template_pm_path; + +like($tmpl, qr/^d-i apt-setup\/multiverse boolean false$/m, 'legacy Ubuntu preseed disables multiverse'); +like($tmpl, qr/^d-i apt-setup\/universe boolean false$/m, 'legacy Ubuntu preseed disables universe'); +like($tmpl, qr/^d-i apt-setup\/backports boolean false$/m, 'legacy Ubuntu preseed disables backports'); +like($tmpl, qr/^d-i apt-setup\/updates boolean false$/m, 'legacy Ubuntu preseed disables release updates'); +like($tmpl, qr/^d-i apt-setup\/services-select multiselect\s*$/m, 'legacy Ubuntu preseed disables security/update services for offline installs'); +unlike($tmpl, qr/^d-i apt-setup\/services-select multiselect .*\S/m, 'legacy Ubuntu preseed does not select any external apt services'); +like($tmpl, qr/sed -i .*security.*updates.*backports.*\/target\/etc\/apt\/sources\.list/s, + 'legacy Ubuntu late command comments disabled apt service suites in the installed target'); + +SKIP: { + skip "Template.pm not found", 3 unless -f $template_pm_path; + my $template_pm = do { local $/; open my $fh, '<', $template_pm_path or die $!; <$fh> }; + + like($template_pm, qr/\$ENV\{HTTPPORT\} \|\| \$ENV\{httpport\} \|\| '80'/, + 'legacy Ubuntu mirror spec uses the rendered HTTP port for local mirrors'); + like($template_pm, qr/d-i apt-setup\/security_host string \$security_host/, + 'legacy Ubuntu mirror spec redirects installer security host to rendered xCAT master'); + like($template_pm, qr/d-i apt-setup\/security_path string \$pkgdir/, + 'legacy Ubuntu mirror spec redirects installer security path to the local pkgdir'); +} + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_bootparams.t b/xCAT-test/unit/ubuntu_subiquity_bootparams.t new file mode 100644 index 000000000..bea2de236 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_bootparams.t @@ -0,0 +1,18 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +my $plugin_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/lib/perl/xCAT_plugin/debian.pm" : ''; +$plugin_path = "xCAT-server/lib/xcat/plugins/debian.pm" + unless -f $plugin_path; + +plan skip_all => "debian.pm not found" unless -f $plugin_path; + +my $src = do { local $/; open my $fh, '<', $plugin_path or die $!; <$fh> }; + +like($src, qr/autoinstall ip=dhcp netboot=nfs/, 'subiquity bootparams enable autoinstall'); +like($src, qr/ds=nocloud-net;s=http:\/\//, 'subiquity bootparams point at NoCloud seed'); +like($src, qr/\$kcmdline\s*\.=\s*" ---";/, 'subiquity bootparams end with installer argument separator'); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_storage.t b/xCAT-test/unit/ubuntu_subiquity_storage.t new file mode 100644 index 000000000..844a493c6 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_storage.t @@ -0,0 +1,45 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +my $pre_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/install/scripts/pre.ubuntu.subiquity" : ''; +$pre_path = "xCAT-server/share/xcat/install/scripts/pre.ubuntu.subiquity" + unless -f $pre_path; + +plan skip_all => "pre.ubuntu.subiquity not found" unless -f $pre_path; + +my $script = do { local $/; open my $fh, '<', $pre_path or die $!; <$fh> }; + +# Shell syntax check +my $rc = system("bash -n $pre_path 2>/dev/null"); +is($rc, 0, 'pre.ubuntu.subiquity passes bash -n syntax check'); + +# UEFI storage layout checks +like($script, qr/if \[ -d \/sys\/firmware\/efi \]/, 'script detects UEFI via /sys/firmware/efi'); + +# UEFI: grub_device on EFI partition, NOT on disk +like($script, qr/id: efi-part.*grub_device: true/s, 'UEFI: grub_device on EFI partition'); + +# UEFI: EFI partition formatted as fat32 +like($script, qr/id: efi-part-fs.*fstype: fat32/s, 'UEFI: EFI partition formatted fat32'); + +# UEFI: EFI partition mounted at /boot/efi +like($script, qr/efi-part-mount.*path: \/boot\/efi/s, 'UEFI: EFI partition mounted at /boot/efi'); + +# BIOS storage layout checks +like($script, qr/id: bios-grub.*flag: bios_grub/s, 'BIOS: has bios_grub partition'); +like($script, qr/id: disk-detected.*grub_device: true/s, 'BIOS: grub_device on disk'); + +# Both paths must have storage: version: 1 +my @storage_version = ($script =~ /storage:\s*\n\s*version:\s*1/g); +is(scalar @storage_version, 2, 'both UEFI and BIOS have storage: version: 1'); + +# Both paths write to /tmp/partitionfile +my @partfile = ($script =~ /\/tmp\/partitionfile/g); +cmp_ok(scalar @partfile, '>=', 2, 'both paths write to /tmp/partitionfile'); + +# Storage at column 0 (for re-serialized autoinstall.yaml) +like($script, qr/^storage:\n version: 1/m, 'storage block starts at column 0'); + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_template.t b/xCAT-test/unit/ubuntu_subiquity_template.t new file mode 100644 index 000000000..2691d8627 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_template.t @@ -0,0 +1,66 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +my $tmpl_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/share/xcat/install/ubuntu/compute.subiquity.tmpl" : ''; +$tmpl_path = "xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl" + unless -f $tmpl_path; + +plan skip_all => "compute.subiquity.tmpl not found" unless -f $tmpl_path; + +my $tmpl = do { local $/; open my $fh, '<', $tmpl_path or die $!; <$fh> }; + +like($tmpl, qr/^#cloud-config/, 'template starts with #cloud-config'); +like($tmpl, qr/autoinstall:/, 'template has autoinstall: key'); +like($tmpl, qr/version:\s*1/, 'template has version: 1'); + +unlike($tmpl, qr/^\s*identity:/m, 'template must not have identity section (use user-data instead)'); +like($tmpl, qr/kernel:/, 'template has kernel section'); +like($tmpl, qr/package:\s*linux-generic/, 'template specifies linux-generic kernel'); +like($tmpl, qr/apt:/, 'template has apt section'); +like($tmpl, qr/fallback:\s*offline-install/, 'template uses offline-install fallback'); +unlike($tmpl, qr/WARN: no partitionfile/, 'template does not silently fall back when xCAT pre-script fails'); +unlike($tmpl, qr/INSTALL_DISK=""/, 'template does not guess an install disk in early-commands'); +like($tmpl, qr/geoip:\s*false/, 'template disables geoip'); + +like($tmpl, qr/ssh:/, 'template has ssh section'); +like($tmpl, qr/install-server:\s*true/, 'template enables ssh install-server'); + +unlike($tmpl, qr/package_update:\s*true/, 'template does not enable package_update'); +unlike($tmpl, qr/^\s*-\s+nfs-common\s*$/m, 'template does not require nfs-common from offline ISO packages'); + +# YAML safety: use printf with single-quoted arguments instead of shell-specific +# escape sequences. dash does not portably interpret printf \xNN. +unlike($tmpl, qr/echo.*GRUB_CMDLINE.*\\"/, 'no escaped double quotes in echo GRUB line'); +unlike($tmpl, qr/\\\\x22/, 'template does not rely on non-portable printf hex escapes'); +like($tmpl, qr/printf ''%s\\n'' ''GRUB_CMDLINE_LINUX="#TABLEBLANKOKAY:bootparams:\$NODE:kcmdline#"''/, 'GRUB line uses portable printf quoting'); +like($tmpl, qr/\/target\/etc\/netplan\/00-xcat-install\.yaml/, 'template writes an xCAT-owned target netplan file'); +like($tmpl, qr/installnic="#TABLE:noderes:\$NODE:installnic#"/, 'target netplan uses node installnic'); +like($tmpl, qr/installmac="#TABLE:mac:\$NODE:mac#"/, 'target netplan uses node MAC'); +like($tmpl, qr/installmac="\$\(printf ''%s'' "\$\{installmac\}" \| tr ''A-F'' ''a-f''\)"/, 'target netplan normalizes MAC case'); +like($tmpl, qr/printf ''%s\\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \\"\$\{installmac\}\\"" " set-name: \$\{installnic\}" " dhcp4: true" >\/target\/etc\/netplan\/00-xcat-install\.yaml;/, 'target netplan printf stays on one shell line'); +like($tmpl, qr/" macaddress: \\"\$\{installmac\}\\""/, 'target netplan matches by MAC address'); +like($tmpl, qr/" set-name: \$\{installnic\}"/, 'target netplan sets the expected installnic name'); +like($tmpl, qr/"\s+dhcp4: true"/, 'target netplan enables DHCPv4 on installnic'); +like($tmpl, qr/printf ''%s\\n'' ''#HOSTNAME#'' >\/target\/etc\/hostname/, 'template writes target hostname before disabling cloud-init'); +like($tmpl, qr/sed -i ''s\/\^127\\\.0\\\.1\\\.1\.\*\/127\.0\.1\.1 #HOSTNAME#\/'' \/target\/etc\/hosts/, 'template updates target hosts entry for hostname'); +like($tmpl, qr/touch \/target\/etc\/cloud\/cloud-init\.disabled/, 'target cloud-init is disabled after target netplan is written'); + +# Regression: downloaded files are required and checked with test -s before use. +like($tmpl, qr/wget -T 30 -O \/tmp\/getinstdisk http:\/\/#XCATVAR:XCATMASTER#/, 'getinstdisk download is required'); +like($tmpl, qr/test -s \/tmp\/getinstdisk/, 'getinstdisk checked with -s not -x'); +like($tmpl, qr/wget -T 30 -O \/tmp\/pre\.sh http:\/\/#XCATVAR:XCATMASTER#/, 'pre.sh download is required'); +like($tmpl, qr/test -s \/tmp\/pre\.sh/, 'pre.sh checked with -s not -x'); +like($tmpl, qr/test -s \/tmp\/partitionfile/, 'partitionfile from pre-script is required'); +unlike($tmpl, qr/wget .*?\|\| true/, 'xCAT control artifact downloads are not masked'); +unlike($tmpl, qr/if \[ -x \/tmp\/getinstdisk \]/, 'getinstdisk not checked with -x'); +unlike($tmpl, qr/if \[ -x \/tmp\/pre\.sh \]/, 'pre.sh not checked with -x'); + +# Regression: disable_suites must be release-independent (no noble-*, jammy-*, etc.) +unlike($tmpl, qr/noble-|jammy-|focal-/, 'disable_suites uses release-independent names'); +like($tmpl, qr/- updates/, 'disable_suites includes updates'); +like($tmpl, qr/- backports/, 'disable_suites includes backports'); +like($tmpl, qr/- security/, 'disable_suites includes security'); + +done_testing(); diff --git a/xCAT-test/unit/xnba_semicolon.t b/xCAT-test/unit/xnba_semicolon.t new file mode 100644 index 000000000..ea07dedcc --- /dev/null +++ b/xCAT-test/unit/xnba_semicolon.t @@ -0,0 +1,31 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +my $xnba_path = defined $ENV{XCATROOT} ? "$ENV{XCATROOT}/lib/perl/xCAT_plugin/xnba.pm" : ''; +$xnba_path = "xCAT-server/lib/xcat/plugins/xnba.pm" + unless -f $xnba_path; + +plan skip_all => "xnba.pm not found" unless -f $xnba_path; + +my $src = do { local $/; open my $fh, '<', $xnba_path or die $!; <$fh> }; + +# iPXE only treats a standalone ; token as a command separator. +# A ; embedded inside an argument value (e.g. ds=nocloud-net;s=...) +# is NOT split by iPXE's parser. Therefore xnba.pm must NOT escape +# the semicolon — doing so (e.g. \;) would corrupt the value and +# prevent cloud-init from parsing the NoCloud seed URL. +unlike($src, qr/kcmd.*=~.*s\/;/, 'BIOS path does not escape semicolons'); +unlike($src, qr/ucmd.*=~.*s\/;/, 'UEFI path does not escape semicolons'); + +# The kcmdline is passed directly to imgargs without modification +like($src, qr/imgargs kernel.*\$kern->\{kcmdline\}/, 'BIOS kcmdline passed directly to imgargs'); + +# UEFI nodes must not keep a stale install script when the node moves +# back to boot/standby; otherwise they PXE back into the installer. +like($src, qr/sub _write_uefi_exit_script\b/, 'UEFI local boot helper exists'); +like($src, qr/_write_uefi_exit_script\(\$bootloader_root, \$node, \$cref->\{currstate\}\);/, 'boot/local states rewrite UEFI xNBA script'); +like($src, qr/print \$ucfg "exit\\n";/, 'UEFI local boot script exits iPXE to firmware'); + +done_testing();