2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-20 22:55:40 +00:00

test(imgport): cover the boot files after a failed definition write

Each failure of the definition write runs over a published image, an
image without a checksum, a new image, or a statelite image. The
previous root image, checksum, metainfo, kernel and ramdisk must be
back, and the files of a new image must be gone. When a row does not go
back, also after a changed rootimgdir, the new files must stay. The
publication lock must be held during the writes and released after
them. A row written before its call failed must keep the new files.
This commit is contained in:
Vinícius Ferrão
2026-10-17 00:16:05 -03:00
parent c0f1bef473
commit 431bd0c23f
+92 -40
View File
@@ -8,7 +8,7 @@ use lib "$FindBin::Bin/../../perl-xCAT";
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Digest::SHA qw(sha256_hex);
use File::Path qw(make_path);
use File::Slurper qw(read_binary write_text);
use File::Slurper qw(read_binary write_binary write_text);
use File::Temp qw(tempdir);
use Test::More;
use XCAT::Test::File qw(repo_path);
@@ -98,6 +98,15 @@ sub committed {
return $value;
}
# The bytes of a file that a failed import can leave missing.
sub content {
my ($path) = @_;
return -e $path ? read_binary($path) : undef;
}
# packimage wrote the previous image with a checksum, unless it predates them, and a BitTorrent metainfo.
my $metainfo = 'd4:infod6:pieces20:' . join('', map { chr } 0 .. 19) . 'ee';
# Imports a bundle over an image while the database refuses the writes to $case->{refuse}, or a call fails the way
# $case->{fail} names.
sub failed_import {
@@ -105,8 +114,17 @@ sub failed_import {
my $name = 'failed-' . ++$sequence;
my $dest = "$fixture/install/netboot/$name";
my $bundle = "$fixture/bundle-$sequence";
my @boot = ($case->{statelite} ? 'rootimgtree.gz' : 'rootimg.cpio.gz', qw(kernel initrd-stateless.gz));
make_path($dest, $bundle);
write_text("$bundle/$_", "$_ from the bundle\n") for qw(rootimg.cpio.gz kernel initrd-stateless.gz);
write_text("$bundle/$_", "$_ from the bundle\n") for @boot;
if ($case->{previous}) {
if (!$case->{statelite}) {
write_text("$dest/rootimg.cpio.gz", $published);
write_text("$dest/rootimg.cpio.gz.sha256", sha256_hex($published) . " rootimg.cpio.gz\n") if $case->{sums};
write_binary("$dest/rootimg.cpio.gz.metainfo", $metainfo);
}
write_text("$dest/$_", "published $_\n") for qw(kernel initrd-stateless.gz);
}
xCAT::Table->new('osimage', -create => 1)->setAttribs({ imagename => $name }, { osvers => 'rhels9.5', provmethod => 'netboot', comments => 'previous image', ($case->{disabled} ? (disable => '1') : ()) });
xCAT::Table->new('linuximage', -create => 1)->setAttribs({ imagename => $name }, { rootimgdir => $case->{rootimgdir} }) if $case->{rootimgdir};
my $fail = $case->{fail} // '';
@@ -118,7 +136,7 @@ sub failed_import {
$shared = xCAT::Table->new('osimage', -create => 1, -autocommit => 0);
$shared->setAttribs({ imagename => "$name-other" }, { osvers => 'other' });
}
my (@errors, @warnings, @messages, $placed);
my (@errors, @warnings, @messages, $placed, $locked);
{
no warnings 'redefine';
my ($set, $commit, $delete, $commit_failed) = (\&xCAT::Table::setAttribs, \&xCAT::Table::commit, \&xCAT::Table::delEntries, 0);
@@ -133,20 +151,19 @@ sub failed_import {
# The database worker passes the exception of a failed call to the caller, and returns undef when it does not answer.
local *xCAT::Table::setAttribs = sub {
goto &$set if caller() ne 'xCAT_plugin::imgport';
# After this import wrote them, another import adopts the kit, another host changes the osimage row, or
# another writer fills more of the linuximage row.
xCAT::Table->new('kit')->setAttribs({ kitname => $kit }, { basename => 'mykit', version => '1.0' })
if $case->{other_kit} && $_[0]{tabname} eq 'kitrepo' && caller() eq 'xCAT_plugin::imgport';
xCAT::Table->new('osimage')->setAttribs({ imagename => $name }, { osvers => 'rhels9.7' })
if $case->{other_host} && $_[0]{tabname} eq 'linuximage' && caller() eq 'xCAT_plugin::imgport';
xCAT::Table->new('linuximage')->setAttribs({ imagename => $name }, { pkglist => '/install/custom/other.pkglist' })
if $case->{filled} && $_[0]{tabname} eq 'kit' && caller() eq 'xCAT_plugin::imgport';
if ($fail eq 'die in the linuximage restore' && $_[0]{tabname} eq 'linuximage' && caller() eq 'xCAT_plugin::imgport') {
xCAT::Table->new('kit')->setAttribs({ kitname => $kit }, { basename => 'mykit', version => '1.0' }) if $case->{other_kit} && $_[0]{tabname} eq 'kitrepo';
xCAT::Table->new('osimage')->setAttribs({ imagename => $name }, { osvers => 'rhels9.7' }) if $case->{other_host} && $_[0]{tabname} eq 'linuximage';
xCAT::Table->new('linuximage')->setAttribs({ imagename => $name }, { pkglist => '/install/custom/other.pkglist' }) if $case->{filled} && $_[0]{tabname} eq 'kit';
$locked //= -d "$dest/.packimage-publish";
if ($fail eq 'die in the linuximage restore' && $_[0]{tabname} eq 'linuximage') {
goto &$set if !$linuximage_writes++;
die "the database is gone\n";
}
goto &$set if caller() ne 'xCAT_plugin::imgport' || $_[0]{tabname} ne 'osimage' || $fail !~ /^(?:die|no reply|die after the write|die in the restore|unreadable after the write)$/;
goto &$set if $_[0]{tabname} ne 'osimage' || $fail !~ /^(?:die|no reply|die after the write|die in the restore|unreadable after the write)$/;
if ($fail eq 'die in the restore') {
goto &$set if !$osimage_writes++;
die "the database is gone\n";
@@ -168,13 +185,18 @@ sub failed_import {
goto &$commit;
};
local *xCAT::Table::new = sub {
return $shared if $shared && $_[1] eq 'osimage' && ((caller(1))[3] // '') eq 'xCAT_plugin::imgport::set_config';
my $opener = (caller(1))[3] // '';
return $shared if $shared && $_[1] eq 'osimage' && $opener eq 'xCAT_plugin::imgport::set_config';
if ($fail eq 'die opening a table' && $_[1] eq 'kit' && $opener eq 'xCAT_plugin::imgport::set_config') {
$locked //= -d "$dest/.packimage-publish";
die "the database is gone\n";
}
goto &$new;
};
$placed = eval {
xCAT_plugin::imgport::place_image(
{
rootimg => "$dest/rootimg.cpio.gz",
($case->{statelite} ? (rootimgtree => "$dest/rootimgtree.gz") : (rootimg => "$dest/rootimg.cpio.gz")),
kernel => "$dest/kernel",
ramdisk => "$dest/initrd-stateless.gz",
osimage => { imagename => $name, osvers => 'rhels9.6', osarch => 'x86_64', profile => 'compute', provmethod => 'netboot', comments => "operator's image", ($case->{enabled} ? (disable => '0') : ()) },
@@ -189,40 +211,47 @@ sub failed_import {
}
my $free = table_free($case->{refuse} // 'osimage');
$allow->();
return { placed => $placed, dest => $dest, errors => join("\n", @errors), warnings => join("\n", @warnings), messages => join("\n", @messages),
free => $free, osvers => committed('osimage', 'osvers', $name), rootimgdir => committed('linuximage', 'rootimgdir', $name),
return { placed => $placed, errors => join("\n", @errors), warnings => join("\n", @warnings), messages => join("\n", @messages),
dest => $dest, free => $free, locked => $locked, osvers => committed('osimage', 'osvers', $name),
rootimgdir => committed('linuximage', 'rootimgdir', $name),
disable => committed('osimage', 'disable', $name), comments => committed('osimage', 'comments', $name),
pkglist => committed('linuximage', 'pkglist', $name), visible => scalar(xCAT::Table->new('osimage')->getAttribs({ imagename => $name }, 'osvers')), kitversion => committed('kit', 'version', "kit-$name", 'kitname'),
other => committed('osimage', 'osvers', "$name-other") };
pkglist => committed('linuximage', 'pkglist', $name), visible => scalar(xCAT::Table->new('osimage')->getAttribs({ imagename => $name }, 'osvers')), kitversion => committed('kit', 'version', "kit-$name", 'kitname'), other => committed('osimage', 'osvers', "$name-other") };
}
my $refused = qr/Cannot write the (\w+) table: .*the database refuses the write/;
for my $case (
{ refuse => 'osimage', osvers => 'rhels9.5' },
{ refuse => 'linuximage', osvers => 'rhels9.5', rows => qr/Put back the previous osimage row of imagename=/ },
{ refuse => 'kit', kit => 1, osvers => 'rhels9.5', rootimgdir => '/install/netboot/previous', rows => qr/Put back the previous linuximage row of imagename=/ },
{ refuse => 'kit', kit => 1, osvers => 'rhels9.5', rows => qr/Removed the new linuximage row of imagename=/ },
{ refuse => 'linuximage', disabled => 1, osvers => 'rhels9.5', rows => qr/Put back the previous osimage row of imagename=/ },
{ refuse => 'kitrepo', kit => 1, other_kit => 1, osvers => 'rhels9.5' },
{ refuse => 'linuximage', other_host => 1, osvers => 'rhels9.7', changed => qr/The osimage row of imagename=\S+ changed after this import wrote it, so the change stays\./ },
{ fail => 'die in the commit', osvers => 'rhels9.5', error => qr/Cannot commit the linuximage table: the database is gone/, rows => qr/Put back the previous osimage row of imagename=/ },
{ fail => 'die', pending => 1, osvers => 'rhels9.5', error => qr/Cannot write the osimage table: the database is gone/ },
{ fail => 'no reply', osvers => 'rhels9.5', error => qr/Cannot write the osimage table: no reply from the database/ },
{ fail => 'die in the restore', refuse => 'linuximage', osvers => 'rhels9.6', error => qr/The previous osimage row of imagename=\S+ is not back\./ },
{ fail => 'change before the removal', refuse => 'kit', kit => 1, osvers => 'rhels9.5', after => '/install/netboot/other', error => qr/The previous linuximage row of imagename=\S+ is not back\./ },
{ refuse => 'kit', kit => 1, filled => 1, osvers => 'rhels9.5', changed => qr/The linuximage row of imagename=\S+ changed after this import wrote it, so the change stays\./ },
{ refuse => 'linuximage', enabled => 1, osvers => 'rhels9.5', rows => qr/Put back the previous osimage row of imagename=/ },
{ fail => 'die in the linuximage restore', refuse => 'kit', kit => 1, rootimgdir => '/install/netboot/previous', after => 'new', osvers => 'rhels9.5',
{ refuse => 'osimage', previous => 1, sums => 1 },
{ refuse => 'osimage', previous => 1, sums => 0 },
{ refuse => 'osimage', previous => 0 },
{ refuse => 'linuximage', previous => 1, sums => 1, rows => qr/Put back the previous osimage row of imagename=/ },
{ refuse => 'linuximage', previous => 1, statelite => 1 },
{ refuse => 'kit', kit => 1, previous => 1, sums => 1, rootimgdir => '/install/netboot/previous', rows => qr/Put back the previous linuximage row of imagename=/ },
{ refuse => 'kit', kit => 1, previous => 1, sums => 1, rows => qr/Removed the new linuximage row of imagename=/ },
{ refuse => 'linuximage', disabled => 1, previous => 1, sums => 1, rows => qr/Put back the previous osimage row of imagename=/ },
{ refuse => 'kitrepo', kit => 1, other_kit => 1, previous => 1, sums => 1 },
{ refuse => 'linuximage', other_host => 1, previous => 1, sums => 1, stay => 1, osvers => 'rhels9.7', changed => qr/The osimage row of imagename=\S+ changed after this import wrote it, so the change stays\./ },
{ fail => 'die in the commit', previous => 1, sums => 1, error => qr/Cannot commit the linuximage table: the database is gone/, rows => qr/Put back the previous osimage row of imagename=/ },
{ fail => 'die', previous => 1, sums => 1, pending => 1, error => qr/Cannot write the osimage table: the database is gone/ },
{ fail => 'no reply', previous => 1, sums => 1, error => qr/Cannot write the osimage table: no reply from the database/ },
{ fail => 'die opening a table', previous => 1, sums => 1, error => qr/Cannot import the image: the database is gone/ },
{ fail => 'die in the restore', refuse => 'linuximage', previous => 1, sums => 1, stay => 1, error => qr/The previous osimage row of imagename=\S+ is not back\./ },
{ fail => 'change before the removal', refuse => 'kit', kit => 1, previous => 1, sums => 1, stay => 1, after => '/install/netboot/other', error => qr/The previous linuximage row of imagename=\S+ is not back\./ },
{ refuse => 'kit', kit => 1, filled => 1, previous => 1, sums => 1, stay => 1, changed => qr/The linuximage row of imagename=\S+ changed after this import wrote it, so the change stays\./ },
{ refuse => 'linuximage', enabled => 1, previous => 1, sums => 1, rows => qr/Put back the previous osimage row of imagename=/ },
{ fail => 'die in the linuximage restore', refuse => 'kit', kit => 1, rootimgdir => '/install/netboot/previous', after => 'new', previous => 1, sums => 1, stay => 1,
error => qr/The previous linuximage row of imagename=\S+ is not back\./ },
{ malformed => 1, osvers => 'rhels9.5', error => qr/Cannot write the image definition: /, rows => qr/Put back the previous osimage row of imagename=/ },
{ fail => 'unreadable after the write', osvers => 'rhels9.6', error => qr/The previous osimage row of imagename=\S+ is not back: the table cannot be read\./ },
{ fail => 'die after the write', osvers => 'rhels9.6', imported => 1, warning => qr/The osimage table holds the new row, but its write reported: a notification failed/ },
{ malformed => 1, previous => 1, sums => 1, error => qr/Cannot write the image definition: /, rows => qr/Put back the previous osimage row of imagename=/ },
{ fail => 'unreadable after the write', previous => 1, sums => 1, stay => 1, osvers => 'rhels9.6',
error => qr/The previous osimage row of imagename=\S+ is not back: the table cannot be read\./ },
{ fail => 'die after the write', previous => 1, sums => 1, imported => 1, warning => qr/The osimage table holds the new row, but its write reported: a notification failed/ },
) {
my $label = join(', ', grep { $_ } ($case->{fail}, $case->{refuse} && "refused $case->{refuse} write", $case->{rootimgdir} && 'previous linuximage row',
$case->{disabled} && 'disabled osimage row', $case->{other_kit} && 'kit of another import', $case->{other_host} && 'osimage row of another host',
$case->{malformed} && 'kitrepo of an unexpected shape', $case->{filled} && 'linuximage row that another writer filled',
$case->{enabled} && 'disable column that was NULL'));
my $label = join(', ', grep { $_ } ($case->{fail}, $case->{refuse} && "refused $case->{refuse} write", $case->{statelite} && 'statelite image',
!$case->{previous} && 'new image', $case->{previous} && !$case->{statelite} && !$case->{sums} && 'image without checksum',
$case->{rootimgdir} && 'previous linuximage row', $case->{disabled} && 'disabled osimage row', $case->{other_kit} && 'kit of another import',
$case->{other_host} && 'osimage row of another host', $case->{malformed} && 'kitrepo of an unexpected shape',
$case->{filled} && 'linuximage row that another writer filled', $case->{enabled} && 'disable column that was NULL'));
my $result = failed_import($case);
my $dest = $result->{dest};
if ($case->{imported}) {
ok($result->{placed}, "$label: the import succeeds, as the row is written") or diag($result->{errors});
like($result->{warnings}, $case->{warning}, "$label: the failure after the write is reported");
@@ -237,7 +266,30 @@ for my $case (
is($result->{kitversion}, '1.0', "$label: the kit that another import uses stays") if $case->{other_kit};
ok($result->{free}, "$label: no transaction of the import stays open");
is($result->{other}, 'other', "$label: the pending write of another request stays") if $case->{pending};
is($result->{osvers}, $case->{osvers}, "$label: the osimage row is as expected");
# Another host publishes or imports this image only after the outcome of the definition is known.
ok($result->{locked}, "$label: the publication lock is held while the definition is written");
ok(!-e "$dest/.packimage-publish", "$label: the publication lock is released");
like($result->{errors}, qr/The imported boot files stay, because the image definition has rows that did not go back\./, "$label: the kept boot files are reported") if $case->{stay};
if ($case->{imported} || $case->{stay}) {
is(content("$dest/$_"), "$_ from the bundle\n", "$label: the new $_ stays with the new image definition") for qw(rootimg.cpio.gz kernel initrd-stateless.gz);
} else {
if ($case->{statelite}) {
ok(!-e "$dest/rootimgtree.gz", "$label: the root tree of the bundle is removed");
} elsif ($case->{previous}) {
is(content("$dest/rootimg.cpio.gz"), $published, "$label: the previous image is published again");
if ($case->{sums}) {
is(content("$dest/rootimg.cpio.gz.sha256"), sha256_hex($published) . " rootimg.cpio.gz\n", "$label: the previous checksum is back");
} else {
ok(!-e "$dest/rootimg.cpio.gz.sha256", "$label: the image has no checksum, as before");
}
is(content("$dest/rootimg.cpio.gz.metainfo"), $metainfo, "$label: the previous metainfo is back");
} else {
ok(!-e "$dest/$_", "$label: $_, which had no previous version, is removed") for qw(rootimg.cpio.gz rootimg.cpio.gz.sha256);
}
is(content("$dest/$_"), $case->{previous} ? "published $_\n" : undef, "$label: $_ is as it was") for qw(kernel initrd-stateless.gz);
}
is($result->{osvers}, $case->{osvers} // ($case->{imported} || ($case->{fail} // '') eq 'die in the restore' ? 'rhels9.6' : 'rhels9.5'), "$label: the osimage row is as expected");
my $rootimgdir = ($case->{after} // '') eq 'new' ? $result->{dest} : $case->{after} // $case->{rootimgdir};
is($result->{rootimgdir}, $rootimgdir, "$label: the linuximage row is as expected") if $case->{kit} && !$case->{filled};
is($result->{pkglist}, '/install/custom/other.pkglist', "$label: the linuximage row that another writer filled stays") if $case->{filled};