diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index bcf37d647..ea45ea0e7 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -372,6 +372,8 @@ sub subvars { $inc =~ s/#XCATVAR:([^#]+)#/envvar($1)/eg; $inc =~ s/#ENV:([^#]+)#/envvar($1)/eg; $inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir)/eg; + $inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg; + $inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg; $inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg; $inc =~ s/#CRYPT:([^:]+):([^:]+):([^#]+)#/crydb($1,$2,$3)/eg; $inc =~ s/#CRYPTORLOCKED:([^:]+):([^:]+):([^#]+)#/crydb_or_locked($1,$2,$3)/eg; @@ -1654,6 +1656,71 @@ sub crydb_or_locked return '*'; } +#-------------------------------------------------------------------------------- + +=head3 subiquity_install_netcfg + + Resolve the interface the INSTALLED system must bring up, in xCAT's own order: + noderes.installnic, else noderes.primarynic, else match on mac.mac. Either attribute may name + an interface OR carry a MAC address. xCAT::NetworkUtils::gen_net_boot_params already owns that + order for the netboot kernel parameters, so it is reused here rather than re-derived -- and in + particular the install template never re-derives any part of it in shell. + + Arguments: + $installnic - noderes.installnic (may be undef or empty) + $primarynic - noderes.primarynic (may be undef or empty) + $macentry - the raw mac.mac entry (may hold |-separated, !hostname-suffixed entries) + $nodename - the node the entry is resolved for + Returns: + ($setname, $macaddress) + $setname - the name netplan must rename the matched device to, empty when the device is + matched by MAC alone (installnic unset/"mac", or a MAC address given) + $macaddress - the address netplan matches on, lower-cased + +=cut + +#-------------------------------------------------------------------------------- +sub subiquity_install_netcfg { + my ($installnic, $primarynic, $macentry, $nodename) = @_; + + my $macmac = xCAT::Utils->parseMacTabEntry(defined($macentry) ? $macentry : '', $nodename); + my $params = xCAT::NetworkUtils->gen_net_boot_params($installnic, $primarynic, $macmac); + + my $setname = defined($params->{nicname}) ? $params->{nicname} : ''; + my $macaddress = defined($params->{mac}) ? lc($params->{mac}) : ''; + return ($setname, $macaddress); +} + +#-------------------------------------------------------------------------------- + +=head3 subiquity_install_nic / subiquity_install_mac + + Render #SUBIQUITYINSTALLNIC# / #SUBIQUITYINSTALLMAC# for the node being templated. installnic + and primarynic are read blank-okay (a node that sets neither is normal, and is what + "match on mac.mac" means); mac.mac stays a required lookup, as it was when the template read it + directly -- a node with no MAC cannot be matched by netplan at all. + +=cut + +#-------------------------------------------------------------------------------- +sub subiquity_install_netcfg_for_node { + return subiquity_install_netcfg( + tabdb('noderes', '$NODE', 'installnic', 1), + tabdb('noderes', '$NODE', 'primarynic', 1), + tabdb('mac', '$NODE', 'mac'), + $node); +} + +sub subiquity_install_nic { + my ($setname) = subiquity_install_netcfg_for_node(); + return $setname; +} + +sub subiquity_install_mac { + my (undef, $macaddress) = subiquity_install_netcfg_for_node(); + return $macaddress; +} + sub ubuntu_subiquity_apt_mirror { # Apt mirror for Subiquity installs. site.ubuntu_apt_mirror overrides; otherwise default to the diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index 31a978cbc..8399cef6c 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -74,11 +74,10 @@ autoinstall: - 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}" | cut -d''|'' -f1 | cut -d''!'' -f1 | tr ''A-F'' ''a-f'')"; + installnic="#SUBIQUITYINSTALLNIC#"; + installmac="#SUBIQUITYINSTALLMAC#"; mkdir -p /target/etc/netplan; - if [ "${installnic}" = "mac" ]; then + if [ -z "${installnic}" ]; then printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " dhcp4: true" >/target/etc/netplan/00-xcat-install.yaml; else printf ''%s\n'' "network:" " version: 2" " ethernets:" " xcat-install:" " match:" " macaddress: \"${installmac}\"" " set-name: ${installnic}" " dhcp4: true" >/target/etc/netplan/00-xcat-install.yaml; diff --git a/xCAT-test/unit/ubuntu_subiquity_installnic.t b/xCAT-test/unit/ubuntu_subiquity_installnic.t new file mode 100644 index 000000000..83d5ee498 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_installnic.t @@ -0,0 +1,141 @@ +#!/usr/bin/env perl +# The interface the INSTALLED system brings up must be resolved in xCAT's own order -- +# noderes.installnic, else noderes.primarynic, else match on mac.mac -- and either attribute may +# hold an interface NAME or a MAC address. Treating an empty installnic as "mac" straight away +# skips primarynic entirely, so a node that configures only primarynic is provisioned with a +# netplan that matches on mac.mac and never renames the interface. +# +# Two halves are covered: the resolution itself (xCAT::Template, which delegates the order to +# xCAT::NetworkUtils::gen_net_boot_params rather than re-deriving it), and the netplan the +# template's late-command actually writes once rendered with the resolved values. +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; +use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; +use File::Temp qw(tempdir); +use Test::More; +use xCAT::Template; + +my $NODE = 'node01'; +my $NODE_MAC = 'AA:BB:CC:DD:EE:01'; + +# ---- resolution: installnic -> primarynic -> mac.mac, each of which may be a name or a MAC ------ +my @cases = ( + { + name => 'installnic names an interface', + installnic => 'ens4', + primarynic => 'eno1', + setname => 'ens4', + macaddress => 'aa:bb:cc:dd:ee:01', + why => 'renames the device matched by mac.mac', + }, + { + name => 'installnic holds a MAC address', + installnic => 'aa:bb:cc:dd:ee:02', + primarynic => 'eno1', + setname => '', + macaddress => 'aa:bb:cc:dd:ee:02', + why => 'matches that MAC and renames nothing', + }, + { + name => 'installnic empty, primarynic names an interface', + installnic => '', + primarynic => 'eno1', + setname => 'eno1', + macaddress => 'aa:bb:cc:dd:ee:01', + why => 'falls back to primarynic instead of skipping to mac', + }, + { + name => 'installnic empty, primarynic holds a MAC address', + installnic => '', + primarynic => 'aa:bb:cc:dd:ee:03', + setname => '', + macaddress => 'aa:bb:cc:dd:ee:03', + why => 'falls back to the primarynic MAC', + }, + { + name => 'installnic and primarynic both unset', + installnic => '', + primarynic => '', + setname => '', + macaddress => 'aa:bb:cc:dd:ee:01', + why => 'finally falls back to mac.mac', + }, + { + name => 'installnic set to the literal "mac"', + installnic => 'mac', + primarynic => 'eno1', + setname => '', + macaddress => 'aa:bb:cc:dd:ee:01', + why => 'is an explicit request to match by MAC, so primarynic is not consulted', + }, +); + +for my $case (@cases) { + my ($setname, $macaddress) = xCAT::Template::subiquity_install_netcfg( + $case->{installnic}, $case->{primarynic}, $NODE_MAC, $NODE); + is($setname, $case->{setname}, "$case->{name}: $case->{why} (set-name)"); + is($macaddress, $case->{macaddress}, "$case->{name}: $case->{why} (macaddress)"); +} + +# mac.mac carries |-separated, !hostname-suffixed entries; the netplan must match this node's. +{ + my $entry = "aa:bb:cc:dd:ee:09!other|aa:bb:cc:dd:ee:0a!$NODE"; + my (undef, $macaddress) = + xCAT::Template::subiquity_install_netcfg('', '', $entry, $NODE); + is($macaddress, 'aa:bb:cc:dd:ee:0a', + 'a multi-entry mac.mac resolves to this node\'s address, lower-cased'); +} + +# ---- rendering: run the template's own late-command and read the netplan it writes -------------- +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; +$tmpl_path = "$FindBin::Bin/../../xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl" + unless -f $tmpl_path; + +SKIP: { + skip 'compute.subiquity.tmpl not found', 4 unless -f $tmpl_path; + my $tmpl = do { local $/; open my $fh, '<', $tmpl_path or die $!; <$fh> }; + + # The netplan-writing part of late-commands, verbatim: from the resolved values down to the + # chmod. Rendering it here is what proves the shell branches on what Perl resolved. + my ($snippet) = $tmpl =~ /(installnic="#SUBIQUITYINSTALLNIC#".*?fi;)/s; + ok($snippet, 'the netplan late-command is rendered from the resolved values'); + + skip 'netplan late-command not found in the template', 3 unless $snippet; + $snippet =~ s/''/'/g; # undo the YAML single-quote escaping + + for my $case ( + { name => 'a resolved interface name', setname => 'eno1', mac => 'aa:bb:cc:dd:ee:01' }, + { name => 'no interface name', setname => '', mac => 'aa:bb:cc:dd:ee:03' }, + ) + { + my $dir = tempdir(CLEANUP => 1); + my $script = $snippet; + $script =~ s/#SUBIQUITYINSTALLNIC#/$case->{setname}/g; + $script =~ s/#SUBIQUITYINSTALLMAC#/$case->{mac}/g; + $script =~ s{/target/}{$dir/target/}g; + system('sh', '-c', "set -e\n$script") == 0 + or die "netplan late-command failed for $case->{name}\n"; + + my $netplan = do { + local $/; + open my $fh, '<', "$dir/target/etc/netplan/00-xcat-install.yaml" or die $!; + <$fh>; + }; + my @expected = ( + 'network:', ' version: 2', ' ethernets:', ' xcat-install:', + ' match:', qq( macaddress: "$case->{mac}"), + ($case->{setname} ne '' ? " set-name: $case->{setname}" : ()), + ' dhcp4: true', + ); + is($netplan, join("\n", @expected) . "\n", + "the netplan written for $case->{name} matches the resolved values"); + } +} + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_subiquity_template.t b/xCAT-test/unit/ubuntu_subiquity_template.t index 2af9692aa..140dfaf39 100644 --- a/xCAT-test/unit/ubuntu_subiquity_template.t +++ b/xCAT-test/unit/ubuntu_subiquity_template.t @@ -36,10 +36,26 @@ unlike($tmpl, qr/echo.*GRUB_CMDLINE.*\\"/, 'no escaped double quotes in echo GRU 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/installmac="\$\(printf ''%s'' "\$\{installmac\}" \| cut -d''\|'' -f1 \| cut -d''!'' -f1/, 'target netplan strips mac table suffixes before matching'); +# Regression: an UNSET noderes.installnic must not fatally break Subiquity xnba generation, and +# the interface must still be resolved in xCAT's order (installnic -> primarynic -> mac.mac). The +# template read #TABLE:noderes:$NODE:installnic#, and Template.pm's tabdb raises "Unable to find +# requested field from table " -> "Failed to generate xnba configurations" +# when the node carries no installnic, so the Ubuntu diskful install never started. The resolution +# now happens in Perl (xCAT::Template, via xCAT::NetworkUtils::gen_net_boot_params) and reaches the +# template already resolved, so no part of that order is re-derived in shell. The resolution itself +# and the netplan it produces are covered by ubuntu_subiquity_installnic.t. +unlike($tmpl, qr/noderes:\$NODE:(?:installnic|primarynic)/, + 'the template does not read installnic/primarynic itself (fatal when unset, and it would have to redo the fallback)'); +like($tmpl, qr/installnic="#SUBIQUITYINSTALLNIC#"/, + 'the target netplan uses the interface name xCAT resolved for this node'); +like($tmpl, qr/installmac="#SUBIQUITYINSTALLMAC#"/, + 'the target netplan uses the MAC address xCAT resolved for this node'); +like($tmpl, qr/if \[ -z "\$\{installnic\}" \]; then/, + 'no resolved interface name means match by MAC alone, with no set-name rename'); +unlike($tmpl, qr/tr ''A-F'' ''a-f''/, + 'the shell no longer normalizes the MAC (Template.pm resolves mac.mac entries)'); +unlike($tmpl, qr/cut -d''\|'' -f1/, + 'the shell no longer splits mac.mac entries (Template.pm resolves them for this node)'); 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');