2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-11 20:16:25 +00:00

Merge pull request #7829 from VersatusHPC/feat/subiquity-pkglist

feat(ubuntu): install the osimage pkglist during the Subiquity autoinstall
This commit is contained in:
Daniel Hilst
2026-09-11 12:53:13 -03:00
committed by GitHub
14 changed files with 785 additions and 17 deletions
@@ -6,6 +6,8 @@ The name of the packages that will be installed on the node are stored in the pa
* The package list file contains the names of the packages that comes from the os distro. They are stored in .pkglist file.
* The other package list file contains the names of the packages that do NOT come from the os distro. They are stored in .otherpkgs.pkglist file.
On Ubuntu releases that install with Subiquity, the packages in the .pkglist file are installed during the autoinstall from the configured apt mirror and the pkgdir mirrors, without recommended packages as ``ospkgs`` installs them, and ``ospkgs`` applies the whole list again after the first boot. A version pin, a target release or an architecture qualifier in the list, a list that carries a ``#ENV:`` setting, and the list of an osimage with ``environvar`` are installed by ``ospkgs`` only. The apt sources the installer uses for the pkgdir mirrors and the otherpkgs repository do not remain on the node: ``ospkgs`` and ``otherpkgs`` write their own after the first boot, as before. ``ospkgs`` writes http mirrors only, so an https mirror or a local directory in pkgdir serves the autoinstall and is not an apt source after the first boot.
The path to the package lists will be read from the osimage definition. Which osimage a node is using is specified by the provmethod attribute. To display this value for a node: ::
lsdef node1 -i provmethod
+62
View File
@@ -1889,6 +1889,68 @@ sub get_pkglist_tex
#----------------------------------------------------------------------------
=head3 get_pkglist_records
The records of one or more pkglist files, one per line kept whole and
includes followed. get_pkglist_tex joins records with commas, so it
cannot separate a record that itself contains a comma.
Arguments: comma-separated pkglist file names
Returns: list of records
=cut
#-----------------------------------------------------------------------------
sub get_pkglist_records
{
my $allfiles_pkglist = shift;
if ($allfiles_pkglist =~ "xCAT::") {
$allfiles_pkglist = shift;
}
my @records;
foreach my $pkglist (split(/,/, $allfiles_pkglist // ''))
{
next if $pkglist eq '';
push(@records, pkglist_file_records($pkglist, dirname($pkglist), 0));
}
return @records;
}
# pkglist_file_records: the records of one pkglist file, read as get_pkglist_tex reads them, with an
# #INCLUDE: record replaced by the records of the named file.
# A nested include resolves against the directory of the listed pkglist, as get_pkglist_tex resolves it.
sub pkglist_file_records
{
my ($file, $idir, $depth) = @_;
my @records;
open(my $fh, '<', $file) or return ("#INCLUDEBAD:cannot open pkglist file $file#");
while (my $line = <$fh>)
{
chomp($line);
$line =~ s/\s+$//;
$line =~ s/^\s*//;
next if $line eq '';
next
if ($line =~ /^#/
&& $line !~ /^#INCLUDE:[^#^\n]+#/
&& $line !~ /^#NEW_INSTALL_LIST#/
&& $line !~ /^#ENV:[^#^\n]+#/);
if ($line =~ /^#INCLUDE:([^#^\n]+)#(.*)$/ && $depth < 20)
{
my ($name, $note) = ($1, $2);
my $include = xCAT::Utils->varsubinline($name, \%ENV);
$include = "$idir/$include" unless $include =~ m{^/};
my @included = pkglist_file_records($include, $idir, $depth + 1);
$included[-1] .= $note if @included && $note ne '';
push(@records, @included);
next;
}
push(@records, $line);
}
close($fh);
return @records;
}
#----------------------------------------------------------------------------
=head3 includefile
handles #INCLUDE# in otherpkg.pkglist file
+226 -13
View File
@@ -173,6 +173,11 @@ sub subvars {
$inc =~ s/#INCLUDE_DEFAULT_RMPKGLIST_S#/#INCLUDE_RMPKGLIST:$pkglistfile#/g;
}
# osimage environvar reaches apt-get through ospkgs alone, so such an image installs its list there,
# and its pkgdir mirrors, which may need those variables too, stay out of the installer's sources
my $environvar_set = ( $namedargs{environvar} // '' ) =~ /\S/;
my $installer_pkgdirs = $environvar_set ? undef : $namedargs{pkgdirs};
my @autoinstall;
if (("ubuntu" eq $platform) || ("debian" eq $platform)) {
# since debian/ubuntu uses a preseed file instead of a kickstart file, pkglist
@@ -186,6 +191,7 @@ sub subvars {
if ($allpkglist =~ /#INCLUDEBAD:(.*)#/) {
return "$1";
}
@autoinstall = ubuntu_autoinstall_packages( xCAT::Postage->get_pkglist_records($pkglistfile) ) unless $environvar_set;
$allpkglist =~ s/,/ /g;
$inc =~ s/#INCLUDE_DEFAULT_PKGLIST_PRESEED#/$allpkglist/g;
@@ -364,7 +370,7 @@ sub subvars {
$inc =~ s/#INSTALL_SOURCES_IN_PRE#/$source_in_pre/g;
if (("ubuntu" eq $platform) || ("debian" eq $platform)) {
$inc =~ s/#INCLUDE_OSIMAGE_PKGDIR#/$pkgdirs[-1]/;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch})/eg;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $installer_pkgdirs)/eg;
}
$inc =~ s/#WRITEREPO#/$writerepo/g;
}
@@ -377,7 +383,9 @@ sub subvars {
$inc =~ s/#INCLUDE_NOP:([^#^\n]+)#/includefile($1,1,0)/eg;
$inc =~ s/#XCATVAR:([^#]+)#/envvar($1)/eg;
$inc =~ s/#ENV:([^#]+)#/envvar($1)/eg;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch})/eg;
$inc =~ s/#UBUNTU_SUBIQUITY_APT_CONFIG#/ubuntu_subiquity_apt_config($media_dir, $namedargs{osarch}, $installer_pkgdirs)/eg;
# in the include pass, so a template that includes the stock Subiquity one gets its list as well
$inc =~ s/^((?:[ \t]*- [^\n]*\n)*)([ \t]*)- #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#[ \t]*\n/$1 . ubuntu_autoinstall_items($2, $1, \@autoinstall)/meg;
$inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg;
$inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg;
$inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg;
@@ -1764,6 +1772,59 @@ sub subiquity_install_mac {
return $macaddress;
}
# ubuntu_autoinstall_packages: the packages of the pkglist records (whole lines, as
# get_pkglist_records returns them) that a Subiquity autoinstall can install through its packages
# list. A record holds one or more space-separated packages, as the preseed path reads it, each a
# plain name or a task. A version pin or a target release stays with ospkgs, because the installer
# runs apt-get without --allow-downgrades and a pin can require one, and so does a name with an
# architecture qualifier, because a foreign architecture is enabled by a postscript that runs later. A preseed directive, told by its question type, a record that begins with a removal
# or a group, which ospkgs removes or installs whole, a removal written with a trailing hyphen as
# apt-get reads it, or a marker has
# no autoinstall form, a comment ends the packages of a record, and a record with a token that is
# none of these is left out whole. A list
# that carries a #ENV: setting, which only ospkgs can pass to apt-get, or an unreadable include is
# left to ospkgs whole; ospkgs still applies the whole list after the install.
my %PRESEED_TYPE = map { $_ => 1 } qw(string boolean select multiselect note password text seen title error);
sub ubuntu_autoinstall_packages
{
my @records = grep { defined } @_;
return () if grep { /#(?:ENV:|INCLUDEBAD:)/ } @records;
my (@packages, %seen);
RECORD: foreach my $record (@records) {
my @tokens = grep { length } split( /\s+/, $record );
next unless @tokens;
next if @tokens >= 3 && $PRESEED_TYPE{ $tokens[2] };
next if $tokens[0] =~ /^[-@]/; # ospkgs removes or installs the whole record
my @found;
foreach my $token (@tokens) {
last if $token =~ /^#/;
next if $token =~ /^[-@]/ || $token =~ /-$/;
next RECORD unless $token =~ m{^[a-z0-9][a-z0-9+.-]*(?::[a-z0-9-]+)?(?:[=/][^\s/=]+|\^)?$};
next if $token =~ m{[:=/]};
push @found, $token;
}
push @packages, grep { !$seen{$_}++ } @found;
}
return @packages;
}
# ubuntu_autoinstall_items: the list items for the pkglist packages at the token's indentation,
# leaving out packages the items above the token already name, each quoted so a name such as null
# or true stays a string. The time daemons exclude each
# other, so when the template names one, the pkglist's stay with ospkgs, as they did before.
my %UBUNTU_TIME_DAEMON = map { $_ => 1 } qw(chrony ntp ntpsec ntpdate ntpsec-ntpdate openntpd systemd-timesyncd);
sub ubuntu_autoinstall_items
{
my ($indent, $listed, $packages) = @_;
my %named = map { $_ => 1 } ( $listed =~ /^[ \t]*- (\S+)[ \t]*$/mg );
my $fixed_time_daemon = grep { $UBUNTU_TIME_DAEMON{$_} } keys %named;
my @items = grep { !$named{$_} } @$packages;
@items = grep { !$UBUNTU_TIME_DAEMON{ (split /[:=\/^]/, $_)[0] } } @items if $fixed_time_daemon;
return join( '', map { "$indent- \"$_\"\n" } @items );
}
sub ubuntu_subiquity_apt_mirror
{
my ($osarch) = @_;
@@ -1786,13 +1847,28 @@ sub ubuntu_subiquity_apt_mirror
return ($ent && defined($ent->{value}) && length($ent->{value})) ? $ent->{value} : $default;
}
# The key curtin names in the Deb822 source it writes for the primary apt mirror on 24.04 and later.
my $UBUNTU_ARCHIVE_KEYRING = '/usr/share/keyrings/ubuntu-archive-keyring.gpg';
sub ubuntu_subiquity_apt_config
{
my ($media_dir, $osarch) = @_;
my ($media_dir, $osarch, $pkgdirs) = @_;
my $use_deb822 = ubuntu_subiquity_uses_deb822_sources($media_dir);
my @otherpkg_sources = ubuntu_subiquity_otherpkg_sources();
my $online_mirror = ubuntu_subiquity_apt_mirror($osarch);
my $mirror_key = $use_deb822 ? $UBUNTU_ARCHIVE_KEYRING : '';
my @otherpkg_sources = map { ubuntu_subiquity_otherpkg_source_spec( $_, $mirror_key, $online_mirror ) } ubuntu_subiquity_otherpkg_sources();
my @pkgdir_sources = ubuntu_subiquity_pkgdir_source_specs( $pkgdirs, $mirror_key, $online_mirror );
# apt rejects two sources for one repository whose options differ, so a pkgdir entry that repeats an
# otherpkgs repository adds its components to that source instead
my %otherpkg_by_key = map { ( my $uri = $_->{uri} ) =~ s{/+$}{}; ( "$uri $_->{suites}" => $_ ) } @otherpkg_sources;
@pkgdir_sources = grep {
( my $uri = $_->{uri} ) =~ s{/+$}{};
my $other = $otherpkg_by_key{"$uri $_->{suites}"};
ubuntu_subiquity_add_components( $other, $_->{components} ) if $other;
!$other;
} @pkgdir_sources;
my $online_mirror = ubuntu_subiquity_apt_mirror($osarch);
if ($online_mirror) {
# Online install: use the configured archive as the primary apt mirror so
# Subiquity/curtin can fetch whatever the minimal media lacks. No
@@ -1801,6 +1877,7 @@ sub ubuntu_subiquity_apt_config
' apt:',
' preserve_sources_list: false',
' geoip: false',
q( conf: 'APT::Install-Recommends "false";'),
' mirror-selection:',
' primary:',
" - uri: $online_mirror",
@@ -1818,12 +1895,16 @@ sub ubuntu_subiquity_apt_config
push @lines, ' xcat-ubuntu-updates.list:';
push @lines, qq( source: "deb $online_mirror \$RELEASE-updates main restricted universe multiverse");
}
if (@otherpkg_sources) {
if (@otherpkg_sources || @pkgdir_sources) {
push @lines, ' sources:' unless $need_sources_block;
my $index = 0;
foreach my $source (@otherpkg_sources) {
push @lines, " xcat-otherpkgs-$index.list:";
push @lines, qq( source: "deb [trusted=yes] $source ./");
push @lines, ubuntu_subiquity_source_lines( "xcat-otherpkgs-$index", $source, $use_deb822 );
$index++;
}
$index = 0;
foreach my $source (@pkgdir_sources) {
push @lines, ubuntu_subiquity_source_lines( "xcat-pkgdir-$index", $source, $use_deb822 );
$index++;
}
}
@@ -1835,6 +1916,7 @@ sub ubuntu_subiquity_apt_config
' preserve_sources_list: false',
' fallback: offline-install',
' geoip: false',
q( conf: 'APT::Install-Recommends "false";'),
' disable_suites:',
' - updates',
' - backports',
@@ -1864,22 +1946,36 @@ sub ubuntu_subiquity_apt_config
foreach my $source (@otherpkg_sources) {
push @lines, '';
push @lines, ' Types: deb';
push @lines, " URIs: $source";
push @lines, ' Suites: ./';
push @lines, ' Components:';
push @lines, " URIs: $source->{uri}";
push @lines, " Suites: $source->{suites}";
push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' );
push @lines, ' Trusted: yes';
}
foreach my $source (@pkgdir_sources) {
push @lines, '';
push @lines, ' Types: deb';
push @lines, " URIs: $source->{uri}";
push @lines, " Suites: $source->{suites}";
push @lines, ' Components:' . ( length $source->{components} ? " $source->{components}" : '' );
push @lines, ' Trusted: yes' if $source->{trusted};
}
} else {
push @lines, ' mirror-selection:';
push @lines, ' primary:';
push @lines, ' - uri: file:/cdrom';
if (@otherpkg_sources) {
if (@otherpkg_sources || @pkgdir_sources) {
push @lines, ' sources:';
my $index = 0;
foreach my $source (@otherpkg_sources) {
push @lines, " xcat-otherpkgs-$index.list:";
push @lines, qq( source: "deb [trusted=yes] $source ./");
push @lines, qq( source: "$source->{line}");
$index++;
}
$index = 0;
foreach my $source (@pkgdir_sources) {
push @lines, " xcat-pkgdir-$index.list:";
push @lines, qq( source: "$source->{line}");
$index++;
}
}
@@ -1888,6 +1984,56 @@ sub ubuntu_subiquity_apt_config
return join( "\n", @lines );
}
# ubuntu_subiquity_pkgdir_source_specs: the apt sources of the entries after the install media in
# an osimage pkgdir value, which mkinstall hands over as pkgdirs and ospkgs receives as OSPKGDIR.
# An entry written as "URL suite components" is an apt source line, as ospkgs writes it, and a
# suite that is an exact path needs no component. A local directory that is a flat repository is
# served by the management node and trusted, as an otherpkgdir is. Anything else is no apt source
# for ospkgs either and is left out. An entry that names an Ubuntu archive mirror the installer
# already has a source for, the configured one or a default one, carries that source's signing
# key, the archive keyring on the Deb822 releases and none before them: apt rejects a second source
# for the same suite whose signing key differs.
sub ubuntu_subiquity_pkgdir_source_specs
{
my ( $pkgdirval, $mirror_key, @mirrors ) = @_;
$mirror_key //= '';
my %signed_uri = ubuntu_subiquity_signed_mirror_uris(@mirrors);
my @specs;
foreach my $entry ( split( /,/, $pkgdirval // '' ) ) {
$entry =~ s/^\s+|\s+$//g;
next if $entry eq '';
if ( $entry =~ m{^https?://} ) {
my ( $uri, $suite, @components ) = split( /\s+/, $entry );
next unless defined $suite && ( @components || $suite =~ m{/$} );
( my $bare = $uri ) =~ s{/+$}{};
my %spec = ( uri => $uri, suites => $suite, components => join( ' ', @components ), trusted => 0, signed_by => $signed_uri{$bare} ? $mirror_key : '' );
$spec{line} = ubuntu_subiquity_source_line( \%spec );
push @specs, \%spec;
}
elsif ( $entry !~ m{^[a-z]+://} && ubuntu_subiquity_local_apt_repo($entry) ) {
my $uri = ubuntu_subiquity_pkgdir_uri($entry);
my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' );
$spec{line} = ubuntu_subiquity_source_line( \%spec );
push @specs, \%spec;
}
}
# a directory and its own URL are one repository: one source, with the trust and components of both
my ( %kept, @unique );
foreach my $spec (@specs) {
( my $uri = $spec->{uri} ) =~ s{/+$}{};
if ( my $first = $kept{"$uri $spec->{suites}"} ) {
$first->{trusted} ||= $spec->{trusted};
$first->{signed_by} ||= $spec->{signed_by};
ubuntu_subiquity_add_components( $first, $spec->{components} );
next;
}
push @unique, $kept{"$uri $spec->{suites}"} = $spec;
}
return @unique;
}
sub ubuntu_subiquity_otherpkg_sources
{
my $nodetype_tab = xCAT::Table->new('nodetype');
@@ -1915,6 +2061,73 @@ sub ubuntu_subiquity_otherpkg_sources
return @sources;
}
# ubuntu_subiquity_add_components: the components of a repeated repository join the source kept for it.
sub ubuntu_subiquity_add_components
{
my ( $spec, $components ) = @_;
my %have = map { $_ => 1 } split( ' ', $spec->{components} );
$spec->{components} = join( ' ', split( ' ', $spec->{components} ), grep { !$have{$_}++ } split( ' ', $components // '' ) );
$spec->{line} = ubuntu_subiquity_source_line($spec);
return;
}
# ubuntu_subiquity_signed_mirror_uris: the apt mirror the installer already has a source for, the
# configured one or the architecture default, without a trailing slash.
sub ubuntu_subiquity_signed_mirror_uris
{
my (@mirrors) = @_;
return map { ( my $uri = $_ ) =~ s{/+$}{}; ( $uri => 1 ) } grep { defined && length } @mirrors;
}
# ubuntu_subiquity_source_line: the one-line form of a source, with the option its Deb822 form carries.
sub ubuntu_subiquity_source_line
{
my ($spec) = @_;
my @option = $spec->{trusted} ? ('[trusted=yes]') : $spec->{signed_by} ? ("[signed-by=$spec->{signed_by}]") : ();
return join( ' ', 'deb', @option, $spec->{uri}, $spec->{suites}, grep { length } $spec->{components} );
}
# ubuntu_subiquity_otherpkg_source_spec: the apt source of one otherpkgdir entry the installer gets.
# A bare URL or a local repository is a flat trusted repository, as otherpkgs treats it. An entry
# written as URL, suite and components is that source, trusted as well, unless the URL is an Ubuntu
# archive mirror the installer already has a source for: that one gets the same signing key and no
# trust, since apt rejects a second source for one suite whose options differ.
sub ubuntu_subiquity_otherpkg_source_spec
{
my ( $entry, $mirror_key, @mirrors ) = @_;
my ( $uri, $suite, @components ) = split( /\s+/, $entry );
my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' );
if ( defined $suite && length $suite ) {
my %signed = ubuntu_subiquity_signed_mirror_uris(@mirrors);
( my $bare = $uri ) =~ s{/+$}{};
@spec{qw(suites components)} = ( $suite, join( ' ', @components ) );
@spec{qw(trusted signed_by)} = ( 0, $mirror_key // '' ) if $signed{$bare};
}
$spec{line} = ubuntu_subiquity_source_line( \%spec );
return \%spec;
}
# ubuntu_subiquity_source_lines: one entry of the autoinstall sources mapping, a one-line source
# before Deb822 and a Deb822 stanza from 24.04 on: curtin converts a one-line source to Deb822
# there and keeps only its type, URI, suite and components, so a trusted repository would come out
# unsigned and be rejected.
sub ubuntu_subiquity_source_lines
{
my ( $name, $source, $use_deb822 ) = @_;
return ( " $name.list:", qq( source: "$source->{line}") ) unless $use_deb822;
my @lines = (
" $name.sources:",
' source: |',
' Types: deb',
" URIs: $source->{uri}",
" Suites: $source->{suites}",
' Components:' . ( length $source->{components} ? " $source->{components}" : '' ),
);
push @lines, " Signed-By: $source->{signed_by}" if $source->{signed_by};
push @lines, ' Trusted: yes' if $source->{trusted};
return @lines;
}
sub ubuntu_subiquity_uses_deb822_sources
{
my ($media_dir) = @_;
+7 -2
View File
@@ -895,6 +895,7 @@ sub mkinstall {
my $partitionfile;
my $pkgdir;
my $pkgdirval;
my $environvar;
my @mirrors;
my $pkglistfile;
my $imagename; # set it if running of 'nodeset osimage=xxx'
@@ -917,12 +918,13 @@ sub mkinstall {
if (!$osimagetab) {
$osimagetab = xCAT::Table->new('osimage', -create => 1);
}
(my $ref) = $osimagetab->getAttribs({ imagename => $imagename }, 'osvers', 'osarch', 'profile', 'provmethod');
(my $ref) = $osimagetab->getAttribs({ imagename => $imagename }, 'osvers', 'osarch', 'profile', 'provmethod', 'environvar');
if ($ref) {
$img_hash{$imagename}->{osver} = $ref->{'osvers'};
$img_hash{$imagename}->{osarch} = $ref->{'osarch'};
$img_hash{$imagename}->{profile} = $ref->{'profile'};
$img_hash{$imagename}->{provmethod} = $ref->{'provmethod'};
$img_hash{$imagename}->{environvar} = $ref->{'environvar'};
if (!$linuximagetab) {
$linuximagetab = xCAT::Table->new('linuximage', -create => 1);
}
@@ -995,6 +997,7 @@ sub mkinstall {
$tmplfile = $ph->{template};
$pkgdirval = $ph->{pkgdir};
$environvar = $ph->{environvar};
my @pkgdirlist = split(/,/, $pkgdirval);
foreach (@pkgdirlist) {
if ($_ =~ /^http|ssh/) {
@@ -1123,7 +1126,9 @@ sub mkinstall {
$platform,
$partitionfile,
\%tmpl_hash,
osarch => $arch
osarch => $arch,
pkgdirs => $pkgdirval,
environvar => $environvar
);
}
@@ -43,6 +43,7 @@ autoinstall:
- bind9-dnsutils
- chrony
- gpg
- #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#
early-commands:
- |
exec >/tmp/pre-install.log 2>&1
@@ -111,6 +112,11 @@ autoinstall:
cp ./#HOSTNAME#.post /target/root/post.script;
curtin in-target --target /target /root/post.script;
} >>/target/var/log/xcat/xcat.log 2>&1'
# The installer's sources for the otherpkgs repository and the pkgdir mirrors, and the apt
# configuration that kept recommended packages out, served the install; ospkgs and otherpkgs
# write their own after the first boot. A separate item, so the status of the post script above
# still decides whether the install goes on.
- rm -f /target/etc/apt/sources.list.d/xcat-otherpkgs-*.list /target/etc/apt/sources.list.d/xcat-otherpkgs-*.sources /target/etc/apt/sources.list.d/xcat-pkgdir-*.list /target/etc/apt/sources.list.d/xcat-pkgdir-*.sources /target/etc/apt/apt.conf.d/94curtin-config
# Flip the node to local-disk boot, or it PXE-loops back into the installer on reboot.
# xcatd's install monitor greets with "ready", then answers "next" with "done" and runs
# "nodeset <node> next". Require both tokens: another service on that port is not a flipped
@@ -0,0 +1,5 @@
openssh-server
chrony
gawk
nfs-common
snmpd
@@ -0,0 +1,5 @@
openssh-server
chrony
gawk
nfs-common
snmpd
@@ -0,0 +1,5 @@
openssh-server
chrony
gawk
nfs-common
snmpd
+8 -1
View File
@@ -39,13 +39,20 @@ sub resolved {
}
# The shared lists keep ntp for the releases that still carry it. aarch64 has no list of its own.
foreach my $case ( [ $install, 'compute' ], [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) {
# The install compute profile has a 24.04 list of its own, with chrony: the Subiquity template
# installs chrony and ntp cannot join it in one apt transaction (ubuntu_subiquity_pkglists.t).
foreach my $case ( [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'compute' ] ) {
my ( $dir, $profile ) = @$case;
my ( $file, $p ) = resolved( $dir, $profile, 'ubuntu24.04.4', 'aarch64' );
is( $file, "$profile.pkglist", "$profile on 24.04 without a list of its own resolves to the shared list" );
ok( $p->{ntp}, "... which keeps ntp" );
ok( !$p->{$_}, "... and no longer names $_" ) for qw(libodbc1 qemu-kvm libvirt-bin);
}
{
my ( $file, $p ) = resolved( $install, 'compute', 'ubuntu24.04.4', 'aarch64' );
is( $file, 'compute.ubuntu24.04.pkglist', 'the install compute profile on 24.04 resolves its own list' );
ok( $p->{chrony} && !$p->{ntp}, '... which names chrony and not ntp' );
}
ok( packages_in("$install/service.pkglist")->{unixodbc}, 'the shared service list names unixodbc' );
ok( packages_in("$install/service.pkglist")->{'libdbd-pg-perl'}, '... and the PostgreSQL driver beside the MySQL one' );
+22 -1
View File
@@ -21,11 +21,32 @@ sub apt_config_for {
no warnings 'redefine';
local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { $opt{mirror} };
local *xCAT::Template::ubuntu_subiquity_uses_deb822_sources = sub { $opt{deb822} };
local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () };
local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { @{ $opt{others} || [] } };
local *xCAT::Template::ubuntu_subiquity_uses_generated_cdrom_source = sub { 0 };
return xCAT::Template::ubuntu_subiquity_apt_config('/some/media/dir');
}
# --- the otherpkgs repository: a one-line source before Deb822, a Deb822 stanza from 24.04 on, ---
# --- since curtin drops the options of a one-line source when it converts it there ---
my $others = [ 'http://192.0.2.10/install/post/otherpkgs/ubuntu24.04/x86_64' ];
my $classic_others = apt_config_for( mirror => $MIRROR, deb822 => 0, others => $others );
like( $classic_others, qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://192\.0\.2\.10/install/post/otherpkgs/ubuntu24\.04/x86_64 \./"$}m,
'classic: the otherpkgs repository is a one-line trusted source' );
my $deb822_others = apt_config_for( mirror => $MIRROR, deb822 => 1, others => $others );
like( $deb822_others,
qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://192\.0\.2\.10/install/post/otherpkgs/ubuntu24\.04/x86_64\n Suites: \./\n Components:\n Trusted: yes(?:\n|\z)}m,
'Deb822: the otherpkgs repository is a Deb822 stanza carrying Trusted: yes' );
unlike( $deb822_others, qr/xcat-otherpkgs-0\.list|trusted=yes/, 'Deb822: and no one-line form remains' );
# an otherpkgdir written as URL, suite and components is that source, trusted, not a flat repository at a URL with spaces
my $mirror_others = [ 'http://mirror.example/ubuntu noble main universe' ];
like( apt_config_for( mirror => $MIRROR, deb822 => 0, others => $mirror_others ),
qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://mirror\.example/ubuntu noble main universe"$}m,
'classic: an otherpkgdir mirror entry keeps its suite and components' );
like( apt_config_for( mirror => $MIRROR, deb822 => 1, others => $mirror_others ),
qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main universe\n Trusted: yes(?:\n|\z)}m,
'Deb822: and becomes a stanza with them as fields, so apt reads one URI' );
# --- online, classic sources (20.04 / 22.04): the archive must be added via sources: ---
my $classic = apt_config_for( mirror => $MIRROR, deb822 => 0 );
@@ -0,0 +1,138 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use File::Temp;
use FindBin;
use Test::More;
# ospkgs installs the pkglist from every pkgdir entry, the media first and the mirrors after it,
# and the Subiquity autoinstall now installs the pkglist too, so the installer must be given the
# same mirrors. The specs are derived from the pkgdir value alone, and the apt configuration is
# rendered for real with only the database readers stubbed.
my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my @incs = ( "$repo/perl-xCAT", "$repo/xCAT-server/lib/perl" );
my $devnull = File::Spec->devnull();
my $probe = join( ' ', $^X, ( map { "-I$_" } @incs ), '-e', "'require xCAT::Template; 1'", ">$devnull", "2>&1" );
plan skip_all => 'xCAT::Template cannot be loaded here' if system($probe) != 0;
require lib;
lib->import(@incs);
require xCAT::Template;
{
no warnings qw(redefine once);
*xCAT::Template::ubuntu_subiquity_pkgdir_uri = sub { return "http://192.0.2.10$_[0]" };
}
my $flat = File::Temp->newdir();
open( my $pfh, '>', "$flat/Packages" ) or die $!; close $pfh;
open( my $rfh, '>', "$flat/Release" ) or die $!; close $rfh;
my @specs = xCAT::Template::ubuntu_subiquity_pkgdir_source_specs(
"/install/ubuntu24.04.4/x86_64, http://mirror.example/ubuntu noble main universe ,http://repo.example/extra,ssh://host/path,http://mirror.example/ubuntu jammy,http://flat.example/pool ./,$flat"
);
is( scalar(@specs), 3, 'the media path, the bare URL, the ssh entry and a suite without components are no apt sources; the rest are' );
is_deeply( $specs[0], { uri => 'http://mirror.example/ubuntu', suites => 'noble', components => 'main universe', trusted => 0, signed_by => '',
line => 'deb http://mirror.example/ubuntu noble main universe' },
'an entry written as URL suite components is used as written, and is not marked trusted' );
is_deeply( $specs[1], { uri => 'http://flat.example/pool', suites => './', components => '', trusted => 0, signed_by => '',
line => 'deb http://flat.example/pool ./' },
'an exact-path suite needs no component' );
is_deeply( $specs[2], { uri => "http://192.0.2.10$flat", suites => './', components => '', trusted => 1, signed_by => '',
line => "deb [trusted=yes] http://192.0.2.10$flat ./" },
'a local flat repository is served by the management node and trusted, as an otherpkgdir is' );
is_deeply( [ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs(undef) ], [], 'no pkgdir gives no sources' );
my @alias = xCAT::Template::ubuntu_subiquity_pkgdir_source_specs("$flat,http://192.0.2.10$flat/ ./");
is( scalar(@alias), 1, 'a local repository and its own URL in pkgdir are one source' );
is( $alias[0]{trusted}, 1, '... trusted, as the directory entry is' );
my $keyring = '/usr/share/keyrings/ubuntu-archive-keyring.gpg';
is_deeply(
[ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( 'http://archive.example/ubuntu/ noble-proposed main,http://mirror.example/ubuntu noble main', $keyring, 'http://archive.example/ubuntu' ) ],
[ { uri => 'http://archive.example/ubuntu/', suites => 'noble-proposed', components => 'main', trusted => 0, signed_by => $keyring,
line => "deb [signed-by=$keyring] http://archive.example/ubuntu/ noble-proposed main" },
{ uri => 'http://mirror.example/ubuntu', suites => 'noble', components => 'main', trusted => 0, signed_by => '', line => 'deb http://mirror.example/ubuntu noble main' } ],
'an entry that names the apt mirror, trailing slash or not, carries the key given for that mirror; another mirror gets none' );
is_deeply(
[ xCAT::Template::ubuntu_subiquity_pkgdir_source_specs( 'http://archive.example/ubuntu jammy main', '', 'http://archive.example/ubuntu' ) ],
[ { uri => 'http://archive.example/ubuntu', suites => 'jammy', components => 'main', trusted => 0, signed_by => '', line => 'deb http://archive.example/ubuntu jammy main' } ],
'without a key for the mirror the entry is used as written' );
our ( $apt_mirror, @otherpkg_sources ) = ( '', () );
{
no warnings qw(redefine once);
*xCAT::Template::ubuntu_subiquity_apt_mirror = sub { return $main::apt_mirror };
*xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { return @main::otherpkg_sources };
}
# the value mkinstall hands over: the media first, then two mirrors
my $mirrors = '/install/ubuntu24.04.4/x86_64,http://mirror.example/ubuntu noble main,http://repo.example/extra';
sub apt_config_for {
my ( $media_dir, %args ) = @_;
local $apt_mirror = $args{mirror} // '';
local @otherpkg_sources = @{ $args{others} || [] };
return xCAT::Template::ubuntu_subiquity_apt_config( $media_dir, undef, $args{pkgdirs} );
}
my $online = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu', pkgdirs => $mirrors );
like( $online, qr/^ sources:$/m, 'online: the pkgdir mirrors open the sources mapping' );
like( $online, qr{^ xcat-pkgdir-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main(?:\n|\z)}m,
'... as a Deb822 stanza on a Deb822 release, with the suite and components as written' );
unlike( $online, qr{xcat-pkgdir-1|repo\.example}, '... and the bare URL is not offered as a repository' );
my $online_both = apt_config_for( 'ubuntu22.04', mirror => 'http://archive.example/ubuntu', others => ['http://mn/otherpkgs'], pkgdirs => $mirrors );
is( scalar( () = $online_both =~ /^ sources:$/mg ), 1, 'online with otherpkgs and pkgdir mirrors: one sources mapping' );
like( $online_both, qr/xcat-otherpkgs-0\.list:.*xcat-pkgdir-0\.list:/s, '... otherpkgs first, then the mirror' );
my $offline_deb822 = apt_config_for( 'ubuntu24.04', pkgdirs => $mirrors );
like( $offline_deb822, qr{^ URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: main(?:\n|\z)}m, 'offline Deb822: a stanza per mirror' );
unlike( $offline_deb822, qr{repo\.example|Trusted: yes\n(?:.*\n)* URIs: http://mirror}, '... nothing trusted and no bare URL' );
my $offline_legacy = apt_config_for( 'ubuntu22.04', pkgdirs => $mirrors );
like( $offline_legacy, qr{^ sources:\n xcat-pkgdir-0\.list:\n source: "deb http://mirror\.example/ubuntu noble main"$}m, 'offline legacy: .list files under sources' );
my $same = apt_config_for( 'ubuntu24.04', mirror => 'http://mirror.example/ubuntu', pkgdirs => 'http://mirror.example/ubuntu noble-proposed main' );
like( $same, qr{^ xcat-pkgdir-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble-proposed\n Components: main\n Signed-By: \Q$keyring\E(?:\n|\z)}m,
'another suite of the apt mirror is added with the signing key the installer gives that mirror' );
# before Deb822 the archive sources are rendered here without a key, so a repeated mirror entry must carry none either
foreach my $release ( [ 'ubuntu20.04', 'focal' ], [ 'ubuntu22.04', 'jammy' ] ) {
my ( $media, $suite ) = @$release;
my $legacy_same = apt_config_for( $media, mirror => 'http://mirror.example/ubuntu', pkgdirs => "http://mirror.example/ubuntu $suite main" );
like( $legacy_same, qr{^ xcat-pkgdir-0\.list:\n source: "deb http://mirror\.example/ubuntu \Q$suite\E main"$}m,
"$media: an entry that repeats the apt mirror carries no signing key, like the archive sources rendered next to it" );
like( $legacy_same, qr{^ xcat-ubuntu-archive\.list:\n source: "deb http://mirror\.example/ubuntu \$RELEASE main restricted universe multiverse"$}m,
"$media: which stay as they were" );
}
# a repository named by otherpkgdir and by pkgdir is offered once, trusted, on both source forms
foreach my $media ( 'ubuntu24.04', 'ubuntu22.04' ) {
my $overlap = apt_config_for( $media, mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/ubuntu'],
pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/ubuntu/ ./' );
like( $overlap, qr/xcat-otherpkgs-0\./, "$media: the repository the otherpkgdir names is offered as the trusted otherpkgs source" );
unlike( $overlap, qr/xcat-pkgdir/, "$media: and not again as a pkgdir source with other options" );
}
# an otherpkgdir entry that names the apt mirror itself gets the options of the source the installer already has for it
my $archive_others = ['http://mirror.example/ubuntu noble universe'];
like( apt_config_for( 'ubuntu22.04', mirror => 'http://mirror.example/ubuntu', others => $archive_others ),
qr{^ xcat-otherpkgs-0\.list:\n source: "deb http://mirror\.example/ubuntu noble universe"$}m,
'classic: an otherpkgdir entry for the apt mirror carries no option, like the archive sources next to it' );
like( apt_config_for( 'ubuntu24.04', mirror => 'http://mirror.example/ubuntu', others => $archive_others ),
qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://mirror\.example/ubuntu\n Suites: noble\n Components: universe\n Signed-By: \Q$keyring\E(?:\n|\z)}m,
'Deb822: and carries the archive keyring and no Trusted, as the primary source does' );
# the same repository and suite with other components: the pkgdir components join the otherpkgs source
my $union = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/team stable tools'],
pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/team/ stable compute tools' );
like( $union, qr{^ xcat-otherpkgs-0\.sources:\n source: \|\n Types: deb\n URIs: http://repo\.example/team\n Suites: stable\n Components: tools compute\n Trusted: yes(?:\n|\z)}m,
'a pkgdir entry that repeats an otherpkgs repository adds its components to that source' );
unlike( $union, qr/xcat-pkgdir/, '... and is not a second source' );
my $union_legacy = apt_config_for( 'ubuntu22.04', mirror => 'http://archive.example/ubuntu', others => ['http://repo.example/team stable tools'],
pkgdirs => '/install/ubuntu24.04.4/x86_64,http://repo.example/team stable compute' );
like( $union_legacy, qr{^ xcat-otherpkgs-0\.list:\n source: "deb \[trusted=yes\] http://repo\.example/team stable tools compute"$}m,
'in the one-line form as well' );
my $none = apt_config_for( 'ubuntu24.04', mirror => 'http://archive.example/ubuntu' );
unlike( $none, qr/sources:/, 'without otherpkgs or mirrors no sources mapping is rendered on a Deb822 release' );
done_testing();
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env perl
use strict;
use warnings;
no warnings 'once';
use FindBin;
use lib "$FindBin::Bin/../lib";
use File::Spec;
use File::Temp;
use Test::More;
use XCAT::Test::File qw(repo_path);
sub read_text { my ($path) = @_; open( my $fh, '<', $path ) or die "$path: $!"; local $/; my $text = <$fh>; close($fh); return $text; }
sub write_text { my ( $path, $text ) = @_; open( my $fh, '>', $path ) or die "$path: $!"; print {$fh} $text; close($fh); return; }
# A Subiquity autoinstall installs the packages of its user-data packages list, and until now the
# template named a fixed set, so the osimage pkglist reached the node only through ospkgs after
# the first boot. The template can now carry #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# on a list line,
# and Template.pm renders one list item per pkglist package in its place.
my $module = repo_path('xCAT-server/lib/perl/xCAT/Template.pm');
plan skip_all => 'Template.pm not found' unless -r $module;
my @incs = ( repo_path('perl-xCAT'), repo_path('xCAT-server/lib/perl') );
# Every xCAT module prepends $XCATROOT/lib/perl as it compiles, so on a host with xCAT installed
# the modules Template.pm loads afterwards would come from /opt/xcat. Point it at the checkout.
my $xcatroot = File::Temp->newdir();
mkdir "$xcatroot/lib" or die "$xcatroot/lib: $!";
symlink( repo_path('xCAT-server/lib/perl'), "$xcatroot/lib/perl" ) or die "symlink: $!";
$ENV{XCATROOT} = "$xcatroot";
my $devnull = File::Spec->devnull();
my $probe = join( ' ', $^X, ( map { "-I$_" } @incs ), '-e', "'require xCAT::Template; 1'", ">$devnull", "2>&1" );
plan skip_all => 'xCAT::Template cannot be loaded here' if system($probe) != 0;
require lib;
lib->import(@incs);
require xCAT::Template;
require xCAT::Postage; # the pkglist reader Template.pm calls, loaded by the plugin in production
# ---- the record filter: only what apt can be asked for in an autoinstall packages list --------
my @packages = xCAT::Template::ubuntu_autoinstall_packages(
'openssh-server', ' gawk', 'ntp', '-snmpd', '@core', '#NEW_INSTALL_LIST#', 'd-i pkgsel/include string foo',
'd-i tasksel/first multiselect standard,not-a-real-package', 'nfs-common=1:2.6.4-3ubuntu5', 'gawk', 'Bad_Name', '',
'libc6', 'libc6:i386', 'curl/noble', 'dns-server^', 'a/b/c', 'vim rsync -busybox-static gpg', '@Group With Space', 'wget Bad_Name', 'bc # a calculator',
'wget-', 'libc6:i386-', 'tree+', '-snmpd apache2', '@core bc'
);
is_deeply( \@packages, [qw(openssh-server gawk ntp libc6 dns-server^ vim rsync gpg bc tree+)],
'names and tasks are kept once each, a space-separated line gives each package, a comment ends it; pins, target releases, architecture qualifiers, removals in either hyphen form, a record that begins with a removal or a group, markers, directives and unknown syntax are not' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'msodbcsql18', '#ENV:ACCEPT_EULA=Y#', 'gawk' ) ], [],
'a list with an apt environment setting stays with ospkgs whole' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'gawk', 'msodbcsql18 #ENV:ACCEPT_EULA=Y#' ) ], [],
'so does a list with the setting after a package on the same line, where get_envlist reads it too' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages( 'gawk', '#INCLUDEBAD:cannot open pkglist file /absent.pkglist#' ) ], [],
'a list with an unreadable include stays with ospkgs whole' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages() ], [], 'no records give no packages' );
# ---- the record reader: lines kept whole, includes followed, the comma text unchanged ---------
{
my $d = File::Temp->newdir();
write_text( "$d/common.pkglist", "# shared\nnfs-common\n\@Group With Space\n" );
write_text( "$d/compute.pkglist", "openssh-server\n # a comment\nd-i tasksel/first multiselect standard,not-a-real-package\n#INCLUDE:$d/common.pkglist#\n#NEW_INSTALL_LIST#\nchrony\n" );
my @records = xCAT::Postage->get_pkglist_records("$d/compute.pkglist");
is_deeply( \@records,
[ 'openssh-server', 'd-i tasksel/first multiselect standard,not-a-real-package', 'nfs-common', '@Group With Space', '#NEW_INSTALL_LIST#', 'chrony' ],
'records are whole lines, comments dropped, the include expanded in place' );
is( xCAT::Postage->get_pkglist_tex("$d/compute.pkglist"),
'openssh-server,d-i tasksel/first multiselect standard,not-a-real-package,nfs-common,@Group With Space,#NEW_INSTALL_LIST#,chrony',
'the comma text ospkgs receives is unchanged, and cannot tell the directive comma apart' );
my @missing = xCAT::Postage->get_pkglist_records("$d/absent.pkglist");
like( $missing[0], qr/^#INCLUDEBAD:/, 'an unreadable file yields the INCLUDEBAD marker record' );
# top.pkglist includes sub/common.pkglist, which includes leaf.pkglist: the leaf next to top.pkglist is the one meant
mkdir "$d/sub" or die "$d/sub: $!";
write_text( "$d/top.pkglist", "#INCLUDE:sub/common.pkglist#\n" );
write_text( "$d/sub/common.pkglist", "#INCLUDE:leaf.pkglist#\n" );
write_text( "$d/leaf.pkglist", "nfs-common\n" );
write_text( "$d/sub/leaf.pkglist", "snmpd\n" );
is_deeply( [ xCAT::Postage->get_pkglist_records("$d/top.pkglist") ], ['nfs-common'],
'a nested include resolves against the directory of the listed pkglist' );
is_deeply( [ xCAT::Postage->get_pkglist_records("$d/top.pkglist") ], [ split /,/, xCAT::Postage->get_pkglist_tex("$d/top.pkglist") ],
'and reads the same files get_pkglist_tex reads' );
write_text( "$d/note.pkglist", "#INCLUDE:leaf.pkglist# # the shared leaf\nbc # a calculator\n" );
my @noted = xCAT::Postage->get_pkglist_records("$d/note.pkglist");
is_deeply( \@noted, [ split /,/, xCAT::Postage->get_pkglist_tex("$d/note.pkglist") ],
'an include followed by a note is expanded, the note staying on the last record as get_pkglist_tex leaves it' );
is_deeply( [ xCAT::Template::ubuntu_autoinstall_packages(@noted) ], [qw(nfs-common bc)], 'and the notes add no packages' );
}
is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n - gpg\n", [qw(gawk gpg chrony)] ),
" - \"gawk\"\n - \"chrony\"\n", 'items already listed above the token are not repeated, and every item is a quoted string' );
is( xCAT::Template::ubuntu_autoinstall_items( " ", "", [qw(null true 12)] ), " - \"null\"\n - \"true\"\n - \"12\"\n",
'names YAML would read as null, boolean or number stay strings' );
is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n - chrony\n", [qw(gawk ntp ntpdate snmpd)] ),
" - \"gawk\"\n - \"snmpd\"\n", 'a time daemon the template installs keeps the pkglist time daemons with ospkgs' );
is( xCAT::Template::ubuntu_autoinstall_items( " ", " - wget\n", [qw(gawk ntp)] ),
" - \"gawk\"\n - \"ntp\"\n", 'without a fixed time daemon the pkglist one is installed' );
# ---- the rendering: the token line becomes one item per package, at its own indentation -------
my %site;
no warnings 'redefine', 'once';
local *xCAT::TableUtils::get_site_attribute = sub {
my ( undef, $key ) = @_;
return defined $site{$key} ? ( $site{$key} ) : ();
};
local *xCAT::NetworkUtils::getipaddr = sub { return '192.0.2.10'; };
local *xCAT::Template::getPersistentKcmdline = sub { return ''; };
use warnings;
my $dir = File::Temp->newdir();
my $included = File::Spec->catfile( "$dir", 'common.pkglist' );
my $pkglist = File::Spec->catfile( "$dir", 'compute.pkglist' );
write_text( $included, "# shared\nnfs-common\nsnmpd\n" );
write_text( $pkglist, "openssh-server\n# a comment\nchrony rsync # time and files\nwget=1.21.2-2ubuntu1\n-ntp\nwget-\n\@standard\nd-i tasksel/first multiselect standard,not-a-real-package\n#INCLUDE:$included#\n" );
my $in = File::Spec->catfile( "$dir", 'in.tmpl' );
write_text( $in,
" packages:\n"
. " - openssh-server\n"
. " - wget\n"
. " - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n"
. " late-commands:\n"
. " - echo done\n" );
my $render = sub {
my ( $list, %extra ) = @_;
%site = ( installdir => '/install' );
my $out = File::Spec->catfile( "$dir", 'out.' . ( defined $list ? 'list' : 'none' ) );
xCAT::Template->subvars( $in, $out, 'testnode', $list, '/install/ubuntu24.04/x86_64', 'ubuntu', undef,
{ xcatmaster => '192.0.2.10' }, osarch => 'x86_64', %extra );
return read_text($out);
};
my $rendered = $render->($pkglist);
is( $rendered,
" packages:\n"
. " - openssh-server\n"
. " - wget\n"
. " - \"chrony\"\n"
. " - \"rsync\"\n"
. " - \"nfs-common\"\n"
. " - \"snmpd\"\n"
. " late-commands:\n"
. " - echo done\n",
'the pkglist packages, includes followed, become list items at the token indentation, without repeating the items above' );
unlike( $rendered, qr/not-a-real-package/, 'a package name inside a preseed directive with commas is not a package' );
unlike( $rendered, qr/wget=/, 'a version pin is not an item: it stays with ospkgs' );
unlike( $rendered, qr/wget-/, 'a trailing-hyphen removal is not an item either: the installer would remove the package the postscripts need' );
unlike( $rendered, qr/"(?:time|and|files)"/, 'an inline comment adds no items' );
# a site template that includes the stock one: the token arrives with the include and must be expanded too
my $wrapper = File::Spec->catfile( "$dir", 'wrapper.tmpl' );
write_text( $wrapper, "#INCLUDE:$in#\n" );
my $render_via = sub {
my ($list) = @_;
%site = ( installdir => '/install' );
my $out = File::Spec->catfile( "$dir", 'out.wrapper.' . ( defined $list ? 'list' : 'none' ) );
xCAT::Template->subvars( $wrapper, $out, 'testnode', $list, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, { xcatmaster => '192.0.2.10' }, osarch => 'x86_64' );
return read_text($out);
};
is( $render_via->($pkglist), $rendered, 'a template that includes the stock one renders the same package list' );
my $without = $render->(undef);
is( $without,
" packages:\n - openssh-server\n - wget\n late-commands:\n - echo done\n",
'an osimage without a pkglist keeps the template packages and loses only the token line' );
is( $render_via->(undef), $without, 'and through an including template the token line goes as well, leaving no empty item' );
my $env_list = File::Spec->catfile( "$dir", 'env.pkglist' );
my $env_included = File::Spec->catfile( "$dir", 'env-common.pkglist' );
write_text( $env_included, "msodbcsql18 #ENV:ACCEPT_EULA=Y#\n" );
write_text( $env_list, "gawk\n#INCLUDE:$env_included#\n" );
is( $render->($env_list), $without, 'a pkglist whose include carries an apt environment setting is left to ospkgs whole, and the token line goes' );
is( $render->( $pkglist, environvar => 'ACCEPT_EULA=Y' ), $without,
'an osimage with environvar installs its pkglist through ospkgs alone, where the variables reach apt-get, and the token line goes' );
# the pkgdir mirrors may need those variables as well, so they stay out of the installer's apt sources too
{
no warnings 'redefine', 'once';
local *xCAT::Template::ubuntu_subiquity_otherpkg_sources = sub { () };
local *xCAT::Template::ubuntu_subiquity_apt_mirror = sub { 'http://archive.example/ubuntu' };
my $apt_in = File::Spec->catfile( "$dir", 'apt.tmpl' );
write_text( $apt_in, "#UBUNTU_SUBIQUITY_APT_CONFIG#\n" );
my $apt_render = sub {
my (%extra) = @_;
%site = ( installdir => '/install' );
my $out = File::Spec->catfile( "$dir", 'out.apt' );
xCAT::Template->subvars( $apt_in, $out, 'testnode', $pkglist, '/install/ubuntu24.04/x86_64', 'ubuntu', undef, { xcatmaster => '192.0.2.10' },
osarch => 'x86_64', pkgdirs => '/install/ubuntu24.04/x86_64,http://mirror.example/ubuntu noble main', %extra );
return read_text($out);
};
like( $apt_render->(), qr{URIs: http://mirror\.example/ubuntu}, 'the pkgdir mirror joins the installer sources' );
like( $apt_render->(), qr{^ conf: 'APT::Install-Recommends "false";'$}m, 'the installer installs without recommended packages, as ospkgs does' );
unlike( $apt_render->( environvar => 'http_proxy=http://proxy.example:3128' ), qr{mirror\.example|xcat-pkgdir},
'but not for an osimage with environvar, whose mirrors may need those variables' );
}
done_testing();
@@ -0,0 +1,62 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Spec;
use FindBin;
use Test::More;
use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use xCAT::Postage;
use xCAT::SvrUtils;
# The Subiquity autoinstall installs the template's fixed packages and the osimage pkglist in one
# apt transaction, and the template names chrony. The shared compute.pkglist names ntp for the
# releases before Subiquity, and on the Subiquity releases ntp pulls ntpsec, which conflicts with
# chrony, so those releases need their own default list. The lists are resolved the way
# mkinstall resolves them and their packages are read the way ospkgs reads them.
my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) );
my $install = "$repo/xCAT-server/share/xcat/install/ubuntu";
my $template = "$install/compute.subiquity.tmpl";
plan skip_all => 'the Ubuntu install directory is not here' unless -d $install && -f $template;
sub packages_in {
my ($path) = @_;
return { map { $_ => 1 } grep { length } split /,/, xCAT::Postage::get_pkglist_tex($path) };
}
sub resolved {
my ( $os, $arch ) = @_;
return xCAT::SvrUtils->get_pkglist_file_name( $install, 'compute', $os, $arch, $os =~ /^(ubuntu\d+\.\d+)/ ? $1 : $os );
}
open( my $tfh, '<', $template ) or die "$template: $!";
my $body = do { local $/; <$tfh> };
close($tfh);
my ($block) = $body =~ /^ packages:\n((?: - .*\n)+)/m;
my %fixed = map { $_ => 1 } ( $block =~ /^ - ([^#\s]+)$/mg );
ok( $fixed{chrony}, 'the Subiquity template names chrony among its fixed packages' );
my %time_daemon = map { $_ => 1 } qw(chrony ntp ntpsec);
foreach my $case ( [ 'ubuntu20.04.6', 'x86_64' ], [ 'ubuntu20.04.6', 'ppc64le' ],
[ 'ubuntu22.04.5', 'x86_64' ], [ 'ubuntu22.04.5', 'ppc64le' ],
[ 'ubuntu24.04.4', 'x86_64' ], [ 'ubuntu24.04.4', 'ppc64le' ], [ 'ubuntu24.04.4', 'riscv64' ],
[ 'ubuntu26.04.1', 'x86_64' ], [ 'ubuntu26.04.1', 'riscv64' ] ) {
my ( $os, $arch ) = @$case;
my $file = resolved( $os, $arch );
ok( $file, "$os $arch resolves a compute pkglist" ) or next;
my $packages = packages_in($file);
ok( $packages->{chrony}, "$os $arch: the list names chrony, the daemon the template installs" );
ok( !$packages->{ntp} && !$packages->{ntpdate}, "$os $arch: ... and not ntp, which would conflict with it" );
my @daemons = grep { $time_daemon{$_} } keys %{ { %fixed, %$packages } };
is( scalar(@daemons), 1, "$os $arch: the autoinstall transaction carries exactly one time daemon" );
}
# The releases before Subiquity keep the shared list and its ntp.
my $legacy = resolved( 'ubuntu16.04.7', 'x86_64' );
is( $legacy, "$install/compute.pkglist", '16.04 still resolves the shared list' );
ok( packages_in($legacy)->{ntp}, '... which keeps ntp for the releases where chrony was not the default' );
done_testing();
@@ -16,6 +16,9 @@ like($tmpl, qr/autoinstall:/, 'template has autoinstall: key');
like($tmpl, qr/version:\s*1/, 'template has version: 1');
like($tmpl, qr/^\s*identity:/m, 'template has an identity section so subiquity does not prompt');
like($tmpl, qr/^ - #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL#\n/m, 'the packages list carries the osimage pkglist through the autoinstall token');
like($tmpl, qr/^ - openssh-server\n/m, '... and keeps openssh-server, which xCAT needs on the node');
like($tmpl, qr/^ - wget\n/m, '... and wget, which the early and late commands use');
like($tmpl, qr/kernel:/, 'template has kernel section');
like($tmpl, qr/package:\s*linux-generic/, 'template specifies linux-generic kernel');
like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template renders apt section from osimage context');
@@ -78,5 +81,39 @@ unlike($tmpl, qr/if \[ -x \/tmp\/pre\.sh \]/, 'pre.sh not checked with -x');
# Subiquity behavior can be handled without cloning this template per release.
unlike($tmpl, qr/noble-|jammy-|focal-/, 'template avoids release-specific apt suite names');
like($tmpl, qr/#UBUNTU_SUBIQUITY_APT_CONFIG#/, 'template keeps dynamic apt renderer marker');
like($tmpl, qr{2>&1'\n(?:\s*#[^\n]*\n)*\s*- rm -f (?:/target/etc/apt/sources\.list\.d/xcat-(?:otherpkgs|pkgdir)-\*\.(?:list|sources)\s+){4}/target/etc/apt/apt\.conf\.d/94curtin-config$}m,
'the installer sources for the otherpkgs repository and the pkgdir mirrors, and the apt configuration curtin wrote, are removed from the target by a late-command of their own, after the post script block');
# the post script block: its status is the post script's, so a failed post script stops the install
{
my ($block) = $tmpl =~ /\n - '(\{\n.*?\n \} >>\/target\/var\/log\/xcat\/xcat\.log 2>&1)'\n/s;
ok( defined $block, 'the post script block is found' ) or last;
$block =~ s/''/'/g;
require File::Temp;
my $root = File::Temp->newdir();
mkdir "$root/bin" or die;
for my $tool ( 'curtin', 'wget' ) {
open( my $fh, '>', "$root/bin/$tool" ) or die;
print {$fh} $tool eq 'curtin' ? "#!/bin/sh\ncase \"\$*\" in *post.script*) exit 42;; esac\nexit 0\n" : "#!/bin/sh\nfor a; do case \"\$a\" in http*) touch \"\${a##*/}\";; esac; done\nexit 0\n";
close $fh; chmod 0755, "$root/bin/$tool";
}
( my $script = $block ) =~ s{/target}{$root/target}g;
$script =~ s{/tmp/pre-install\.log}{$root/pre-install.log}g;
for my $token ( [ '#SUBIQUITYINSTALLNIC#', '' ], [ '#SUBIQUITYINSTALLMAC#', '52:54:00:00:00:01' ], [ '#HOSTNAME#', 'cn1' ], [ '#XCATVAR:XCATMASTER#', '192.0.2.10' ],
[ '#COLONHTTPPORT#', '' ], [ '#TABLEBLANKOKAY:bootparams:$NODE:kcmdline#', '' ] ) {
$script =~ s/\Q$token->[0]\E/$token->[1]/g;
}
require File::Path;
File::Path::make_path( map { "$root/target/$_" } qw(etc/default root var/log/xcat) );
open( my $hosts, '>', "$root/target/etc/hosts" ) or die; print {$hosts} "127.0.0.1 localhost\n"; close $hosts;
open( my $pre, '>', "$root/pre-install.log" ) or die; close $pre;
my $cwd = File::Spec->rel2abs('.');
chdir $root or die;
local $ENV{PATH} = "$root/bin:$ENV{PATH}";
system( 'sh', '-c', $script );
my $status = $? >> 8;
chdir $cwd or die;
is( $status, 42, 'a failing post script fails the late-command block, so Subiquity stops the install instead of switching the node to disk boot' );
}
done_testing();