diff --git a/xCAT-test/unit/imgport_definition_tables.t b/xCAT-test/unit/imgport_definition_tables.t index d7b60bff6..bf8de4c2d 100644 --- a/xCAT-test/unit/imgport_definition_tables.t +++ b/xCAT-test/unit/imgport_definition_tables.t @@ -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};