From e83ddfe0d9c90365cdcd198035a5813eedc1f0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:22:46 -0300 Subject: [PATCH] test(packaging): exercise Debian service lifecycle --- .github/workflows/xcat_test.yml | 6 + xCAT-test/lib/XCAT/Test/Lifecycle.pm | 200 +++++++++++ xCAT-test/native/xcatd_debian_lifecycle.t | 339 ++++++++++++++++++ .../unit/xcatd_debian_conffile_lifecycle.t | 102 ------ .../unit/xcatd_debian_service_packaging.t | 339 ------------------ 5 files changed, 545 insertions(+), 441 deletions(-) create mode 100644 xCAT-test/lib/XCAT/Test/Lifecycle.pm create mode 100644 xCAT-test/native/xcatd_debian_lifecycle.t delete mode 100644 xCAT-test/unit/xcatd_debian_conffile_lifecycle.t delete mode 100644 xCAT-test/unit/xcatd_debian_service_packaging.t diff --git a/.github/workflows/xcat_test.yml b/.github/workflows/xcat_test.yml index d3413d8d3..90642a8aa 100644 --- a/.github/workflows/xcat_test.yml +++ b/.github/workflows/xcat_test.yml @@ -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 diff --git a/xCAT-test/lib/XCAT/Test/Lifecycle.pm b/xCAT-test/lib/XCAT/Test/Lifecycle.pm new file mode 100644 index 000000000..8aaa597f1 --- /dev/null +++ b/xCAT-test/lib/XCAT/Test/Lifecycle.pm @@ -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; diff --git a/xCAT-test/native/xcatd_debian_lifecycle.t b/xCAT-test/native/xcatd_debian_lifecycle.t new file mode 100644 index 000000000..8b6bb2f6c --- /dev/null +++ b/xCAT-test/native/xcatd_debian_lifecycle.t @@ -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> \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 \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 $/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 \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 )$/mg; + is_deeply(\@reloads, $available ? ['systemctl '] : [], + "$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 |xcatd ))$/mg; + my $stop = $mode eq 'systemd' ? 'systemctl ' : 'xcatd '; + is_deeply(\@stops, $proc && ($available || $mode eq 'legacy') ? [$stop] : [], + 'removal stops the available service only with proc'); + }; + } + } +} + +done_testing(); diff --git a/xCAT-test/unit/xcatd_debian_conffile_lifecycle.t b/xCAT-test/unit/xcatd_debian_conffile_lifecycle.t deleted file mode 100644 index 5a242b475..000000000 --- a/xCAT-test/unit/xcatd_debian_conffile_lifecycle.t +++ /dev/null @@ -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(); diff --git a/xCAT-test/unit/xcatd_debian_service_packaging.t b/xCAT-test/unit/xcatd_debian_service_packaging.t deleted file mode 100644 index ced13a8a2..000000000 --- a/xCAT-test/unit/xcatd_debian_service_packaging.t +++ /dev/null @@ -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();