2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-05 20:47:55 +00:00
Files
xcat-core/xCAT-test/unit/xcatd_debian_service_packaging.t
T
Vinícius Ferrão 036bb24c87 fix(packaging): preserve xcatd init state on Debian (#7615)
* refactor(packaging): share precise systemd state detection

* test(packaging): cover precise systemd state detection

* test(packaging): allow explicit Debian init mode

* fix(packaging): honor explicit Debian init targets

* test(packaging): cover Debian init target detection

* fix(packaging): add Debian xcatd init state helper

* test(packaging): cover Debian xcatd init state helper

* test(packaging): allow delegated SysV registration

* fix(packaging): preserve Debian xcatd conffile lifecycle

* test(packaging): mirror explicit Debian init mode

* test(packaging): cover Debian xcatd conffile lifecycle

* fix(packaging): contain init state file umask

* test(packaging): cover init state permissions

* fix(packaging): contain preinstall context umask

* test(packaging): cover preinstall umask containment

* fix(packaging): detect all systemd enablement links

* test(packaging): cover all systemd enablement links

* test(packaging): allow shared purge state path

* refactor(packaging): reuse Debian init state path

* fix(packaging): detect runtime systemd masks

* test(packaging): cover runtime systemd masks

* test(packaging): model Debian SysV registration

* refactor(packaging): reuse shared init state detection

* test(packaging): enforce shared Debian state probes

* test(packaging): allow explicit unregistered masks

* fix(packaging): preserve Debian SysV registration state

* test(packaging): cover Debian SysV registration states

* fix(packaging): clean failed Debian state writes

* test(packaging): cover failed Debian state writes

* fix(packaging): retain unregistered systemd provenance

* test(packaging): cover unregistered systemd upgrades

* fix(packaging): fail closed on shared state errors

* test(packaging): cover shared state detector failures

* test(packaging): mirror native xcatd runlevels

* fix(packaging): recover rejected SysV layouts

* test(packaging): cover rejected SysV layouts

* test(packaging): cover SysV rebuild retries

* refactor(packaging): reuse systemctl readiness guard

* test(packaging): enforce shared systemctl guard
2026-07-28 19:31:36 +00:00

340 lines
15 KiB
Perl

#!/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();