diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index 51859d23b..575446a1c 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -24,6 +24,7 @@ jobs: run: >- prove xCAT-test/native/ubuntu_live_media_guardrails.t xCAT-test/native/ubuntu_genimage_debootstrap_arch.t + xCAT-test/native/ubuntu_genimage_apt_mirror.t - name: Run tests if: ${{ !cancelled() }} run: perl github_action_xcat_test.pl diff --git a/xCAT-test/lib/XCAT/Test/UbuntuImage.pm b/xCAT-test/lib/XCAT/Test/UbuntuImage.pm index 39662be8b..80d20e638 100644 --- a/xCAT-test/lib/XCAT/Test/UbuntuImage.pm +++ b/xCAT-test/lib/XCAT/Test/UbuntuImage.pm @@ -72,10 +72,33 @@ sub bootstrap { my ($self, %options) = @_; $self->prepare_genimage(); $self->write('work/packages/Packages.gz', 'package index fixture'); - $self->write('work/site.json', encode_json({})); + $self->write('work/site.json', encode_json({ + (exists $options{mirror} ? (ubuntu_apt_mirror => $options{mirror}) : ()), + })); + if ($options{bootstrap_success}) { + $self->write('work/bootstrap-success', ''); + for my $tool (qw(mount umount)) { + my $path = $self->write("work/bin/$tool", "#!/bin/sh\nexit 0\n"); + chmod(0755, $path) or die "chmod $path: $!"; + } + my $chroot = $self->write('work/bin/chroot', <<'SH'); +#!/bin/sh +case "$*" in + '/work/image/rootimg apt-get update') + printf '%s\n' "$@" > /work/apt-update.args + exit 23 ;; + *) printf 'Unexpected chroot command: %s\n' "$*" >&2; exit 97 ;; +esac +SH + chmod(0755, $chroot) or die "chmod $chroot: $!"; + } my $command = $self->write('work/bin/debootstrap', <<'SH'); #!/bin/sh printf '%s\n' "$@" > /work/debootstrap.args +if [ -f /work/bootstrap-success ]; then + mkdir -p "$5/etc/apt" "$5/proc" "$5/usr/sbin" "$5/mnt" + exit 0 +fi exit 23 SH chmod(0755, $command) or die "chmod $command: $!"; diff --git a/xCAT-test/native/fixtures/ubuntu_image/genimage.pl b/xCAT-test/native/fixtures/ubuntu_image/genimage.pl index a2eaaab16..2e3fe97f0 100644 --- a/xCAT-test/native/fixtures/ubuntu_image/genimage.pl +++ b/xCAT-test/native/fixtures/ubuntu_image/genimage.pl @@ -1,14 +1,20 @@ use strict; use warnings; -use File::Slurper qw(read_text); -use JSON::PP qw(decode_json); +use File::Slurper qw(read_text write_binary); +use JSON::PP qw(decode_json encode_json); use xCAT::Table; my %values = (master => '192.0.2.1', httpport => '80', %{ decode_json(read_text('/work/site.json')) }); my $site = xCAT::Table->new('site', -create => 1); -$site->setAttribs({ key => $_ }, { value => $values{$_} }) for keys %values; +for my $key (keys %values) { + $site->setAttribs({ key => $key }, { value => 'https://previous.example.invalid/ubuntu' }) + if $key eq 'ubuntu_apt_mirror' && $values{$key} eq ''; + $site->setAttribs({ key => $key }, { value => $values{$key} }); +} +my $mirror_row = $site->getAttribs({ key => 'ubuntu_apt_mirror' }, qw(key value)); +write_binary('/work/site-row.json', encode_json({ mirror => $mirror_row })); $site->close(); exec 'perl', @ARGV; die "exec genimage: $!"; diff --git a/xCAT-test/native/ubuntu_genimage_apt_mirror.t b/xCAT-test/native/ubuntu_genimage_apt_mirror.t new file mode 100644 index 000000000..9f887e6ff --- /dev/null +++ b/xCAT-test/native/ubuntu_genimage_apt_mirror.t @@ -0,0 +1,70 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../lib"; +use JSON::PP qw(decode_json); +use Test::More; +use XCAT::Test::UbuntuImage; + +plan skip_all => 'Linux filesystem namespaces are required' unless $^O eq 'linux'; + +my $archive = 'http://archive.ubuntu.com/ubuntu'; +my $ports = 'http://ports.ubuntu.com/ubuntu-ports'; +my $site = 'https://site.example.invalid/ubuntu'; +my $explicit = 'https://image.example.invalid/ubuntu'; + +for my $case ( + ['amd64 default', {arch => 'x86_64'}, 'amd64', $archive, 'noble'], + ['i386 default', {arch => 'x86'}, 'i386', $archive, 'noble'], + ['POWER default', {arch => 'ppc64el'}, 'ppc64el', $ports, 'noble'], + ['RISC-V default', {arch => 'riscv64'}, 'riscv64', $ports, 'noble'], + ['s390x default', {arch => 's390x'}, 's390x', $ports, 'noble'], + ['site overrides archive', {mirror => $site}, 'amd64', $site, 'noble'], + ['site overrides ports', {arch => 'riscv64', mirror => $site}, 'riscv64', $site, 'noble'], + ['cleared site keeps default', {arch => 'riscv64', mirror => ''}, 'riscv64', $ports, 'noble'], + ['pkgdir overrides site', {mirror => $site, + pkgdir => "/work/packages,$explicit jammy main"}, 'amd64', $explicit, 'jammy'], + ['first pkgdir mirror wins', {pkgdir => + "/work/packages,$explicit noble main,$site jammy universe"}, 'amd64', $explicit, 'noble'], +) { + my ($name, $options, $arch, $mirror, $suite) = @$case; + subtest $name => sub { + my $image = XCAT::Test::UbuntuImage->new(); + my ($status, $output, $args) = $image->bootstrap(%$options, bootstrap_success => 1); + if (exists $options->{mirror} && $options->{mirror} eq '') { + is_deeply(decode_json($image->read('work/site-row.json'))->{mirror}, + { key => 'ubuntu_apt_mirror' }, 'the cleared setting remains as an empty database row'); + } + ok($status > 0 && ($status & 127) == 0, + 'genimage exits unsuccessfully after the injected package-metadata failure') or diag($output); + is_deeply($args, ['--verbose', '--arch', $arch, $suite, + '/work/image/rootimg', $mirror], 'the selected mirror reaches debootstrap') or diag($output); + like($output, qr/Failed to update package metadata/, + 'package-metadata failure is reported'); + unlike($output, qr/Unexpected chroot command/, 'only the expected chroot command runs'); + is($image->read('work/apt-update.args'), "/work/image/rootimg\napt-get\nupdate\n", + 'APT uses the generated image root'); + my @sources = grep { /\S/ } split /\n/, + $image->read('work/image/rootimg/etc/apt/sources.list'); + my @expected = exists $options->{pkgdir} + ? map { "deb $_" } grep { /^https?:/ } split /,/, $options->{pkgdir} + : map { "deb $mirror $_ main universe" } ($suite, "$suite-updates", "$suite-security"); + unshift @expected, "deb [trusted=yes] http://192.0.2.1:80/work/packages $suite main restricted universe"; + is_deeply([map { [split ' '] } @sources], [map { [split ' '] } @expected], + 'image APT sources retain the local mirror and all configured remote pockets'); + }; +} + +subtest 'explicit mirror without a suite' => sub { + my $image = XCAT::Test::UbuntuImage->new(); + my ($status, $output, $args) = $image->bootstrap( + pkgdir => "/work/packages,$explicit"); + is($status, 256, 'invalid mirror is rejected'); + is($args, undef, 'debootstrap is not invoked'); + like($output, qr/first http mirror path must includes http URL and distribute name/, + 'rejection identifies the missing suite'); +}; + +done_testing(); diff --git a/xCAT-test/unit/ubuntu_genimage_apt_mirror.t b/xCAT-test/unit/ubuntu_genimage_apt_mirror.t deleted file mode 100644 index c3ad36a43..000000000 --- a/xCAT-test/unit/ubuntu_genimage_apt_mirror.t +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use File::Spec; -use FindBin; -use Test::More; - -# archive.ubuntu.com publishes amd64 and i386 only. A ppc64el or riscv64 netboot image built -# against it finds no package at all, and debootstrap fails before it copies anything, so the -# default mirror has to follow the image architecture. -# -# Driven by the real selection code: the two statements are extracted from genimage and -# evaluated here, so this test tracks the script rather than a copy of it. - -my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); -use lib "$FindBin::Bin/../../perl-xCAT"; -require xCAT::Utils; -my $genimage_path = File::Spec->catfile( - $repo_root, 'xCAT-server', 'share', 'xcat', 'netboot', 'ubuntu', 'genimage' -); -plan skip_all => "genimage not found at $genimage_path" unless -f $genimage_path; - -my $src = do { local $/; open my $fh, '<', $genimage_path or die $!; <$fh> }; - -my ($default) = $src =~ /^\s*(my \$default = \(\$uarch =~.*?;)\s*$/ms; -ok( defined $default, 'found the default mirror selection in genimage' ) - or done_testing(), exit; - -my ($pick) = $src =~ /^\s*(my \$mirror = \(defined \$aptmirror\[0\].*?;)\s*$/ms; -ok( defined $pick, 'found the mirror override in genimage' ) - or done_testing(), exit; - -# genimage does not read osimage.osarch directly: it converts it first, so the value the selection -# sees is whatever xCAT::Utils::debian_arch returns. Driving the xCAT token through that conversion -# is what catches a token the map does not know. -sub choose_osarch { - my ( $osarch, $site ) = @_; - return choose( xCAT::Utils->debian_arch($osarch), $site ); -} - -sub choose { - my ( $uarch, $site ) = @_; - my $set = defined $site ? "('$site')" : "()"; - ## no critic (BuiltinFunctions::ProhibitStringyEval) - my $mirror = eval "my \$uarch = '$uarch'; my \@aptmirror = $set; $default $pick \$mirror"; - ## use critic - die "failed to evaluate the genimage mirror selection: $@" if $@; - return $mirror; -} - -is( choose('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports', - 'a riscv64 image takes the ports archive' ); -is( choose('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports', - 'a ppc64el image takes the ports archive' ); -is( choose('amd64'), 'http://archive.ubuntu.com/ubuntu', - 'an amd64 image keeps the main archive' ); -is( choose('i386'), 'http://archive.ubuntu.com/ubuntu', - 'an i386 image keeps the main archive' ); - -is( choose( 'riscv64', 'http://mirror.example.invalid/ubuntu' ), - 'http://mirror.example.invalid/ubuntu', - 'site.ubuntu_apt_mirror overrides the ports archive' ); -is( choose( 'amd64', 'http://mirror.example.invalid/ubuntu' ), - 'http://mirror.example.invalid/ubuntu', - 'site.ubuntu_apt_mirror overrides the main archive' ); -is( choose( 'riscv64', '' ), 'http://ports.ubuntu.com/ubuntu-ports', - 'an empty site.ubuntu_apt_mirror does not blank the mirror' ); - -# The architecture reaches the selection as an xCAT osarch value, not as a Debian one. -is( choose_osarch('x86_64'), 'http://archive.ubuntu.com/ubuntu', - 'osarch x86_64 reaches the archive' ); -is( choose_osarch('x86'), 'http://archive.ubuntu.com/ubuntu', - 'osarch x86 reaches the archive, which is where i386 lives' ); -is( choose_osarch('riscv64'), 'http://ports.ubuntu.com/ubuntu-ports', - 'osarch riscv64 reaches the ports archive' ); -is( choose_osarch('ppc64el'), 'http://ports.ubuntu.com/ubuntu-ports', - 'osarch ppc64el reaches the ports archive' ); - -done_testing();