From e36cdec3e97ff5e9cb9f3a09a173341eb5d61f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:10:49 -0300 Subject: [PATCH 01/13] fix(Template): give the installer otherpkgs sources apt can read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The online autoinstall apt configuration offered every otherpkgdir entry as a flat one-line source with trusted=yes. From 24.04 on curtin converts a one-line source to Deb822 before it writes the file and keeps only the type, URI, suite and components, so the unsigned repository reached apt without the option and apt rejected it during the install. An entry written as URL, suite and components, the form the otherpkgs documentation gives for a mirror, was written whole as the URL, which apt cannot parse either. The sources are now Deb822 stanzas on those releases, which curtin writes as they are, with Trusted: yes, and a mirror entry keeps its suite and components as fields. The releases before 24.04 keep the one-line form, with the same fields. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 54 +++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index de954065f..b4ebc6f04 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -1790,7 +1790,7 @@ sub ubuntu_subiquity_apt_config { my ($media_dir, $osarch) = @_; my $use_deb822 = ubuntu_subiquity_uses_deb822_sources($media_dir); - my @otherpkg_sources = ubuntu_subiquity_otherpkg_sources(); + my @otherpkg_sources = map { ubuntu_subiquity_otherpkg_source_spec($_) } ubuntu_subiquity_otherpkg_sources(); my $online_mirror = ubuntu_subiquity_apt_mirror($osarch); if ($online_mirror) { @@ -1822,8 +1822,7 @@ sub ubuntu_subiquity_apt_config 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++; } } @@ -1864,9 +1863,9 @@ 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'; } } else { @@ -1879,7 +1878,7 @@ sub ubuntu_subiquity_apt_config 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++; } } @@ -1915,6 +1914,47 @@ sub ubuntu_subiquity_otherpkg_sources return @sources; } +# 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 repository, and an entry written as URL, suite and +# components is that source; otherpkgs trusts both, so the installer does too. +sub ubuntu_subiquity_otherpkg_source_spec +{ + my ($entry) = @_; + my ( $uri, $suite, @components ) = split( /\s+/, $entry ); + my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' ); + @spec{qw(suites components)} = ( $suite, join( ' ', @components ) ) if defined $suite && length $suite; + $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, ' Trusted: yes' if $source->{trusted}; + return @lines; +} + sub ubuntu_subiquity_uses_deb822_sources { my ($media_dir) = @_; From f995c8dcb328e9cf64c6be16e5e35295c62d9ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:11:21 -0300 Subject: [PATCH 02/13] test(xCAT-test): pin the otherpkgs source form per release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The online configuration is rendered with an otherpkgs repository on a classic release and on a Deb822 release: the first must carry the one-line trusted source, the second a Deb822 stanza with Trusted: yes and no one-line form. Against the previous module the Deb822 case renders the one-line form. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_apt_sources.t | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t index 0c55d0e8c..d1e53b9d2 100644 --- a/xCAT-test/unit/ubuntu_subiquity_apt_sources.t +++ b/xCAT-test/unit/ubuntu_subiquity_apt_sources.t @@ -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 ); From 539486b7c828566a88d346b501713a1de17df4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:28:43 -0300 Subject: [PATCH 03/13] feat(Template): give the Subiquity installer the pkgdir mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An osimage pkgdir can name mirrors after the install media, and ospkgs installs from all of them, but the autoinstall apt configuration offered the installer only the archive mirror and the otherpkgs repositories. A package that only a pkgdir mirror carries could not be installed before the first boot. The mirrors now join the installer's apt sources, next to the otherpkgs ones, in every form the configuration takes. An entry written as URL, suite and 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. 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, because apt rejects a second source for the same suite whose signing key differs. For the same reason a repository is offered once, whether pkgdir names it twice, as a directory and as its URL, or the otherpkgdir names it too. Anything else is no apt source for ospkgs either and is left out. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 132 ++++++++++++++++++++++--- xCAT-server/lib/xcat/plugins/debian.pm | 3 +- 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index b4ebc6f04..e37652ab1 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -364,7 +364,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}, $namedargs{pkgdirs})/eg; } $inc =~ s/#WRITEREPO#/$writerepo/g; } @@ -377,7 +377,7 @@ 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}, $namedargs{pkgdirs})/eg; $inc =~ s/#SUBIQUITYINSTALLNIC#/subiquity_install_nic()/eg; $inc =~ s/#SUBIQUITYINSTALLMAC#/subiquity_install_mac()/eg; $inc =~ s/#MACHINEPASSWORD#/machinepassword()/eg; @@ -1786,13 +1786,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 = map { ubuntu_subiquity_otherpkg_source_spec($_) } 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 @@ -1818,13 +1833,18 @@ 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, 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++; + } } return join( "\n", @lines ); } @@ -1868,12 +1888,20 @@ sub ubuntu_subiquity_apt_config 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) { @@ -1881,12 +1909,68 @@ sub ubuntu_subiquity_apt_config 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++; + } } } 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'); @@ -1914,6 +1998,24 @@ 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 { @@ -1923,14 +2025,21 @@ sub ubuntu_subiquity_source_line } # 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 repository, and an entry written as URL, suite and -# components is that source; otherpkgs trusts both, so the installer does too. +# 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) = @_; + my ( $entry, $mirror_key, @mirrors ) = @_; my ( $uri, $suite, @components ) = split( /\s+/, $entry ); my %spec = ( uri => $uri, suites => './', components => '', trusted => 1, signed_by => '' ); - @spec{qw(suites components)} = ( $suite, join( ' ', @components ) ) if defined $suite && length $suite; + 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; } @@ -1951,6 +2060,7 @@ sub ubuntu_subiquity_source_lines " 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; } diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index 2a4c2fb39..7edf607bc 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -1123,7 +1123,8 @@ sub mkinstall { $platform, $partitionfile, \%tmpl_hash, - osarch => $arch + osarch => $arch, + pkgdirs => $pkgdirval ); } From c96d879367b2f4d300aa630b886aa42c688bdd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:28:43 -0300 Subject: [PATCH 04/13] test(xCAT-test): cover the pkgdir mirrors in the Subiquity apt configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The source specs are derived from a pkgdir value alone, and the apt configuration is rendered with only the database readers stubbed, online and offline, Deb822 and legacy, with and without otherpkgs repositories. Against the previous module the spec helper does not exist. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../unit/ubuntu_subiquity_pkgdir_sources.t | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t diff --git a/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t b/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t new file mode 100644 index 000000000..2dc89dfb9 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkgdir_sources.t @@ -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(); From a7812caca8fddebe343cb3a99625676c17dfb6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:51:05 -0300 Subject: [PATCH 05/13] feat(Postage): read a pkglist as whole records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_pkglist_tex joins the records of a pkglist with commas for the preseed and OSPKGS consumers, so a caller that needs the records themselves cannot recover a record that contains a comma, such as a tasksel directive. The new reader returns the records whole, comments dropped and includes followed in place. It reads each line as get_pkglist_tex does and resolves every include, nested ones too, against the directory of the listed pkglist, as get_pkglist_tex does. The comma text is unchanged. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Postage.pm | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/xCAT-server/lib/perl/xCAT/Postage.pm b/xCAT-server/lib/perl/xCAT/Postage.pm index e9d40ba8c..0eccbb26c 100644 --- a/xCAT-server/lib/perl/xCAT/Postage.pm +++ b/xCAT-server/lib/perl/xCAT/Postage.pm @@ -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 From ac50225541f64ae1f6fa17db8447872406c96db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 06/13] feat(Template): render the osimage pkglist into the autoinstall package list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Subiquity autoinstall installs what its user-data packages list names, and the templates could only name a fixed set, so the osimage pkglist reached an Ubuntu node through ospkgs after the first boot. The preseed token has no autoinstall form: the package list is YAML, one item per line. A list line that carries #INCLUDE_DEFAULT_PKGLIST_AUTOINSTALL# is now replaced by one item per pkglist package at the same indentation, with includes followed and without repeating the items the template lists above it. The line is replaced in the include pass of subvars, so a site template that includes the stock one is served too. A plain name and a task are installed this way, and a comment after them ends the record. 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 record that begins with a removal or a group is left out whole, as ospkgs removes or installs it whole, and so are a removal written with a trailing hyphen, markers and preseed directives. A list that carries a #ENV: setting or an unreadable include is left to ospkgs whole. So is the list of an osimage with environvar, which mkinstall now hands over, and such an image's pkgdir mirrors stay out of the installer's sources as well: those variables reach apt-get only through ospkgs, and a mirror may need them. An osimage without a pkglist loses only the token line. The installer's apt configuration turns recommended packages off, as ospkgs installs the list without them; curtin writes that setting into the target, where the template removes it with the installer's sources. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-server/lib/perl/xCAT/Template.pm | 67 +++++++++++++++++++++++++- xCAT-server/lib/xcat/plugins/debian.pm | 10 ++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/xCAT-server/lib/perl/xCAT/Template.pm b/xCAT-server/lib/perl/xCAT/Template.pm index e37652ab1..79b26101f 100644 --- a/xCAT-server/lib/perl/xCAT/Template.pm +++ b/xCAT-server/lib/perl/xCAT/Template.pm @@ -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}, $namedargs{pkgdirs})/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}, $namedargs{pkgdirs})/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) = @_; @@ -1816,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", @@ -1854,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', diff --git a/xCAT-server/lib/xcat/plugins/debian.pm b/xCAT-server/lib/xcat/plugins/debian.pm index 7edf607bc..e085efc28 100644 --- a/xCAT-server/lib/xcat/plugins/debian.pm +++ b/xCAT-server/lib/xcat/plugins/debian.pm @@ -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,8 +1126,9 @@ sub mkinstall { $platform, $partitionfile, \%tmpl_hash, - osarch => $arch, - pkgdirs => $pkgdirval + osarch => $arch, + pkgdirs => $pkgdirval, + environvar => $environvar ); } From d266a6ef7baa350640c588e5ee7ace4e6cbbfa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 07/13] test(xCAT-test): cover the autoinstall package list rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry filter is called directly, and a template with the token is rendered through subvars against a pkglist with a comment, a removal, a group and an include. The rendered list must carry one item per package at the token's indentation, and no token line without a pkglist. Against the previous module the helper does not exist. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_pkglist.t | 200 ++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 xCAT-test/unit/ubuntu_subiquity_pkglist.t diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglist.t b/xCAT-test/unit/ubuntu_subiquity_pkglist.t new file mode 100644 index 000000000..05d15c361 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkglist.t @@ -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(); From a711f6c1e1044d97b3e170c77f9b98ec3859b3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 08/13] feat(ubuntu): install the osimage pkglist during the Subiquity autoinstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compute.subiquity.tmpl named a fixed package set, so the osimage pkglist took effect only when ospkgs ran after the first boot. The packages list now ends with the autoinstall token, so the pkglist packages install from the configured apt mirror during the autoinstall. The fixed set stays, so a node installs the same packages as before plus its pkglist. The apt sources the installer gets for the pkgdir mirrors and the otherpkgs repository are removed from the target at the end of the install. ospkgs and otherpkgs write their own after the first boot, and a second source for one repository with other options makes apt refuse the whole list. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../share/xcat/install/ubuntu/compute.subiquity.tmpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl index 5918a61e3..328ccd258 100644 --- a/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl +++ b/xCAT-server/share/xcat/install/ubuntu/compute.subiquity.tmpl @@ -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 next". Require both tokens: another service on that port is not a flipped From 8d4fd845edb0f52549fdd055bf6d28a092f336dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:47 -0300 Subject: [PATCH 09/13] test(xCAT-test): pin the autoinstall token in the Subiquity template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The packages list must carry the token and keep openssh-server and wget, which xCAT and the template's own commands need. Against the previous template the token assertion fails. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_subiquity_template.t | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/xCAT-test/unit/ubuntu_subiquity_template.t b/xCAT-test/unit/ubuntu_subiquity_template.t index 140dfaf39..ecff4688e 100644 --- a/xCAT-test/unit/ubuntu_subiquity_template.t +++ b/xCAT-test/unit/ubuntu_subiquity_template.t @@ -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(); From 0b6478453c273a4762ed1b8a08e4e58349ec1e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:37:48 -0300 Subject: [PATCH 10/13] docs(deployment): the pkglist installs during a Subiquity autoinstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../common/deployment/additionalpkg/additional_pkg_overview.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst b/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst index f867e901a..27ce4d7a3 100644 --- a/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst +++ b/docs/source/guides/admin-guides/manage_clusters/common/deployment/additionalpkg/additional_pkg_overview.rst @@ -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 From bf2f4531511768a0cc6f1daaeeff56793b3b8d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:30:49 -0300 Subject: [PATCH 11/13] test(xCAT-test): stop pinning the shared list for the 24.04 compute profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install compute profile gets a 24.04 pkglist of its own next, so the assertion that it resolves the shared list with ntp is removed ahead of it. The service, kvm and netboot cases keep that pin. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_shared_pkglists.t | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xCAT-test/unit/ubuntu_shared_pkglists.t b/xCAT-test/unit/ubuntu_shared_pkglists.t index 0fa526fdc..ff1d0092e 100644 --- a/xCAT-test/unit/ubuntu_shared_pkglists.t +++ b/xCAT-test/unit/ubuntu_shared_pkglists.t @@ -39,7 +39,8 @@ 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 gets a 24.04 list of its own next, so it is no longer pinned here. +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" ); From eb587982e8c9320f8afbd77aae49dab0ffe6f759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:07:12 -0300 Subject: [PATCH 12/13] fix(ubuntu): give the Subiquity releases default pkglists with chrony MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ubuntu 20.04, 22.04 and 24.04 resolved the shared compute.pkglist on every architecture but x86_64 20.04, and that list names ntp for the releases before Subiquity. The Subiquity template installs chrony, and on these releases ntp pulls ntpsec, which conflicts with it, so one apt transaction with both cannot be satisfied and ospkgs replaced chrony after the first boot. Each of the three releases now has its own default list with chrony, the list 26.04 already had. The shared list keeps ntp for the releases where chrony was not the default. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- .../share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist | 5 +++++ .../share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist | 5 +++++ .../share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist create mode 100644 xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu20.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu22.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd diff --git a/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist new file mode 100644 index 000000000..aaec41835 --- /dev/null +++ b/xCAT-server/share/xcat/install/ubuntu/compute.ubuntu24.04.pkglist @@ -0,0 +1,5 @@ +openssh-server +chrony +gawk +nfs-common +snmpd From 65232ac215d5eac7a4379f2b67cf3a1ef1e0ecbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:30:50 -0300 Subject: [PATCH 13/13] test(xCAT-test): pin one time daemon per Subiquity install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compute pkglist of each Subiquity release and architecture is resolved as mkinstall resolves it, its packages read as ospkgs reads them, and joined with the template's fixed set: chrony must be there, ntp must not, and the union must carry exactly one time daemon. 16.04 must still resolve the shared list with ntp. Against the previous tree the 20.04, 22.04 and 24.04 cases fail. Signed-off-by: Vinícius Ferrão <2031761+viniciusferrao@users.noreply.github.com> --- xCAT-test/unit/ubuntu_shared_pkglists.t | 8 ++- xCAT-test/unit/ubuntu_subiquity_pkglists.t | 62 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 xCAT-test/unit/ubuntu_subiquity_pkglists.t diff --git a/xCAT-test/unit/ubuntu_shared_pkglists.t b/xCAT-test/unit/ubuntu_shared_pkglists.t index ff1d0092e..3046c3985 100644 --- a/xCAT-test/unit/ubuntu_shared_pkglists.t +++ b/xCAT-test/unit/ubuntu_shared_pkglists.t @@ -39,7 +39,8 @@ sub resolved { } # The shared lists keep ntp for the releases that still carry it. aarch64 has no list of its own. -# The install compute profile gets a 24.04 list of its own next, so it is no longer pinned here. +# 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' ); @@ -47,6 +48,11 @@ foreach my $case ( [ $install, 'service' ], [ $install, 'kvm' ], [ $netboot, 'co 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' ); diff --git a/xCAT-test/unit/ubuntu_subiquity_pkglists.t b/xCAT-test/unit/ubuntu_subiquity_pkglists.t new file mode 100644 index 000000000..393dee965 --- /dev/null +++ b/xCAT-test/unit/ubuntu_subiquity_pkglists.t @@ -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();