mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-21 07:08:12 +00:00
test(packaging): exercise Debian service lifecycle
This commit is contained in:
@@ -12,7 +12,13 @@ jobs:
|
||||
run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests bats bubblewrap rpm rsyslog build-essential fakeroot reprepro devscripts debhelper libcapture-tiny-perl libfile-slurper-perl libjson-perl libyaml-pp-perl libparallel-forkmanager-perl libsoap-lite-perl libdbi-perl libcgi-pm-perl quilt openssh-server dpkg looptools genometools software-properties-common grub-common grub-efi-amd64-bin
|
||||
- name: Enable postscript test namespaces
|
||||
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
- name: Install lifecycle test dependencies
|
||||
run: sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests ucf debconf-utils systemd
|
||||
- name: Test Debian package lifecycle
|
||||
run: prove -v xCAT-test/native/xcatd_debian_lifecycle.t
|
||||
- name: Prepare xCAT syslog directory
|
||||
if: ${{ !cancelled() }}
|
||||
run: sudo install -d -o syslog -g adm -m 0750 /var/log/xcat
|
||||
- name: Run tests
|
||||
if: ${{ !cancelled() }}
|
||||
run: perl github_action_xcat_test.pl
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package XCAT::Test::Lifecycle;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Capture::Tiny qw(capture);
|
||||
use Cwd qw(getcwd);
|
||||
use File::Basename qw(dirname basename);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Slurper qw(read_binary write_binary);
|
||||
use File::Temp qw(tempdir);
|
||||
use Text::ParseWords qw(shellwords);
|
||||
use XCAT::Test::File qw(repo_path);
|
||||
|
||||
sub command {
|
||||
my (@argv) = @_;
|
||||
my ($out, $err, $status) = capture { system(@argv) };
|
||||
$status = $status == -1 ? 255 : ($status & 127) ? 128 + ($status & 127) : $status >> 8;
|
||||
return ($status, $out, $err);
|
||||
}
|
||||
|
||||
sub checked {
|
||||
my (@argv) = @_;
|
||||
my ($status, $out, $err) = command(@argv);
|
||||
die "@argv failed ($status):\n$out$err" if $status;
|
||||
return $out;
|
||||
}
|
||||
|
||||
sub build_deb {
|
||||
my ($package) = @_;
|
||||
my $work = tempdir(CLEANUP => 1);
|
||||
checked('tar', '-cf', "$work/source.tar", '-C', repo_path('.'), $package);
|
||||
checked('tar', '-xf', "$work/source.tar", '-C', $work);
|
||||
my $previous = getcwd();
|
||||
chdir "$work/$package" or die "chdir: $!";
|
||||
local $ENV{PWD} = getcwd();
|
||||
my ($status, $out, $err) = command('dpkg-buildpackage', '-b', '-uc', '-us', '-d');
|
||||
chdir $previous or die "chdir: $!";
|
||||
die "Building $package failed ($status):\n$out$err" if $status;
|
||||
my @debs = glob "$work/*.deb";
|
||||
die "Expected one $package DEB, found @debs" unless @debs == 1;
|
||||
return $debs[0];
|
||||
}
|
||||
|
||||
sub new {
|
||||
my ($class) = @_;
|
||||
my $root = tempdir(CLEANUP => 1);
|
||||
my $self = bless {root => $root}, $class;
|
||||
make_path(map { "$root/$_" } qw(etc opt var tmp run install root test-bin
|
||||
usr/sbin usr/lib/systemd/system usr/share/doc proc dev
|
||||
var/lib/dpkg/updates var/lib/dpkg/info var/lib/ucf var/cache/debconf
|
||||
etc/init.d etc/profile.d etc/systemd/system etc/alternatives));
|
||||
$self->write('/var/lib/dpkg/status', '');
|
||||
$self->write('/etc/passwd', "root:x:0:0:root:/root:/bin/sh\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\n");
|
||||
$self->write('/etc/group', "root:x:0:\nwww-data:x:33:\n");
|
||||
$self->write('/etc/profile.d/xcat.sh', "export XCATROOT=/opt/xcat\nexport PATH=/test-bin:/opt/xcat/sbin:/usr/sbin:/usr/bin:/sbin:/bin\n");
|
||||
copy('/etc/ld.so.cache', "$root/etc/ld.so.cache") or die "copy ld.so.cache: $!" if -f '/etc/ld.so.cache';
|
||||
copy('/etc/debconf.conf', "$root/etc/debconf.conf") or die "copy debconf.conf: $!" if -f '/etc/debconf.conf';
|
||||
if (-f '/etc/debian_version') {
|
||||
my @packages = qw(base-files ucf debconf systemd init-system-helpers);
|
||||
$self->write('/var/lib/dpkg/status', checked('dpkg-query', '-s', @packages));
|
||||
for my $record ((map { "/var/lib/dpkg/info/$_.list" } @packages), '/var/lib/dpkg/info/ucf.templates') {
|
||||
copy($record, "$root/var/lib/dpkg/info/" . basename($record)) or die "copy $record: $!";
|
||||
}
|
||||
}
|
||||
for my $dir ('', '/sbin', '/lib', '/lib/systemd', '/share') {
|
||||
for my $entry (glob "/usr$dir/*") {
|
||||
my $name = basename($entry);
|
||||
next if $dir eq '/sbin' && $name eq 'init';
|
||||
next if -d "$root/usr$dir/$name";
|
||||
symlink("/host-usr$dir/$name", "$root/usr$dir/$name") or die "symlink: $!";
|
||||
}
|
||||
}
|
||||
for my $entry (glob '/etc/alternatives/*') {
|
||||
next unless -l $entry;
|
||||
symlink(readlink($entry), "$root/etc/alternatives/" . basename($entry)) or die "symlink: $!";
|
||||
}
|
||||
for my $dir (qw(bin sbin lib lib64)) {
|
||||
next unless -l "/$dir";
|
||||
symlink(readlink("/$dir"), "$root/$dir") or die "symlink: $!";
|
||||
}
|
||||
$self->private_bin('/sbin') if -d '/sbin' && !-l '/sbin';
|
||||
if (-f "$root/var/lib/dpkg/info/ucf.templates") {
|
||||
my ($status, $out, $err) = $self->run('debconf-loadtemplate', 'ucf', '/var/lib/dpkg/info/ucf.templates');
|
||||
die "Loading ucf templates failed: $out$err" if $status;
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub path { return $_[0]->{root} . $_[1]; }
|
||||
|
||||
sub write {
|
||||
my ($self, $path, $text, $mode) = @_;
|
||||
my $destination = $self->path($path);
|
||||
make_path(dirname($destination));
|
||||
write_binary($destination, $text);
|
||||
chmod($mode, $destination) == 1 or die "chmod $destination: $!" if defined $mode;
|
||||
}
|
||||
|
||||
sub read { return read_binary($_[0]->path($_[1])); }
|
||||
|
||||
sub private_bin {
|
||||
my ($self, $dir) = @_;
|
||||
my $target = $self->path($dir);
|
||||
unlink $target or die "unlink $target: $!" if -l $target;
|
||||
make_path($target);
|
||||
my $host = $dir =~ m{^/usr/} ? "/host-usr" . substr($dir, 4) : "/host" . $dir;
|
||||
for my $entry (glob "$dir/*") {
|
||||
my $name = basename($entry);
|
||||
next if $name eq 'init';
|
||||
next if -e "$target/$name" || -l "$target/$name";
|
||||
symlink("$host/$name", "$target/$name") or die "symlink: $!";
|
||||
}
|
||||
}
|
||||
|
||||
sub hide_command {
|
||||
my ($self, $name) = @_;
|
||||
for my $dir (qw(/usr/bin /usr/sbin /bin /sbin)) {
|
||||
next if -l $dir;
|
||||
$self->private_bin($dir);
|
||||
my $entry = $self->path("$dir/$name");
|
||||
unlink $entry or die "unlink $entry: $!" if -e $entry || -l $entry;
|
||||
}
|
||||
}
|
||||
|
||||
sub start_unit {
|
||||
my ($self, $path) = @_;
|
||||
my ($section, @commands);
|
||||
for my $line (split /\n/, $self->read($path)) {
|
||||
if ($line =~ /^\s*\[([^]]+)\]\s*$/) { $section = $1; next; }
|
||||
next unless ($section // '') eq 'Service' && $line =~ /^\s*ExecStart\s*=\s*(.*)$/;
|
||||
my $value = $1;
|
||||
if (length $value) { push @commands, $value; } else { @commands = (); }
|
||||
}
|
||||
die 'Expected one service start command' unless @commands == 1;
|
||||
return $self->run(shellwords($commands[0]));
|
||||
}
|
||||
|
||||
sub run {
|
||||
my ($self, @argv) = @_;
|
||||
my $root = $self->{root};
|
||||
my $prefix = $self->{chroot} ? '/image' : '';
|
||||
my @mounts;
|
||||
if ($self->{chroot}) {
|
||||
push @mounts, '--tmpfs', '/', '--ro-bind', '/usr', '/usr';
|
||||
for my $dir (qw(bin sbin lib lib64)) {
|
||||
next unless -e "/$dir";
|
||||
push @mounts, -l "/$dir" ? ('--symlink', readlink("/$dir"), "/$dir")
|
||||
: ('--ro-bind', "/$dir", "/$dir");
|
||||
}
|
||||
}
|
||||
push @mounts, ('--bind', $root, "$prefix/", '--ro-bind', '/usr', "$prefix/host-usr",
|
||||
'--ro-bind', repo_path('.'), "$prefix/source");
|
||||
for my $dir (qw(bin sbin lib lib64)) {
|
||||
next unless -e "/$dir";
|
||||
next if -l "$root/$dir";
|
||||
push @mounts, -l "/$dir" ? ('--symlink', readlink("/$dir"), "$prefix/$dir")
|
||||
: ('--ro-bind', "/$dir", "$prefix/" . (-d "$root/$dir" ? "host/$dir" : $dir));
|
||||
}
|
||||
push @mounts, ('--ro-bind', $self->{deb}, "$prefix/package.deb") if $self->{deb};
|
||||
push @mounts, ('--proc', "$prefix/proc") if $self->{live};
|
||||
push @mounts, ('--cap-add', 'CAP_SYS_CHROOT') if $self->{chroot};
|
||||
unshift @argv, 'chroot', $prefix if $self->{chroot};
|
||||
return command('/usr/bin/env', '-i', 'PATH=/usr/bin:/bin',
|
||||
'bwrap', '--die-with-parent', '--unshare-all', '--uid', $self->{uid} // 0,
|
||||
'--gid', $self->{gid} // 0,
|
||||
@mounts, '--dev', "$prefix/dev", '--setenv', 'PATH',
|
||||
'/test-bin:/opt/xcat/sbin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
'--setenv', 'HOME', '/root', '--setenv', 'LC_ALL', 'C',
|
||||
'--setenv', 'DEBIAN_FRONTEND', 'noninteractive', '--chdir', '/', @argv);
|
||||
}
|
||||
|
||||
sub record_command {
|
||||
my ($self, $path, $delegate) = @_;
|
||||
my $name = basename($path);
|
||||
my $forward = defined $delegate ? "exec \"$delegate\" \"\$@\"\n" : '';
|
||||
$self->write($path, "#!/bin/sh\nprintf '$name' >> /calls\n" .
|
||||
'for arg do printf " <%s>" "$arg" >> /calls; done' .
|
||||
"\nprintf '\\n' >> /calls\n$forward", 0755);
|
||||
}
|
||||
|
||||
sub install_deb {
|
||||
my ($self, $deb) = @_;
|
||||
local $self->{deb} = $deb;
|
||||
return $self->run('dpkg', '--force-depends', '--force-confold', '-i', '/package.deb');
|
||||
}
|
||||
|
||||
sub deb_script {
|
||||
my ($self, $deb, $script, @args) = @_;
|
||||
local $self->{deb} = $deb;
|
||||
my @unpack = $self->run('dpkg-deb', '-e', '/package.deb', '/tmp/control');
|
||||
return @unpack if $unpack[0];
|
||||
my $package = checked('dpkg-deb', '-f', $deb, 'Package');
|
||||
chomp $package;
|
||||
return $self->run('env', "DPKG_MAINTSCRIPT_PACKAGE=$package",
|
||||
"DPKG_MAINTSCRIPT_NAME=$script", 'DPKG_MAINTSCRIPT_ARCH=all',
|
||||
'/bin/sh', "/tmp/control/$script", @args);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,339 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin;
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use File::Slurper qw(write_binary);
|
||||
use Digest::MD5 qw(md5_hex);
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use Test::More;
|
||||
use XCAT::Test::Lifecycle;
|
||||
use XCAT::Test::File qw(slurp_repo_file);
|
||||
|
||||
plan skip_all => 'Run on Linux with dpkg-buildpackage; other missing prerequisites fail'
|
||||
unless $^O eq 'linux' && -x '/usr/bin/dpkg-buildpackage';
|
||||
my $deb = XCAT::Test::Lifecycle::build_deb('xCAT-server');
|
||||
my $version = XCAT::Test::Lifecycle::checked('dpkg-deb', '-f', $deb, 'Version');
|
||||
chomp $version;
|
||||
my $template = slurp_repo_file('xCAT-server/etc/init.d/xcatd');
|
||||
my $depends = XCAT::Test::Lifecycle::checked('dpkg-deb', '-f', $deb, 'Depends');
|
||||
my @dependencies = split /\s*,\s*/, $depends;
|
||||
ok(grep(/^ucf(?:\s*\([^)]+\))?\s*$/, @dependencies), 'built package requires ucf');
|
||||
|
||||
sub succeeds {
|
||||
my ($label, @result) = @_;
|
||||
is($result[0], 0, $label) or diag($result[1] . $result[2]);
|
||||
}
|
||||
|
||||
for my $mode (qw(systemd legacy explicit-legacy unknown)) {
|
||||
subtest "$mode package lifecycle" => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', $mode eq 'systemd' ? "ID=ubuntu\nVERSION_ID=\"24.04\"\n"
|
||||
: $mode eq 'legacy' ? "ID=ubuntu\nVERSION_ID=\"14.04\"\n" : "ID=fixture\n");
|
||||
$root->write('/sbin/init', '', 0755) if $mode eq 'explicit-legacy';
|
||||
my $modern = $mode eq 'systemd' || $mode eq 'unknown';
|
||||
succeeds('fresh package transaction', $root->install_deb($deb));
|
||||
my %checksums = map { reverse split /\s+/, $_ } split /\n/,
|
||||
$root->read('/opt/xcat/share/xcat/scripts/xcatd.md5sum');
|
||||
is($checksums{'2.17.0'}, '0b1eea60994ff79faa9a8d0bcd53c558', 'package recognizes the pre-transition template');
|
||||
is($checksums{'2.18.0'}, '8797eef6731719e0eacff59612b3e916', 'package recognizes the 2.18.0 template');
|
||||
ok(grep($_ eq md5_hex($template), values %checksums), 'package recognizes its current legacy template');
|
||||
my $init = $root->path('/etc/init.d/xcatd');
|
||||
if ($modern) {
|
||||
ok(!-e $init && !-l $init, 'systemd target has no legacy script');
|
||||
ok(-l $root->path('/etc/systemd/system/multi-user.target.wants/xcatd.service'), 'fresh service is enabled');
|
||||
$root->record_command('/opt/xcat/sbin/xcatd');
|
||||
succeeds('execute installed unit start command', $root->start_unit('/usr/lib/systemd/system/xcatd.service'));
|
||||
is($root->read('/calls'), "xcatd <-p> </run/xcatd.pid>\n", 'unit starts the daemon without a legacy script');
|
||||
succeeds('disable service', $root->run('systemctl', 'disable', 'xcatd.service'));
|
||||
} else {
|
||||
is($root->read('/etc/init.d/xcatd'), $template, 'legacy template installed');
|
||||
ok(-x $init, 'legacy script is executable');
|
||||
succeeds('disable legacy service', $root->run('update-rc.d', 'xcatd', 'disable'));
|
||||
$root->write('/etc/init.d/xcatd', "$template\n# local configuration\n", 0755);
|
||||
}
|
||||
succeeds('upgrade transaction', $root->install_deb($deb));
|
||||
if ($modern) {
|
||||
ok(!-l $root->path('/etc/systemd/system/multi-user.target.wants/xcatd.service'), 'upgrade keeps disabled state');
|
||||
ok(!-e $root->path('/var/lib/xcat/xcatd-init-state/context'), 'reconfiguration has no preinst context');
|
||||
succeeds('reconfigure the installed package', $root->deb_script($deb, 'postinst', 'configure', $version));
|
||||
ok(!-l $root->path('/etc/systemd/system/multi-user.target.wants/xcatd.service'), 'reconfiguration preserves administrator disablement');
|
||||
succeeds('mask service', $root->run('systemctl', 'mask', 'xcatd.service'));
|
||||
succeeds('upgrade masked service', $root->install_deb($deb));
|
||||
is(readlink($root->path('/etc/systemd/system/xcatd.service')), '/dev/null', 'upgrade keeps mask');
|
||||
} else {
|
||||
is($root->read('/etc/init.d/xcatd'), "$template\n# local configuration\n", 'upgrade keeps administrator edits');
|
||||
my @enabled = glob $root->path('/etc/rc[2345].d/S*xcatd');
|
||||
is(scalar @enabled, 0, 'upgrade keeps disabled runlevels');
|
||||
my @disabled = glob $root->path('/etc/rc[2345].d/K*xcatd');
|
||||
ok(@disabled, 'upgrade retains disabled registration');
|
||||
}
|
||||
ok(!-e $root->path('/var/lib/xcat/xcatd-init-state/context'), 'successful configuration clears transaction context');
|
||||
if ($modern) {
|
||||
succeeds('unmask before removal', $root->run('systemctl', 'unmask', 'xcatd.service'));
|
||||
succeeds('enable before removal', $root->run('systemctl', 'enable', 'xcatd.service'));
|
||||
ok(-l $root->path('/etc/systemd/system/multi-user.target.wants/xcatd.service'), 'removal starts with an enabled service');
|
||||
}
|
||||
succeeds('remove package', $root->run('dpkg', '--force-depends', '--remove', 'xcat-server'));
|
||||
ok(!-e $root->path('/usr/sbin/xcatd') && !-l $root->path('/usr/sbin/xcatd'), 'remove clears daemon link');
|
||||
ok(!-l $root->path('/etc/systemd/system/multi-user.target.wants/xcatd.service'), 'remove clears systemd registration');
|
||||
my @registration = glob $root->path('/etc/rc?.d/[SK]??xcatd');
|
||||
is_deeply(\@registration, [], 'remove clears legacy registration');
|
||||
for my $suffix (qw(ucf-old ucf-new ucf-dist dpkg-bak dpkg-backup dpkg-remove)) {
|
||||
$root->write("/etc/init.d/xcatd.$suffix", 'backup fixture');
|
||||
}
|
||||
$root->write('/tmp/ucf-template', 'registered fixture');
|
||||
succeeds('register a ucf checksum', $root->run('ucf', '/tmp/ucf-template', '/etc/init.d/xcatd'));
|
||||
succeeds('register ucf ownership', $root->run('ucfr', 'xcat-server', '/etc/init.d/xcatd'));
|
||||
succeeds('purge package', $root->run('dpkg', '--purge', 'xcat-server'));
|
||||
ok(!-e $init && !-l $init, 'purge clears legacy configuration');
|
||||
ok(!-e $root->path('/var/lib/xcat/xcatd-init-state/state'), 'purge clears durable transition state');
|
||||
my @backups = glob $root->path('/etc/init.d/xcatd.*');
|
||||
is_deeply(\@backups, [], 'purge removes conffile backup artifacts');
|
||||
for my $file (qw(hashfile registry)) {
|
||||
my $contents = -f $root->path("/var/lib/ucf/$file") ? $root->read("/var/lib/ucf/$file") : '';
|
||||
unlike($contents, qr{(?:^|\s)/etc/init\.d/xcatd(?:\s|$)}, "purge removes ucf $file entry");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
subtest 'legacy conffile migration through dpkg' => sub {
|
||||
my $package = tempdir(CLEANUP => 1);
|
||||
make_path("$package/old/DEBIAN", "$package/old/etc/init.d");
|
||||
write_binary("$package/old/DEBIAN/control", "Package: xcat-server\nVersion: 2.0\nArchitecture: all\nMaintainer: xCAT <xcat-user\@lists.sourceforge.net>\nDescription: legacy init conffile fixture\n");
|
||||
write_binary("$package/old/DEBIAN/conffiles", "/etc/init.d/xcatd\n");
|
||||
write_binary("$package/old/etc/init.d/xcatd", $template);
|
||||
chmod 0755, "$package/old/etc/init.d/xcatd" or die "chmod: $!";
|
||||
XCAT::Test::Lifecycle::checked('dpkg-deb', '--root-owner-group', '--build', "$package/old", "$package/old.deb");
|
||||
for my $state (qw(enabled disabled unregistered deleted)) {
|
||||
subtest $state => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=24.04\n");
|
||||
succeeds('install old conffile package', $root->install_deb("$package/old.deb"));
|
||||
if ($state eq 'deleted') {
|
||||
unlink $root->path('/etc/init.d/xcatd') or die "unlink: $!";
|
||||
} else {
|
||||
$root->write('/etc/init.d/xcatd', "$template\n# administrator edit\n", 0750);
|
||||
}
|
||||
if ($state eq 'enabled' || $state eq 'disabled') {
|
||||
make_path($root->path('/etc/rc3.d'));
|
||||
symlink '../init.d/xcatd', $root->path('/etc/rc3.d/' . ($state eq 'enabled' ? 'S' : 'K') . '85xcatd') or die "symlink: $!";
|
||||
}
|
||||
if ($state eq 'disabled' || $state eq 'unregistered') {
|
||||
$root->record_command('/usr/sbin/systemctl', '/host-usr/bin/systemctl');
|
||||
}
|
||||
succeeds('upgrade to native service package', $root->install_deb($deb));
|
||||
ok(!-e $root->path('/etc/init.d/xcatd'), 'modern target removes active legacy script');
|
||||
my $enabled = -l $root->path('/etc/systemd/system/multi-user.target.wants/xcatd.service') ? 1 : 0;
|
||||
is($enabled, $state eq 'enabled' ? 1 : 0, 'migration preserves prior registration');
|
||||
like($root->read('/calls'), qr/^systemctl <disable> <xcatd\.service>$/m,
|
||||
'legacy migration requests disable through the real service tool') if $state eq 'disabled' || $state eq 'unregistered';
|
||||
if ($state ne 'deleted') {
|
||||
is($root->read('/var/lib/xcat/xcatd-init-state/xcatd'), "$template\n# administrator edit\n", 'migration stashes edited bytes');
|
||||
}
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=14.04\n");
|
||||
succeeds('return to legacy target', $root->install_deb($deb));
|
||||
if ($state eq 'deleted') {
|
||||
ok(!-e $root->path('/etc/init.d/xcatd'), 'administrator deletion survives return');
|
||||
} else {
|
||||
is($root->read('/etc/init.d/xcatd'), "$template\n# administrator edit\n", 'return restores administrator bytes');
|
||||
is((stat($root->path('/etc/init.d/xcatd')))[2] & 07777, 0750, 'return restores mode');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
for my $action (qw(abort-install abort-upgrade remove purge upgrade failed-upgrade disappear)) {
|
||||
subtest "postrm $action recovery" => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=14.04\n");
|
||||
my $state = '/var/lib/xcat/xcatd-init-state';
|
||||
for my $file (qw(state xcatd context context.tmp.123 pending-xcatd pending-deleted pending-enabled pending-xcatd.tmp.123)) {
|
||||
$root->write("$state/$file", "retained $file\n");
|
||||
}
|
||||
$root->write('/var/lib/xcat/xcatd-systemd-mode', 'retained marker');
|
||||
succeeds('complete maintainer script', $root->deb_script($deb, 'postrm', $action));
|
||||
my $retained = $action eq 'upgrade' || $action eq 'failed-upgrade' ? 1 : 0;
|
||||
for my $file (qw(context context.tmp.123)) {
|
||||
is(-e $root->path("$state/$file") ? 1 : 0, $retained, "$action $file retention");
|
||||
}
|
||||
is(-e $root->path('/var/lib/xcat/xcatd-systemd-mode') ? 1 : 0,
|
||||
$action eq 'purge' ? 0 : 1, 'systemd marker retention');
|
||||
for my $file (qw(state xcatd)) {
|
||||
is(-e $root->path("$state/$file") ? 1 : 0, $action eq 'purge' ? 0 : 1, "$file retention");
|
||||
}
|
||||
for my $file (qw(pending-xcatd pending-deleted pending-enabled pending-xcatd.tmp.123)) {
|
||||
is(-e $root->path("$state/$file") ? 1 : 0, $action eq 'purge' || $action eq 'abort-install' ? 0 : 1, "$file retention");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
subtest 'abort-install removes an empty state directory' => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=14.04\n");
|
||||
my $state = '/var/lib/xcat/xcatd-init-state';
|
||||
for my $file (qw(context context.tmp.123 pending-xcatd pending-enabled pending-deleted pending-xcatd.tmp.123)) {
|
||||
$root->write("$state/$file", 'aborted attempt');
|
||||
}
|
||||
succeeds('abort before durable state exists', $root->deb_script($deb, 'postrm', 'abort-install'));
|
||||
ok(!-e $root->path($state), 'abort-install removes the empty state directory');
|
||||
};
|
||||
|
||||
subtest 'preinst failure preserves durable state' => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=14.04\n");
|
||||
$root->write('/var/lib/xcat/xcatd-init-state/state', 'durable state');
|
||||
$root->write('/test-bin/mv', "#!/bin/sh\nexit 17\n", 0755);
|
||||
my ($status, $out, $err) = $root->deb_script($deb, 'preinst', 'install');
|
||||
isnt($status, 0, 'context rename failure fails the transaction');
|
||||
is($root->read('/var/lib/xcat/xcatd-init-state/state'), 'durable state', 'failure retains prior durable state');
|
||||
my @temporary = glob $root->path('/var/lib/xcat/xcatd-init-state/context.tmp.*');
|
||||
is(scalar @temporary, 0, 'failure removes context temporary');
|
||||
};
|
||||
|
||||
for my $command (qw(cp mv)) {
|
||||
subtest "preinst cleans up after $command failure" => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=24.04\n");
|
||||
$root->write('/etc/init.d/xcatd', $template, 0750);
|
||||
my $partial = $command eq 'cp' ? 'printf partial > "$destination"' : ':';
|
||||
$root->write("/test-bin/$command", "#!/bin/sh\n" .
|
||||
'for destination; do :; done' . "\n" .
|
||||
'case "$destination" in /var/lib/xcat/xcatd-init-state/pending-xcatd*)' . "\n" .
|
||||
"$partial\nexit 17;;\nesac\nexec /host-usr/bin/$command \"\$\@\"\n", 0755);
|
||||
my ($status) = $root->deb_script($deb, 'preinst', 'upgrade', '2.18.0');
|
||||
isnt($status, 0, 'failed pending capture stops preinst');
|
||||
my @pending = glob $root->path('/var/lib/xcat/xcatd-init-state/pending-*');
|
||||
is_deeply(\@pending, [], 'failed pending capture removes partial evidence');
|
||||
is($root->read('/etc/init.d/xcatd'), $template, 'failed capture preserves administrator bytes');
|
||||
is((stat($root->path('/etc/init.d/xcatd')))[2] & 07777, 0750, 'failed capture preserves administrator mode');
|
||||
};
|
||||
}
|
||||
|
||||
for my $helper (
|
||||
['unsupported', "exit 2", 0],
|
||||
['command-aware', undef, 0],
|
||||
['malformed', "printf '%s\\n' malformed", 1],
|
||||
['failure', "exit 7", 7],
|
||||
) {
|
||||
subtest "preinst with $helper->[0] installed helper" => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=14.04\n");
|
||||
$root->write('/etc/init.d/xcatd', $template, 0755);
|
||||
make_path($root->path('/etc/rc.d/rc3.d'));
|
||||
symlink('/etc/init.d/xcatd', $root->path('/etc/rc.d/rc3.d/S85xcatd')) or die "symlink: $!";
|
||||
$root->write('/opt/xcat/share/xcat/scripts/xcatd-init-compat',
|
||||
defined $helper->[1] ? "#!/bin/sh\n$helper->[1]\n" : slurp_repo_file('xCAT-server/share/xcat/scripts/xcatd-init-compat'), 0755);
|
||||
my ($status, $out, $err) = $root->deb_script($deb, 'preinst', 'upgrade', '2.0');
|
||||
is($status, $helper->[2], 'complete preinst propagates the detector result') or diag "$out$err";
|
||||
if ($status == 0) {
|
||||
is($root->read('/var/lib/xcat/xcatd-init-state/pending-enabled'), "unregistered\n", 'RPM-only links do not enable a Debian service');
|
||||
is($root->read('/var/lib/xcat/xcatd-init-state/pending-xcatd'), $template, 'pre-unpack capture keeps the conffile');
|
||||
is((stat($root->path('/var/lib/xcat/xcatd-init-state/context')))[2] & 07777, 0600, 'context is private');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for my $suffix (qw(dpkg-bak dpkg-backup dpkg-remove)) {
|
||||
subtest "preinst recovers $suffix" => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=\"24.04\"\n");
|
||||
$root->write("/etc/init.d/xcatd.$suffix", "administrator recovery\n", 0750);
|
||||
succeeds('recover interrupted conffile transition', $root->deb_script($deb, 'preinst', 'upgrade', '2.18.0'));
|
||||
my $pending = '/var/lib/xcat/xcatd-init-state/pending-xcatd';
|
||||
ok(-f $root->path($pending), 'recovery stages the conffile backup');
|
||||
if (-f $root->path($pending)) {
|
||||
is($root->read($pending), "administrator recovery\n", 'recovery preserves backup bytes');
|
||||
is((stat($root->path($pending)))[2] & 07777, 0750, 'recovery preserves backup mode');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
subtest 'obsolete conffile is package omission' => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=\"24.04\"\n");
|
||||
$root->write('/var/lib/dpkg/status', $root->read('/var/lib/dpkg/status') .
|
||||
"\nPackage: xcat-server\nStatus: install ok installed\nArchitecture: all\nVersion: 2.18.0\n" .
|
||||
"Maintainer: xCAT <xcat-user\@lists.sourceforge.net>\nDescription: obsolete conffile fixture\n" .
|
||||
"Conffiles:\n /etc/init.d/xcatd 8797eef6731719e0eacff59612b3e916 obsolete\n\n");
|
||||
succeeds('capture obsolete conffile metadata', $root->deb_script($deb, 'preinst', 'upgrade', '2.18.0'));
|
||||
ok(-f $root->path('/var/lib/xcat/xcatd-systemd-mode'), 'obsolete conffile records package omission');
|
||||
ok(!-e $root->path('/var/lib/xcat/xcatd-init-state/pending-deleted'), 'package omission is not an administrator deletion');
|
||||
};
|
||||
|
||||
subtest 'invalid postinst context fails closed' => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=\"24.04\"\n");
|
||||
succeeds('install before invalid-context retry', $root->install_deb($deb));
|
||||
my $state = '/var/lib/xcat/xcatd-init-state';
|
||||
my $before = $root->read("$state/state");
|
||||
$root->write("$state/context", "invalid\n");
|
||||
my ($status, $out, $err) = $root->deb_script($deb, 'postinst', 'configure', $version);
|
||||
isnt($status, 0, 'invalid transaction context fails configuration');
|
||||
like($err, qr/Invalid xcatd package transition context/, 'failure identifies the invalid context');
|
||||
is($root->read("$state/state"), $before, 'invalid context leaves durable state unchanged');
|
||||
};
|
||||
|
||||
subtest 'failed configuration can be retried' => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=24.04\n");
|
||||
$root->write('/test-bin/ln', <<'SHELL', 0755);
|
||||
#!/bin/sh
|
||||
for destination; do :; done
|
||||
case "$destination" in /usr/sbin/xcatd) exit 17;; esac
|
||||
exec /host-usr/bin/ln "$@"
|
||||
SHELL
|
||||
my ($status) = $root->install_deb($deb);
|
||||
isnt($status, 0, 'late configuration failure stops the transaction');
|
||||
my $context = '/var/lib/xcat/xcatd-init-state/context';
|
||||
ok(-f $root->path($context), 'late failure retains transaction context');
|
||||
is($root->read($context), "fresh\n", 'retry retains the original transition') if -f $root->path($context);
|
||||
unlink $root->path('/test-bin/ln') or die "unlink: $!";
|
||||
succeeds('retry package configuration', $root->run('dpkg', '--force-depends', '--configure', 'xcat-server'));
|
||||
is(readlink($root->path('/usr/sbin/xcatd')), '/opt/xcat/sbin/xcatd', 'retry creates the daemon link');
|
||||
ok(!-e $root->path($context), 'successful retry clears transaction context');
|
||||
};
|
||||
|
||||
subtest 'preinst discards stale pending evidence' => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=\"24.04\"\n");
|
||||
my $state = '/var/lib/xcat/xcatd-init-state';
|
||||
for my $file (qw(pending-xcatd pending-enabled pending-deleted pending-xcatd.tmp.123)) {
|
||||
$root->write("$state/$file", "stale evidence\n");
|
||||
}
|
||||
succeeds('begin installation without installed conffile metadata', $root->deb_script($deb, 'preinst', 'install'));
|
||||
my @pending = glob $root->path("$state/pending-*");
|
||||
is_deeply(\@pending, [], 'preinst discards pending evidence without installed ownership');
|
||||
};
|
||||
|
||||
for my $mode (qw(systemd legacy)) {
|
||||
for my $proc (0, 1) {
|
||||
for my $available ($mode eq 'systemd' ? (0, 1) : (0)) {
|
||||
subtest "$mode service dispatch proc=$proc manager=$available" => sub {
|
||||
my $root = XCAT::Test::Lifecycle->new;
|
||||
$root->{live} = $proc;
|
||||
$root->write('/etc/os-release', "ID=ubuntu\nVERSION_ID=\"" . ($mode eq 'systemd' ? '24.04' : '14.04') . "\"\n");
|
||||
make_path($root->path('/run/systemd/system')) if $available;
|
||||
$root->record_command('/usr/sbin/systemctl');
|
||||
$root->record_command('/test-bin/update-rc.d');
|
||||
for my $event (qw(install upgrade)) {
|
||||
$root->write('/calls', '');
|
||||
succeeds("$event dispatch transaction", $root->install_deb($deb));
|
||||
my @reloads = $root->read('/calls') =~ /^(systemctl <daemon-reload>)$/mg;
|
||||
is_deeply(\@reloads, $available ? ['systemctl <daemon-reload>'] : [],
|
||||
"$event reloads service definitions only with systemd available");
|
||||
}
|
||||
$root->record_command('/etc/init.d/xcatd') if $mode eq 'legacy';
|
||||
$root->write('/calls', '');
|
||||
succeeds('remove dispatch transaction', $root->run('dpkg', '--force-depends', '--remove', 'xcat-server'));
|
||||
my @stops = $root->read('/calls') =~ /^((?:systemctl <stop> <xcatd.service>|xcatd <stop>))$/mg;
|
||||
my $stop = $mode eq 'systemd' ? 'systemctl <stop> <xcatd.service>' : 'xcatd <stop>';
|
||||
is_deeply(\@stops, $proc && ($available || $mode eq 'legacy') ? [$stop] : [],
|
||||
'removal stops the available service only with proc');
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Digest::MD5 qw(md5_hex);
|
||||
use File::Spec;
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
|
||||
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
open( my $fh, '<', $path ) or die "Unable to read $path: $!";
|
||||
binmode($fh);
|
||||
my $contents = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
return $contents;
|
||||
}
|
||||
|
||||
my $deb_install = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'install' )
|
||||
);
|
||||
my $dpkg_managed =
|
||||
$deb_install =~ qr{^etc/init\.d/xcatd etc/init\.d$}m;
|
||||
my $ucf_managed =
|
||||
$deb_install =~ qr{^etc/init\.d/xcatd opt/xcat/share/xcat/scripts$}m;
|
||||
|
||||
ok( $dpkg_managed || $ucf_managed,
|
||||
'Debian selects a configuration-aware legacy init backend' );
|
||||
|
||||
if ($dpkg_managed) {
|
||||
pass('dpkg owns the directly packaged legacy init conffile');
|
||||
done_testing();
|
||||
exit;
|
||||
}
|
||||
|
||||
my $deb_control = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'control' )
|
||||
);
|
||||
like( $deb_control, qr{^Depends:.*(?:,\s+|^)ucf(?:,|$)}m,
|
||||
'the generated-conffile backend depends on ucf' );
|
||||
like( $deb_install,
|
||||
qr{^debian/xcatd\.md5sum opt/xcat/share/xcat/scripts$}m,
|
||||
'historical template checksums are installed beside the template' );
|
||||
|
||||
my $sum_path = File::Spec->catfile(
|
||||
$repo_root, 'xCAT-server', 'debian', 'xcatd.md5sum'
|
||||
);
|
||||
ok( -f $sum_path, 'the Debian package carries historical template checksums' );
|
||||
my $sums = -f $sum_path ? read_file($sum_path) : '';
|
||||
like( $sums, qr{^0b1eea60994ff79faa9a8d0bcd53c558\s+2\.17\.0$}m,
|
||||
'the last pre-transition release is recognized as unmodified' );
|
||||
|
||||
my $template = read_file(
|
||||
File::Spec->catfile(
|
||||
$repo_root, 'xCAT-server', 'etc', 'init.d', 'xcatd'
|
||||
)
|
||||
);
|
||||
like( $sums, qr{^\Q@{[ md5_hex($template) ]}\E\s+2\.18\.0$}m,
|
||||
'the current released template is recognized as unmodified' );
|
||||
|
||||
my $postinst = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'postinst' )
|
||||
);
|
||||
like( $postinst,
|
||||
qr{"\$xcatd_init_state" configure-legacy "\$xcatd_transition_context"},
|
||||
'postinst delegates legacy conffile transitions to the state helper' );
|
||||
|
||||
my $state_helper = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'xcatd-init-state' )
|
||||
);
|
||||
like( $state_helper, qr{ucf "\$legacy_template" "\$legacy_init"},
|
||||
'legacy configuration delegates updates to ucf' );
|
||||
like( $state_helper, qr{ucfr xcat-server "\$legacy_init"},
|
||||
'legacy configuration registers ucf ownership' );
|
||||
like( $state_helper, qr{state_content=stashed.*?stash_candidate}s,
|
||||
'a present legacy script is represented by an exact stash' );
|
||||
like( $state_helper, qr{state_content=deleted},
|
||||
'administrator deletion has a distinct persistent state' );
|
||||
like( $state_helper,
|
||||
qr{package-default\).*?UCF_FORCE_CONFFMISS=1 ucf}s,
|
||||
'only known package omission forces recreation of a missing script' );
|
||||
like( $state_helper,
|
||||
qr{state_content=unknown.*?preserving its absence}s,
|
||||
'ambiguous markerless upgrades preserve absence instead of guessing' );
|
||||
|
||||
my $postrm = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'postrm' )
|
||||
);
|
||||
like( $postrm,
|
||||
qr{xcatd\.ucf-old.*?xcatd\.ucf-new.*?xcatd\.ucf-dist}s,
|
||||
'purge removes ucf backup artifacts' );
|
||||
like( $postrm, qr{ucf --purge /etc/init\.d/xcatd},
|
||||
'purge removes ucf checksum state' );
|
||||
like( $postrm, qr{ucfr --purge xcat-server /etc/init\.d/xcatd},
|
||||
'purge removes the ucf ownership registration' );
|
||||
like( $postrm,
|
||||
qr{xcatd_state_dir=/var/lib/xcat/xcatd-init-state.*?(?:/var/lib/xcat/xcatd-init-state/state|"\$xcatd_state_dir/state")}s,
|
||||
'purge removes the persistent init transition state' );
|
||||
|
||||
done_testing();
|
||||
@@ -1,339 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
|
||||
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
open( my $fh, '<', $path ) or die "Unable to read $path: $!";
|
||||
my $contents = do { local $/; <$fh> };
|
||||
close($fh);
|
||||
return $contents;
|
||||
}
|
||||
|
||||
sub init_classifier_block {
|
||||
my ($script) = @_;
|
||||
return $1
|
||||
if $script =~
|
||||
/(xcat_systemd_os_release_status\(\).*?\n}\n\nxcat_target_uses_systemd\(\).*?\n})\n\n/s;
|
||||
return '';
|
||||
}
|
||||
|
||||
sub pending_cleanup_block {
|
||||
my ($script) = @_;
|
||||
return $1
|
||||
if $script =~ /(clear_xcatd_pending_state\(\)\n\{.*?\n\})\n/s;
|
||||
return '';
|
||||
}
|
||||
|
||||
sub legacy_state_block {
|
||||
my ($script) = @_;
|
||||
return $1
|
||||
if $script =~
|
||||
/(xcatd_legacy_enable_state\(\)\n\{.*?\n\})\n\nif xcat_target_uses_systemd/s;
|
||||
return '';
|
||||
}
|
||||
|
||||
sub run_legacy_state_block {
|
||||
my ( $block, $helper_source ) = @_;
|
||||
my $root = tempdir( CLEANUP => 1 );
|
||||
my $helper = File::Spec->catfile( $root, 'xcatd-init-compat' );
|
||||
open( my $helper_fh, '>', $helper )
|
||||
or die "Unable to stage compatibility helper: $!";
|
||||
print {$helper_fh} $helper_source;
|
||||
close($helper_fh);
|
||||
chmod 0755, $helper;
|
||||
|
||||
my $rpm_runlevel =
|
||||
File::Spec->catdir( $root, 'etc', 'rc.d', 'rc3.d' );
|
||||
make_path($rpm_runlevel);
|
||||
symlink( '/etc/init.d/xcatd',
|
||||
File::Spec->catfile( $rpm_runlevel, 'S85xcatd' ) )
|
||||
or die "Unable to stage RPM-only runlevel link: $!";
|
||||
|
||||
my $isolated_block = $block;
|
||||
$isolated_block =~ s{/etc/rc}{\$fixture_root/etc/rc}g;
|
||||
my $runner = File::Spec->catfile( $root, 'run-detector' );
|
||||
open( my $runner_fh, '>', $runner )
|
||||
or die "Unable to stage detector runner: $!";
|
||||
print {$runner_fh} "#!/bin/sh\nset -e\n";
|
||||
print {$runner_fh} "fixture_root=\${XCAT_TEST_ROOT:?}\n";
|
||||
print {$runner_fh} "xcatd_init_compat=\${XCAT_TEST_HELPER:?}\n";
|
||||
print {$runner_fh} "$isolated_block\n\nxcatd_legacy_enable_state\n";
|
||||
close($runner_fh);
|
||||
chmod 0755, $runner;
|
||||
|
||||
local $ENV{XCAT_TEST_ROOT} = $root;
|
||||
local $ENV{XCAT_TEST_HELPER} = $helper;
|
||||
open( my $pipe, '-|', '/bin/sh', $runner )
|
||||
or die "Unable to run preinst detector: $!";
|
||||
my $output = do { local $/; <$pipe> };
|
||||
close($pipe);
|
||||
my $status = $? >> 8;
|
||||
$output =~ s/\s+\z//;
|
||||
return ( $status, $output );
|
||||
}
|
||||
|
||||
my $deb_install = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'install' )
|
||||
);
|
||||
like( $deb_install, qr{^etc/init\.d/xcatd opt/xcat/share/xcat/scripts$}m,
|
||||
'Debian stages the legacy script as a compatibility template' );
|
||||
unlike( $deb_install, qr{^etc/init\.d/xcatd etc/init\.d$}m,
|
||||
'Debian does not install the legacy script directly' );
|
||||
|
||||
my $deb_dirs = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'dirs' )
|
||||
);
|
||||
unlike( $deb_dirs, qr{^etc/init\.d/?$}m,
|
||||
'Debian does not package an empty legacy init directory' );
|
||||
|
||||
foreach my $maintainer_script (qw(preinst postinst postrm)) {
|
||||
my $script = read_file(
|
||||
File::Spec->catfile(
|
||||
$repo_root, 'xCAT-server', 'debian', $maintainer_script
|
||||
)
|
||||
);
|
||||
like( $script,
|
||||
qr{dpkg-maintscript-helper rm_conffile /etc/init\.d/xcatd -- "\$\@"},
|
||||
"Debian $maintainer_script participates in graceful conffile removal" );
|
||||
}
|
||||
|
||||
my $deb_postinst = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'postinst' )
|
||||
);
|
||||
like( $deb_postinst,
|
||||
qr{if "\$xcatd_init_compat" uses-systemd --explicit-target; then},
|
||||
'Debian postinst delegates target init detection to the packaged helper' );
|
||||
unlike( $deb_postinst,
|
||||
qr{"\$xcatd_init_compat" (?:uses-systemd|configure)(?! --explicit-target)},
|
||||
'every Debian postinst helper call uses explicit target detection' );
|
||||
unlike( $deb_postinst, qr{sub xcat_target_uses_systemd|xcat_target_uses_systemd\(\)},
|
||||
'Debian postinst does not carry weaker duplicate init detection' );
|
||||
my $deb_init_state = read_file(
|
||||
File::Spec->catfile(
|
||||
$repo_root, 'xCAT-server', 'debian', 'xcatd-init-state'
|
||||
)
|
||||
);
|
||||
like( $deb_init_state,
|
||||
qr{\[ -x "\$legacy_init" \].*?update-rc\.d xcatd defaults}s,
|
||||
'Debian registers SysV only after materializing an executable script' );
|
||||
like( $deb_init_state,
|
||||
qr{apply_legacy_registration\(\).*?no\).*?update-rc\.d xcatd defaults.*?update-rc\.d xcatd disable.*?unregistered\|unknown\).*?update-rc\.d -f xcatd remove}s,
|
||||
'Debian distinguishes registered-disabled from unregistered SysV state' );
|
||||
like( $deb_init_state,
|
||||
qr{stash_candidate\(\).*?if ! cp -a.*?\|\|\s*! mv -f.*?rm -f "\$stash_tmp".*?return 1}s,
|
||||
'Debian removes a failed atomic stash temporary' );
|
||||
like( $deb_init_state, qr{printf 'format=2},
|
||||
'Debian writes registration-aware state format 2' );
|
||||
like( $deb_init_state,
|
||||
qr{state_format" = 1.*?state_enabled" = no.*?state_enabled=unregistered}s,
|
||||
'Debian preserves format-1 remove-all-links behavior' );
|
||||
my @unregistered_refresh_guards =
|
||||
$deb_init_state =~ /state_enabled" != unregistered/g;
|
||||
is( scalar @unregistered_refresh_guards, 2,
|
||||
'both systemd refresh paths retain unregistered provenance' );
|
||||
like( $deb_init_state,
|
||||
qr{init_compat=.*?xcatd-init-compat.*?shared_init_state\(\).*?"\$init_compat" "\$\@".*?debian-legacy-state.*?systemd-state --allow-unknown}s,
|
||||
'Debian delegates legacy and precise systemd state detection to the shared helper' );
|
||||
like( $deb_init_state,
|
||||
qr{if ! detected_legacy_state=.*?return 1.*?if ! detected_systemd_state=.*?return 1}s,
|
||||
'Debian propagates failures from both shared state probes' );
|
||||
unlike( $deb_init_state,
|
||||
qr{systemctl (?:--root=.*? )?is-enabled|systemd/system/\*\.(?:wants|requires)/xcatd\.service|rc\?\.d/S\?\?xcatd},
|
||||
'Debian init state does not duplicate shared service-state probes' );
|
||||
unlike( $deb_init_state, qr{systemd_is_masked},
|
||||
'Debian does not hide detector failures in a boolean mask probe' );
|
||||
like( $deb_postinst,
|
||||
qr{transition_origin=.*?get origin.*?fresh:yes\|legacy:yes.*?systemctl enable}s,
|
||||
'Debian changes systemd enablement only for fresh installs and init transitions' );
|
||||
like( $deb_postinst,
|
||||
qr{legacy:no\|legacy:unregistered\) systemctl disable xcatd\.service},
|
||||
'Debian maps both disabled SysV registration states to systemd disablement' );
|
||||
unlike( $deb_postinst, qr{systemd:yes.*?systemctl enable}s,
|
||||
'Debian same-systemd upgrades do not normalize existing enablement' );
|
||||
|
||||
my %classifier_blocks;
|
||||
foreach my $fallback_script (qw(preinst postrm)) {
|
||||
my $script = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', $fallback_script )
|
||||
);
|
||||
$classifier_blocks{$fallback_script} = init_classifier_block($script);
|
||||
ok( $classifier_blocks{$fallback_script} ne '',
|
||||
"Debian $fallback_script classifier can be extracted" );
|
||||
like( $script, qr{VERSION_ID.*?ubuntu.*?-ge 15.*?debian.*?-ge 8}s,
|
||||
"Debian $fallback_script recognizes stripped modern targets" );
|
||||
like( $script, qr{\[ "\$os_id" = "debian" \].*?\[ -z "\$os_version" \]}s,
|
||||
"Debian $fallback_script recognizes testing and sid without VERSION_ID" );
|
||||
like( $script, qr{value=\$\{value#\\'\}\s+value=\$\{value%\\'\}}s,
|
||||
"Debian $fallback_script strips single-quoted os-release values" );
|
||||
like( $script,
|
||||
qr{xcat_target_uses_systemd\(\).*?\[ -d /run/systemd/system \].*?\[ -L /sbin/init \].*?\*\) return 1 ;;.*?\[ -e /sbin/init \].*?return 1.*?if xcat_systemd_os_release_status; then.*?\[ -x /usr/lib/systemd/systemd \]}s,
|
||||
"Debian $fallback_script prefers active and explicit target evidence" );
|
||||
like( $script,
|
||||
qr{xcat_systemd_os_release_status\(\).*?return 2.*?return 1.*?return 2}s,
|
||||
"Debian $fallback_script distinguishes legacy from unknown releases" );
|
||||
}
|
||||
my $deb_preinst = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'preinst' )
|
||||
);
|
||||
my $preinst_legacy_state_block = legacy_state_block($deb_preinst);
|
||||
ok( $preinst_legacy_state_block ne '',
|
||||
'preinst legacy registration detector can be extracted' );
|
||||
is_deeply(
|
||||
[ run_legacy_state_block( $preinst_legacy_state_block, <<'SH' ) ],
|
||||
#!/bin/sh
|
||||
root=${XCAT_TEST_ROOT:?}
|
||||
case "${1:-}" in
|
||||
legacy-state)
|
||||
for link in "$root"/etc/rc.d/rc?.d/S??xcatd; do
|
||||
if [ -e "$link" ] || [ -L "$link" ]; then
|
||||
printf '%s\n' enabled
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
printf '%s\n' unregistered
|
||||
;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
SH
|
||||
[ 0, 'unregistered' ],
|
||||
'preinst falls back to Debian links when the installed helper lacks the command'
|
||||
);
|
||||
is_deeply(
|
||||
[ run_legacy_state_block( $preinst_legacy_state_block, <<'SH' ) ],
|
||||
#!/bin/sh
|
||||
root=${XCAT_TEST_ROOT:?}
|
||||
case "${1:-}:$#" in
|
||||
debian-legacy-state:1)
|
||||
for link in "$root"/etc/rc?.d/S??xcatd; do
|
||||
if [ -e "$link" ] || [ -L "$link" ]; then
|
||||
printf '%s\n' enabled
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
printf '%s\n' unregistered
|
||||
;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
SH
|
||||
[ 0, 'unregistered' ],
|
||||
'preinst uses Debian-only state from a command-aware installed helper'
|
||||
);
|
||||
my ( $malformed_helper_status, $malformed_helper_output ) =
|
||||
run_legacy_state_block( $preinst_legacy_state_block, <<'SH' );
|
||||
#!/bin/sh
|
||||
[ "${1:-}" = debian-legacy-state ] || exit 2
|
||||
printf '%s\n' malformed
|
||||
SH
|
||||
is( $malformed_helper_status, 1,
|
||||
'preinst rejects malformed successful helper output' );
|
||||
is( $malformed_helper_output, '',
|
||||
'preinst does not promote malformed helper output' );
|
||||
my ( $failed_helper_status, $failed_helper_output ) =
|
||||
run_legacy_state_block( $preinst_legacy_state_block, <<'SH' );
|
||||
#!/bin/sh
|
||||
exit 7
|
||||
SH
|
||||
is( $failed_helper_status, 7,
|
||||
'preinst fails closed on non-capability helper errors' );
|
||||
is( $failed_helper_output, '',
|
||||
'preinst does not fall back after a real helper failure' );
|
||||
like( $deb_preinst,
|
||||
qr{write_xcatd_context\(\)\s*\(.*?umask 077.*?\n\)}s,
|
||||
'preinst confines the context-file umask to a subshell' );
|
||||
like( $deb_preinst,
|
||||
qr{context_tmp=.*?if ! printf.*?\|\|\s*! mv -f.*?rm -f "\$context_tmp".*?return 1}s,
|
||||
'preinst removes a failed context-file temporary' );
|
||||
my $preinst_pending_cleanup = pending_cleanup_block($deb_preinst);
|
||||
like( $preinst_pending_cleanup,
|
||||
qr{pending-xcatd.*?pending-deleted.*?pending-enabled.*?pending-xcatd\.tmp\.}s,
|
||||
'preinst clears every transaction-local pending state artifact' );
|
||||
like( $deb_preinst,
|
||||
qr{conffile_record=.*?dpkg-query.*?case "\$conffile_record" in.*?obsolete.*?touch "\$xcatd_old_systemd_marker".*?\*\).*?touch "\$xcatd_state_dir/pending-deleted"}s,
|
||||
'preinst distinguishes package-obsolete conffiles from administrator deletion' );
|
||||
like( $deb_preinst,
|
||||
qr{xcatd_transition_context=\s*case "\$1" in\s*upgrade\) xcatd_transition_context=upgrade ;;\s*install\) xcatd_transition_context=fresh ;;\s*esac\s*if \[ -n "\$xcatd_transition_context" \]}s,
|
||||
'preinst prepares transition state only for install and upgrade calls' );
|
||||
like( $deb_preinst,
|
||||
qr{\$xcatd_legacy_init\.dpkg-bak.*?\$xcatd_legacy_init\.dpkg-backup.*?\$xcatd_legacy_init\.dpkg-remove}s,
|
||||
'preinst recovers every conffile backup name used during interrupted removal' );
|
||||
like( $deb_preinst,
|
||||
qr{pending_tmp=.*?if ! cp -a.*?\|\|\s*! mv -f.*?rm -f "\$pending_tmp".*?exit 1}s,
|
||||
'preinst removes a failed pending-stash temporary' );
|
||||
like( $deb_preinst,
|
||||
qr{if \[ -n "\$conffile_record" \]; then.*?case "\$conffile_record" in.*?esac\s*else\s*# A retry without an installed conffile record.*?clear_xcatd_pending_state\s*fi}s,
|
||||
'preinst discards stale pending evidence when dpkg has no conffile record' );
|
||||
like( $deb_preinst,
|
||||
qr{xcatd_legacy_enable_state\(\).*?"\$xcatd_init_compat" debian-legacy-state 2>/dev/null.*?detector_status=\$\?.*?"\$detector_status" -ne 2.*?/etc/rc\?\.d/S\?\?xcatd.*?/etc/rc\?\.d/K\?\?xcatd.*?unregistered}s,
|
||||
'preinst reuses shared registration detection with a pre-unpack S/K fallback' );
|
||||
like( $deb_preinst,
|
||||
qr{upgrade\)\s*write_xcatd_context upgrade.*?install\)\s*write_xcatd_context fresh.*?abort-upgrade\)\s*rm -f "\$xcatd_context_file"}s,
|
||||
'preinst records explicit durable package transition context' );
|
||||
like( $deb_postinst,
|
||||
qr{ln -sf /opt/xcat/sbin/xcatd /usr/sbin/xcatd.*?#DEBHELPER#.*?rm -f "\$xcatd_context_file"}s,
|
||||
'postinst retains upgrade context until every fallible installation step succeeds' );
|
||||
like( $deb_postinst,
|
||||
qr{if \[ -r "\$xcatd_context_file" \].*?fresh\|upgrade.*?elif \[ "\$1" = configure \] && \[ -n "\$\{2:-\}" \].*?xcatd_transition_context=upgrade}s,
|
||||
'postinst validates explicit context and preserves state on configured-package fallback' );
|
||||
unlike( "$deb_preinst\n$deb_postinst", qr{/tmp/xCAT-server_upgrade\.tmp},
|
||||
'maintainer scripts keep transaction state out of shared temporary storage' );
|
||||
is( $classifier_blocks{preinst}, $classifier_blocks{postrm},
|
||||
'Debian preinst and postrm carry the same lifecycle-safe classifier' );
|
||||
|
||||
my $deb_postrm = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'postrm' )
|
||||
);
|
||||
my $postrm_pending_cleanup = pending_cleanup_block($deb_postrm);
|
||||
is( $postrm_pending_cleanup, $preinst_pending_cleanup,
|
||||
'preinst and postrm share the same pending-state cleanup contract' );
|
||||
unlike( $deb_postrm, qr{/tmp/xCAT-server_upgrade\.tmp},
|
||||
'postrm keeps transaction state out of shared temporary storage' );
|
||||
my ($abort_install) = $deb_postrm =~ /\n\s*abort-install\)\n(.*?)\n\s*;;/s;
|
||||
my ($abort_upgrade) =
|
||||
$deb_postrm =~ /\n\s*abort-upgrade\|disappear\)\n(.*?)\n\s*;;/s;
|
||||
ok( defined($abort_install) && defined($abort_upgrade),
|
||||
'postrm abort recovery arms can be inspected independently' );
|
||||
like( $abort_install,
|
||||
qr{clear_xcatd_pending_state.*?rm -f "\$xcatd_context_file".*?rmdir "\$xcatd_state_dir"}s,
|
||||
'abort-install clears pending context and removes only an empty state directory' );
|
||||
unlike( $abort_install,
|
||||
qr{\$xcatd_state_dir/(?:state|xcatd)|xcatd_old_systemd_marker},
|
||||
'abort-install preserves durable init state and compatibility evidence' );
|
||||
like( $abort_upgrade, qr{rm -f "\$xcatd_context_file"},
|
||||
'abort-upgrade clears only the completed-attempt context' );
|
||||
unlike( $abort_upgrade,
|
||||
qr{clear_xcatd_pending_state|pending-(?:xcatd|deleted|enabled)},
|
||||
'abort-upgrade preserves pending recovery evidence for retry' );
|
||||
like( $deb_postrm,
|
||||
qr{purge\).*?rm -f "\$xcatd_context_file".*?upgrade\|failed-upgrade\)\s*;;.*?remove\).*?rm -f "\$xcatd_context_file"}s,
|
||||
'postrm clears upgrade context only when installation cannot continue' );
|
||||
|
||||
my $deb_prerm = read_file(
|
||||
File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'prerm' )
|
||||
);
|
||||
like( $deb_prerm,
|
||||
qr{if "\$xcatd_init_compat" uses-systemd --explicit-target; then.*?update-rc\.d -f xcatd remove}s,
|
||||
'Debian prerm removes registration through the detected init implementation' );
|
||||
unlike( $deb_prerm,
|
||||
qr{"\$xcatd_init_compat" uses-systemd(?! --explicit-target)},
|
||||
'every Debian prerm helper call uses explicit target detection' );
|
||||
unlike( $deb_postinst, qr{xcat_can_use_systemctl},
|
||||
'Debian postinst does not duplicate the shared systemctl readiness guard' );
|
||||
like( $deb_postinst,
|
||||
qr{if "\$xcatd_init_compat" can-use-systemctl; then\s+systemctl daemon-reload}s,
|
||||
'Debian postinst delegates its active-systemd guard to the shared helper' );
|
||||
unlike( $deb_prerm, qr{xcat_can_use_systemctl},
|
||||
'Debian prerm does not duplicate the shared systemctl readiness guard' );
|
||||
like( $deb_prerm,
|
||||
qr{if "\$xcatd_init_compat" can-use-systemctl; then\s+systemctl stop xcatd\.service}s,
|
||||
'Debian prerm delegates its active-systemd guard to the shared helper' );
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user