diff --git a/xCAT-server/debian/control b/xCAT-server/debian/control index 9e8998bcb..4560b5462 100644 --- a/xCAT-server/debian/control +++ b/xCAT-server/debian/control @@ -8,7 +8,7 @@ Homepage: https://xcat.org/ Package: xcat-server Architecture: all -Depends: ${perl:Depends}, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl +Depends: ${perl:Depends}, grub2-xcat (>= 2.02-0.76.el7.1.snap201905160255), perl-xcat (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libsys-syslog-perl, libio-socket-ssl-perl, libxml-simple-perl, make, ucf, libdbd-sqlite3-perl, libexpect-perl, libnet-dns-perl, libsoap-lite-perl, libxml-libxml-perl, libsnmp-perl, debootstrap, libdigest-sha-perl,libcrypt-rijndael-perl,libcrypt-cbc-perl,libjson-perl, libnet-https-nb-perl, libhttp-async-perl Description: Server and configuration utilities of xCAT xCAT-server provides the core server and configuration management components of xCAT. diff --git a/xCAT-server/debian/install b/xCAT-server/debian/install index 6858f3764..103003e55 100644 --- a/xCAT-server/debian/install +++ b/xCAT-server/debian/install @@ -29,6 +29,8 @@ lib/xcat/Confluent/* opt/xcat/lib/perl/Confluent/ lib/xcat/shfunctions opt/xcat/lib etc/init.d/xcatd opt/xcat/share/xcat/scripts +debian/xcatd.md5sum opt/xcat/share/xcat/scripts +debian/xcatd-init-state opt/xcat/share/xcat/scripts etc/init.d/xcatd.service lib/systemd/system/ xCAT-wsapi/* opt/xcat/ws/ diff --git a/xCAT-server/debian/postinst b/xCAT-server/debian/postinst index f303134c8..3895869ab 100644 --- a/xCAT-server/debian/postinst +++ b/xCAT-server/debian/postinst @@ -5,17 +5,48 @@ set -e -xcat_can_use_systemctl() -{ - [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 -} - xcatd_init_compat=/opt/xcat/share/xcat/scripts/xcatd-init-compat +xcatd_init_state=/opt/xcat/share/xcat/scripts/xcatd-init-state +xcatd_state_dir=/var/lib/xcat/xcatd-init-state +xcatd_context_file=$xcatd_state_dir/context -if "$xcatd_init_compat" uses-systemd && \ - command -v dpkg-maintscript-helper >/dev/null 2>&1 && \ - dpkg-maintscript-helper supports rm_conffile; then - dpkg-maintscript-helper rm_conffile /etc/init.d/xcatd -- "$@" +if [ -L "$xcatd_state_dir" ] || \ + { [ -e "$xcatd_state_dir" ] && [ ! -d "$xcatd_state_dir" ]; }; then + echo "Invalid xcatd init state directory: $xcatd_state_dir" >&2 + exit 1 +fi + +if "$xcatd_init_compat" uses-systemd --explicit-target; then + xcatd_uses_systemd=yes +else + xcatd_uses_systemd=no +fi + +if [ -r "$xcatd_context_file" ]; then + xcatd_transition_context=$(cat "$xcatd_context_file") + case "$xcatd_transition_context" in + fresh|upgrade) ;; + *) + echo "Invalid xcatd package transition context: $xcatd_transition_context" >&2 + exit 1 + ;; + esac +elif [ "$1" = configure ] && [ -n "${2:-}" ]; then + # Preserve state when invoked for an already configured package even if a + # prior packaging tool did not provide the explicit preinst context. + xcatd_transition_context=upgrade +else + xcatd_transition_context=fresh +fi + +if [ "$xcatd_uses_systemd" = yes ]; then + if [ "$1" = configure ]; then + "$xcatd_init_state" prepare-systemd "$xcatd_transition_context" + fi + if command -v dpkg-maintscript-helper >/dev/null 2>&1 && \ + dpkg-maintscript-helper supports rm_conffile; then + dpkg-maintscript-helper rm_conffile /etc/init.d/xcatd -- "$@" + fi fi # summary of how this script can be called: @@ -34,47 +65,37 @@ fi case "$1" in configure) . /etc/profile.d/xcat.sh - xcat_server_upgrade=0 - if [ -f /tmp/xCAT-server_upgrade.tmp ]; then - xcat_server_upgrade=1 - fi - if "$xcatd_init_compat" uses-systemd; then - legacy_xcatd_enabled=0 - if ls /etc/rc?.d/S??xcatd >/dev/null 2>&1; then - legacy_xcatd_enabled=1 - fi - if [ "$xcat_server_upgrade" = "1" ] && command -v update-rc.d >/dev/null 2>&1; then + if [ "$xcatd_uses_systemd" = yes ]; then + if command -v update-rc.d >/dev/null 2>&1; then update-rc.d -f xcatd remove fi - "$xcatd_init_compat" configure - if xcat_can_use_systemctl; then + "$xcatd_init_compat" configure --explicit-target + if "$xcatd_init_compat" can-use-systemctl; then systemctl daemon-reload fi - if command -v systemctl >/dev/null 2>&1 && \ - { [ "$xcat_server_upgrade" = "0" ] || [ "$legacy_xcatd_enabled" = "1" ]; }; then - # Fresh installs are enabled by default. Upgrades preserve the - # legacy enabled state so HA nodes remain disabled. - systemctl enable xcatd.service + desired_enabled=$("$xcatd_init_state" get enabled) + transition_origin=$("$xcatd_init_state" get origin) + if command -v systemctl >/dev/null 2>&1; then + case "$transition_origin:$desired_enabled" in + fresh:yes|legacy:yes) systemctl enable xcatd.service ;; + legacy:no|legacy:unregistered) systemctl disable xcatd.service ;; + esac fi + "$xcatd_init_state" commit-systemd else - "$xcatd_init_compat" configure - if [ "$xcat_server_upgrade" = "0" ] && \ - [ -x /etc/init.d/xcatd ] && \ - command -v update-rc.d >/dev/null 2>&1; then - update-rc.d xcatd defaults - fi + "$xcatd_init_state" configure-legacy "$xcatd_transition_context" fi - if [ "$xcat_server_upgrade" = "1" ]; then - # No need to reload xcatd here as it will be covered by xCAT/xCATsn now. - rm -f /tmp/xCAT-server_upgrade.tmp - fi ln -sf /opt/xcat/sbin/xcatd /usr/sbin/xcatd ;; - abort-upgrade|abort-remove|abort-deconfigure) + abort-upgrade) + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* + ;; + + abort-remove|abort-deconfigure) ;; *) @@ -88,4 +109,10 @@ esac #DEBHELPER# +if [ "$1" = configure ]; then + # Keep explicit context across every fallible postinst step. A retry after + # this point is a completed configuration and may safely drop it. + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* +fi + exit 0 diff --git a/xCAT-server/debian/postrm b/xCAT-server/debian/postrm index 955ded4d0..a537de088 100644 --- a/xCAT-server/debian/postrm +++ b/xCAT-server/debian/postrm @@ -5,13 +5,76 @@ set -e -xcat_target_uses_systemd() +xcatd_state_dir=/var/lib/xcat/xcatd-init-state +xcatd_context_file=$xcatd_state_dir/context + +clear_xcatd_pending_state() { - if [ -d /run/systemd/system ]; then + rm -f "$xcatd_state_dir/pending-xcatd" \ + "$xcatd_state_dir/pending-deleted" \ + "$xcatd_state_dir/pending-enabled" \ + "$xcatd_state_dir/pending-xcatd.tmp."* +} + +if [ -L "$xcatd_state_dir" ] || \ + { [ -e "$xcatd_state_dir" ] && [ ! -d "$xcatd_state_dir" ]; }; then + echo "Invalid xcatd init state directory: $xcatd_state_dir" >&2 + exit 1 +fi + +xcat_systemd_os_release_status() +{ + [ -r /etc/os-release ] || return 2 + + os_id= + os_version= + while IFS='=' read -r key value; do + value=${value#\"} + value=${value%\"} + value=${value#\'} + value=${value%\'} + case "$key" in + ID) os_id=$value ;; + VERSION_ID) os_version=$value ;; + esac + done < /etc/os-release + + if [ "$os_id" = "debian" ] && [ -z "$os_version" ]; then return 0 fi - if [ -x /usr/lib/systemd/systemd ] || [ -x /lib/systemd/systemd ]; then + os_major=${os_version%%.*} + case "$os_major" in + ''|*[!0-9]*) return 2 ;; + esac + + case "$os_id" in + rhel|centos|rocky|almalinux|ol|scientific) + [ "$os_major" -ge 7 ] && return 0 + return 1 + ;; + fedora|ubuntu) + [ "$os_major" -ge 15 ] && return 0 + return 1 + ;; + debian) + [ "$os_major" -ge 8 ] && return 0 + return 1 + ;; + sles|sled|opensuse|opensuse-leap|opensuse-tumbleweed) + [ "$os_major" -ge 12 ] && return 0 + return 1 + ;; + *) + return 2 + ;; + esac + +} + +xcat_target_uses_systemd() +{ + if [ -d /run/systemd/system ]; then return 0 fi @@ -19,35 +82,25 @@ xcat_target_uses_systemd() init_target=$(readlink /sbin/init 2>/dev/null || true) case "$init_target" in *systemd*) return 0 ;; + *) return 1 ;; esac fi - if [ -r /etc/os-release ]; then - os_id= - os_version= - while IFS='=' read -r key value; do - value=${value#\"} - value=${value%\"} - value=${value#\'} - value=${value%\'} - case "$key" in - ID) os_id=$value ;; - VERSION_ID) os_version=$value ;; - esac - done < /etc/os-release + if [ -e /sbin/init ]; then + return 1 + fi - if [ "$os_id" = "debian" ] && [ -z "$os_version" ]; then - return 0 + if xcat_systemd_os_release_status; then + return 0 + else + os_release_status=$? + if [ "$os_release_status" -eq 1 ]; then + return 1 fi + fi - os_major=${os_version%%.*} - case "$os_major" in - ''|*[!0-9]*) return 1 ;; - esac - case "$os_id" in - ubuntu) [ "$os_major" -ge 15 ] && return 0 ;; - debian) [ "$os_major" -ge 8 ] && return 0 ;; - esac + if [ -x /usr/lib/systemd/systemd ] || [ -x /lib/systemd/systemd ]; then + return 0 fi return 1 @@ -75,16 +128,47 @@ fi case "$1" in purge) - rm -f /etc/init.d/xcatd + rm -f /etc/init.d/xcatd \ + /etc/init.d/xcatd.ucf-old \ + /etc/init.d/xcatd.ucf-new \ + /etc/init.d/xcatd.ucf-dist \ + /etc/init.d/xcatd.dpkg-bak \ + /etc/init.d/xcatd.dpkg-backup \ + /etc/init.d/xcatd.dpkg-remove \ + /var/lib/xcat/xcatd-systemd-mode + rm -f "$xcatd_state_dir/state" \ + "$xcatd_state_dir/xcatd" \ + "$xcatd_state_dir/state.tmp."* \ + "$xcatd_state_dir/xcatd.tmp."* + clear_xcatd_pending_state + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* + rmdir "$xcatd_state_dir" 2>/dev/null || true + if command -v ucf >/dev/null 2>&1; then + ucf --purge /etc/init.d/xcatd + fi + if command -v ucfr >/dev/null 2>&1; then + ucfr --purge xcat-server /etc/init.d/xcatd + fi ;; - upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + abort-install) + clear_xcatd_pending_state + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* + rmdir "$xcatd_state_dir" 2>/dev/null || true + ;; + + abort-upgrade|disappear) + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* + ;; + + upgrade|failed-upgrade) ;; remove) - rm -f /usr/sbin/xcatd #remove the symbolic + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* + rm -f /usr/sbin/xcatd #remove the symbolic - rm -f /etc/apache2/conf-enabled/xcat-ws.conf + rm -f /etc/apache2/conf-enabled/xcat-ws.conf ;; *) diff --git a/xCAT-server/debian/preinst b/xCAT-server/debian/preinst index 1aabad540..dd69a6915 100644 --- a/xCAT-server/debian/preinst +++ b/xCAT-server/debian/preinst @@ -5,13 +5,59 @@ set -e -xcat_target_uses_systemd() +xcat_systemd_os_release_status() { - if [ -d /run/systemd/system ]; then + [ -r /etc/os-release ] || return 2 + + os_id= + os_version= + while IFS='=' read -r key value; do + value=${value#\"} + value=${value%\"} + value=${value#\'} + value=${value%\'} + case "$key" in + ID) os_id=$value ;; + VERSION_ID) os_version=$value ;; + esac + done < /etc/os-release + + if [ "$os_id" = "debian" ] && [ -z "$os_version" ]; then return 0 fi - if [ -x /usr/lib/systemd/systemd ] || [ -x /lib/systemd/systemd ]; then + os_major=${os_version%%.*} + case "$os_major" in + ''|*[!0-9]*) return 2 ;; + esac + + case "$os_id" in + rhel|centos|rocky|almalinux|ol|scientific) + [ "$os_major" -ge 7 ] && return 0 + return 1 + ;; + fedora|ubuntu) + [ "$os_major" -ge 15 ] && return 0 + return 1 + ;; + debian) + [ "$os_major" -ge 8 ] && return 0 + return 1 + ;; + sles|sled|opensuse|opensuse-leap|opensuse-tumbleweed) + [ "$os_major" -ge 12 ] && return 0 + return 1 + ;; + *) + return 2 + ;; + esac + +} + +xcat_target_uses_systemd() +{ + if [ -d /run/systemd/system ]; then return 0 fi @@ -19,41 +65,193 @@ xcat_target_uses_systemd() init_target=$(readlink /sbin/init 2>/dev/null || true) case "$init_target" in *systemd*) return 0 ;; + *) return 1 ;; esac fi - if [ -r /etc/os-release ]; then - os_id= - os_version= - while IFS='=' read -r key value; do - value=${value#\"} - value=${value%\"} - value=${value#\'} - value=${value%\'} - case "$key" in - ID) os_id=$value ;; - VERSION_ID) os_version=$value ;; - esac - done < /etc/os-release + if [ -e /sbin/init ]; then + return 1 + fi - if [ "$os_id" = "debian" ] && [ -z "$os_version" ]; then - return 0 + if xcat_systemd_os_release_status; then + return 0 + else + os_release_status=$? + if [ "$os_release_status" -eq 1 ]; then + return 1 fi + fi - os_major=${os_version%%.*} - case "$os_major" in - ''|*[!0-9]*) return 1 ;; - esac - case "$os_id" in - ubuntu) [ "$os_major" -ge 15 ] && return 0 ;; - debian) [ "$os_major" -ge 8 ] && return 0 ;; - esac + if [ -x /usr/lib/systemd/systemd ] || [ -x /lib/systemd/systemd ]; then + return 0 fi return 1 } -if xcat_target_uses_systemd && \ +xcatd_init_state=/opt/xcat/share/xcat/scripts/xcatd-init-state +xcatd_init_compat=/opt/xcat/share/xcat/scripts/xcatd-init-compat +xcatd_legacy_init=/etc/init.d/xcatd +xcatd_state_dir=/var/lib/xcat/xcatd-init-state +xcatd_old_systemd_marker=/var/lib/xcat/xcatd-systemd-mode +xcatd_context_file=$xcatd_state_dir/context + +write_xcatd_context() +( + context_value=$1 + context_tmp="$xcatd_context_file.tmp.$$" + umask 077 + if ! printf '%s\n' "$context_value" > "$context_tmp" || + ! mv -f "$context_tmp" "$xcatd_context_file"; then + rm -f "$context_tmp" + return 1 + fi +) + +clear_xcatd_pending_state() +{ + rm -f "$xcatd_state_dir/pending-xcatd" \ + "$xcatd_state_dir/pending-deleted" \ + "$xcatd_state_dir/pending-enabled" \ + "$xcatd_state_dir/pending-xcatd.tmp."* +} + +xcatd_legacy_enable_state() +{ + if [ -x "$xcatd_init_compat" ]; then + if detected_legacy_state=$( + "$xcatd_init_compat" debian-legacy-state 2>/dev/null + ); then + case "$detected_legacy_state" in + enabled) printf '%s\n' yes ;; + disabled) printf '%s\n' no ;; + unregistered) printf '%s\n' unregistered ;; + *) + echo "Invalid xcatd legacy state: $detected_legacy_state" >&2 + return 1 + ;; + esac + return + else + detector_status=$? + if [ "$detector_status" -ne 2 ]; then + echo "Unable to query xcatd Debian legacy state with $xcatd_init_compat" >&2 + return "$detector_status" + fi + fi + fi + + # Direct upgrades from a package without the Debian-specific command + # still need to capture registration before dpkg removes the conffile. + for rc_link in /etc/rc?.d/S??xcatd; do + if [ -e "$rc_link" ] || [ -L "$rc_link" ]; then + printf '%s\n' yes + return + fi + done + for rc_link in /etc/rc?.d/K??xcatd; do + if [ -e "$rc_link" ] || [ -L "$rc_link" ]; then + printf '%s\n' no + return + fi + done + printf '%s\n' unregistered +} + +if xcat_target_uses_systemd; then + xcatd_uses_systemd=yes +else + xcatd_uses_systemd=no +fi + +xcatd_transition_context= +case "$1" in + upgrade) xcatd_transition_context=upgrade ;; + install) xcatd_transition_context=fresh ;; +esac + +if [ -n "$xcatd_transition_context" ] || [ "$1" = abort-upgrade ]; then + if [ -L "$xcatd_state_dir" ] || \ + { [ -e "$xcatd_state_dir" ] && [ ! -d "$xcatd_state_dir" ]; }; then + echo "Invalid xcatd init state directory: $xcatd_state_dir" >&2 + exit 1 + fi + mkdir -p "$xcatd_state_dir" + chmod 700 "$xcatd_state_dir" +fi + +if [ -n "$xcatd_transition_context" ] && \ + [ "$xcatd_uses_systemd" = yes ] && [ -x "$xcatd_init_state" ]; then + "$xcatd_init_state" prepare-systemd "$xcatd_transition_context" +elif [ -n "$xcatd_transition_context" ] && [ ! -x "$xcatd_init_state" ]; then + # Before the first package carrying the state helper is unpacked, save + # only the evidence that dpkg-maintscript-helper could remove. + recovery_candidate= + for candidate in \ + "$xcatd_legacy_init" \ + "$xcatd_legacy_init.dpkg-bak" \ + "$xcatd_legacy_init.dpkg-backup" \ + "$xcatd_legacy_init.dpkg-remove"; do + if [ -e "$candidate" ] || [ -L "$candidate" ]; then + recovery_candidate=$candidate + break + fi + done + + known_legacy=no + if [ -e "$xcatd_state_dir/state" ] || \ + [ -L "$xcatd_state_dir/state" ]; then + # Durable state from an earlier package is stronger than the + # legacy dpkg metadata left behind after removal. + clear_xcatd_pending_state + elif [ -n "$recovery_candidate" ]; then + clear_xcatd_pending_state + pending_tmp="$xcatd_state_dir/pending-xcatd.tmp.$$" + if ! cp -a "$recovery_candidate" "$pending_tmp" || + ! mv -f "$pending_tmp" "$xcatd_state_dir/pending-xcatd"; then + rm -f "$pending_tmp" + exit 1 + fi + rm -f "$xcatd_old_systemd_marker" + known_legacy=yes + else + conffile_record= + if command -v dpkg-query >/dev/null 2>&1; then + conffile_record=$( + dpkg-query -W -f='${Conffiles}\n' xcat-server 2>/dev/null | + sed -n '\|^ /etc/init.d/xcatd |p' + ) + fi + if [ -n "$conffile_record" ]; then + case "$conffile_record" in + *" obsolete"*) + # The installed package already removed the conffile. + # Record package omission rather than an admin deletion. + clear_xcatd_pending_state + touch "$xcatd_old_systemd_marker" + ;; + *) + # A still-owned conffile that is absent was deleted by + # the administrator and must stay absent. + clear_xcatd_pending_state + rm -f "$xcatd_old_systemd_marker" + touch "$xcatd_state_dir/pending-deleted" + known_legacy=yes + ;; + esac + else + # A retry without an installed conffile record must not reuse + # evidence captured by an earlier, interrupted installation. + clear_xcatd_pending_state + fi + fi + + if [ "$known_legacy" = yes ]; then + xcatd_legacy_enable_state > "$xcatd_state_dir/pending-enabled" + fi +fi + +if [ "$xcatd_uses_systemd" = yes ] && \ command -v dpkg-maintscript-helper >/dev/null 2>&1 && \ dpkg-maintscript-helper supports rm_conffile; then dpkg-maintscript-helper rm_conffile /etc/init.d/xcatd -- "$@" @@ -70,12 +268,14 @@ fi case "$1" in upgrade) - touch /tmp/xCAT-server_upgrade.tmp + write_xcatd_context upgrade ;; install) + write_xcatd_context fresh ;; abort-upgrade) + rm -f "$xcatd_context_file" "$xcatd_context_file.tmp."* ;; *) diff --git a/xCAT-server/debian/prerm b/xCAT-server/debian/prerm index 818be25a4..c4adbfda6 100644 --- a/xCAT-server/debian/prerm +++ b/xCAT-server/debian/prerm @@ -5,11 +5,6 @@ set -e -xcat_can_use_systemctl() -{ - [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 -} - xcatd_init_compat=/opt/xcat/share/xcat/scripts/xcatd-init-compat # summary of how this script can be called: @@ -27,13 +22,13 @@ xcatd_init_compat=/opt/xcat/share/xcat/scripts/xcatd-init-compat case "$1" in remove) if [ -f "/proc/cmdline" ]; then # prevent running it during install into chroot image - if xcat_can_use_systemctl; then + if "$xcatd_init_compat" can-use-systemctl; then systemctl stop xcatd.service - elif ! "$xcatd_init_compat" uses-systemd && [ -x /etc/init.d/xcatd ]; then + elif ! "$xcatd_init_compat" uses-systemd --explicit-target && [ -x /etc/init.d/xcatd ]; then /etc/init.d/xcatd stop fi fi - if "$xcatd_init_compat" uses-systemd; then + if "$xcatd_init_compat" uses-systemd --explicit-target; then if command -v systemctl >/dev/null 2>&1; then systemctl disable xcatd.service fi diff --git a/xCAT-server/debian/xcatd-init-state b/xCAT-server/debian/xcatd-init-state new file mode 100755 index 000000000..ff53451e0 --- /dev/null +++ b/xCAT-server/debian/xcatd-init-state @@ -0,0 +1,701 @@ +#!/bin/sh + +# Preserve the Debian xcatd SysV configuration while the target uses systemd. +# The state file is parsed as data; it is never sourced. + +set -e + +compat_root=${XCAT_COMPAT_ROOT:-} +compat_root=${compat_root%/} +xcat_root=${XCATROOT:-/opt/xcat} + +case "$xcat_root" in + /*) ;; + *) + echo "XCATROOT must be an absolute path" >&2 + exit 2 + ;; +esac + +legacy_init="$compat_root/etc/init.d/xcatd" +legacy_template="$compat_root$xcat_root/share/xcat/scripts/xcatd" +init_compat="$compat_root$xcat_root/share/xcat/scripts/xcatd-init-compat" +state_dir="$compat_root/var/lib/xcat/xcatd-init-state" +state_file="$state_dir/state" +state_stash="$state_dir/xcatd" +pending_stash="$state_dir/pending-xcatd" +pending_deleted="$state_dir/pending-deleted" +pending_enabled="$state_dir/pending-enabled" +old_systemd_marker="$compat_root/var/lib/xcat/xcatd-systemd-mode" + +state_mode=unknown +state_content=unknown +state_enabled=unknown +state_origin=unknown +state_format= + +read_state() +{ + [ -r "$state_file" ] || return 1 + + state_mode=unknown + state_content=unknown + state_enabled=unknown + state_origin=unknown + state_format= + seen_format=0 + seen_mode=0 + seen_content=0 + seen_enabled=0 + seen_origin=0 + while IFS='=' read -r state_key state_value; do + case "$state_key" in + format) + [ "$seen_format" -eq 0 ] || return 1 + state_format=$state_value + seen_format=1 + ;; + mode) + [ "$seen_mode" -eq 0 ] || return 1 + state_mode=$state_value + seen_mode=1 + ;; + content) + [ "$seen_content" -eq 0 ] || return 1 + state_content=$state_value + seen_content=1 + ;; + enabled) + [ "$seen_enabled" -eq 0 ] || return 1 + state_enabled=$state_value + seen_enabled=1 + ;; + origin) + [ "$seen_origin" -eq 0 ] || return 1 + state_origin=$state_value + seen_origin=1 + ;; + '') ;; + *) + echo "Invalid xcatd init state key: $state_key" >&2 + return 1 + ;; + esac + done < "$state_file" + + if [ "$seen_format" -ne 1 ] || [ "$seen_mode" -ne 1 ] || + [ "$seen_content" -ne 1 ] || [ "$seen_enabled" -ne 1 ] || + [ "$seen_origin" -ne 1 ]; then + echo "Invalid xcatd init state format: ${state_format:-missing}" >&2 + return 1 + fi + case "$state_format" in + 1|2) ;; + *) + echo "Invalid xcatd init state format: ${state_format:-missing}" >&2 + return 1 + ;; + esac + case "$state_mode" in + legacy|systemd|transition-systemd|transition-legacy|unknown) ;; + *) + echo "Invalid xcatd init mode state: $state_mode" >&2 + return 1 + ;; + esac + case "$state_content" in + active|stashed|deleted|package-default|unknown) ;; + *) + echo "Invalid xcatd init content state: $state_content" >&2 + return 1 + ;; + esac + case "$state_enabled" in + yes|no|unknown) ;; + unregistered) + if [ "$state_format" = 2 ]; then + : + else + echo "Invalid xcatd init enablement state: $state_enabled" >&2 + return 1 + fi + ;; + *) + echo "Invalid xcatd init enablement state: $state_enabled" >&2 + return 1 + ;; + esac + case "$state_origin" in + fresh|legacy|systemd|unknown) ;; + *) + echo "Invalid xcatd init transition origin: $state_origin" >&2 + return 1 + ;; + esac + + # Format 1 collapsed registered-disabled and unregistered into "no", + # then restored that value by removing every SysV link. Preserve that + # behavior when recovering an interrupted pre-format-2 transaction. + if [ "$state_format" = 1 ] && [ "$state_enabled" = no ]; then + state_enabled=unregistered + fi +} + +ensure_state_dir() +{ + if [ -L "$state_dir" ] || { [ -e "$state_dir" ] && [ ! -d "$state_dir" ]; }; then + echo "Invalid xcatd init state directory: $state_dir" >&2 + exit 1 + fi + mkdir -p "$state_dir" + chmod 700 "$state_dir" +} + +write_state() +{ + new_mode=$1 + new_content=$2 + new_enabled=$3 + new_origin=$4 + + ensure_state_dir + state_tmp="$state_file.tmp.$$" + if ! ( + umask 077 + printf 'format=2\nmode=%s\ncontent=%s\nenabled=%s\norigin=%s\n' \ + "$new_mode" "$new_content" "$new_enabled" "$new_origin" > "$state_tmp" && + mv -f "$state_tmp" "$state_file" + ); then + rm -f "$state_tmp" + return 1 + fi + state_mode=$new_mode + state_content=$new_content + state_enabled=$new_enabled + state_origin=$new_origin +} + +legacy_init_exists() +{ + [ -e "$legacy_init" ] || [ -L "$legacy_init" ] +} + +find_recovery_candidate() +{ + recovery_candidate= + for candidate in \ + "$legacy_init" \ + "$legacy_init.dpkg-bak" \ + "$legacy_init.dpkg-backup" \ + "$legacy_init.dpkg-remove"; do + if [ -e "$candidate" ] || [ -L "$candidate" ]; then + recovery_candidate=$candidate + return 0 + fi + done + return 1 +} + +stash_candidate() +{ + stash_source=$1 + ensure_state_dir + stash_tmp="$state_stash.tmp.$$" + rm -f "$stash_tmp" + if ! cp -a "$stash_source" "$stash_tmp" || + ! mv -f "$stash_tmp" "$state_stash"; then + rm -f "$stash_tmp" + return 1 + fi +} + +shared_init_state() +{ + if [ ! -x "$init_compat" ]; then + echo "Missing xcatd init compatibility helper: $init_compat" >&2 + return 1 + fi + "$init_compat" "$@" +} + +legacy_enable_state() +{ + if ! detected_legacy_state=$(shared_init_state debian-legacy-state); then + return 1 + fi + case "$detected_legacy_state" in + enabled) printf '%s\n' yes ;; + disabled) printf '%s\n' no ;; + unregistered) printf '%s\n' unregistered ;; + *) + echo "Invalid legacy state from $init_compat: $detected_legacy_state" >&2 + return 1 + ;; + esac +} + +systemd_enable_state() +{ + if ! detected_systemd_state=$(shared_init_state systemd-state --allow-unknown); then + return 1 + fi + case "$detected_systemd_state" in + enabled) printf '%s\n' yes ;; + disabled) printf '%s\n' no ;; + masked) printf '%s\n' unregistered ;; + unknown) printf '%s\n' unknown ;; + *) + echo "Invalid systemd state from $init_compat: $detected_systemd_state" >&2 + return 1 + ;; + esac +} + +validate_context() +{ + case "$1" in + fresh|upgrade) ;; + *) + echo "Invalid xcatd init transition context: $1" >&2 + exit 2 + ;; + esac +} + +prepare_systemd() +{ + transition_context=$1 + validate_context "$transition_context" + + if [ -e "$state_file" ]; then + read_state + had_state=yes + else + had_state=no + state_mode=unknown + state_content=unknown + state_enabled=unknown + state_origin=unknown + if [ -f "$old_systemd_marker" ]; then + state_mode=systemd + state_content=package-default + fi + fi + + previous_mode=$state_mode + previous_origin=$state_origin + known_legacy=no + case "$previous_mode" in + systemd|transition-systemd|transition-legacy) from_systemd=yes ;; + *) from_systemd=no ;; + esac + case "$previous_mode" in + transition-systemd|transition-legacy) transition_origin=$previous_origin ;; + systemd) transition_origin=systemd ;; + legacy) transition_origin=legacy ;; + *) + if [ "$transition_context" = fresh ]; then + transition_origin=fresh + else + transition_origin=systemd + fi + ;; + esac + detected_systemd_enabled=$(systemd_enable_state) + + if [ "$had_state" = no ] && \ + { [ -e "$pending_stash" ] || [ -L "$pending_stash" ]; }; then + stash_candidate "$pending_stash" + state_content=stashed + known_legacy=yes + transition_origin=legacy + elif [ "$had_state" = no ] && [ -f "$pending_deleted" ]; then + rm -f "$state_stash" + state_content=deleted + known_legacy=yes + transition_origin=legacy + elif [ "$previous_mode" = transition-legacy ]; then + case "$state_content" in + stashed) + if [ ! -e "$state_stash" ] && [ ! -L "$state_stash" ]; then + echo "Stashed xcatd init script is missing: $state_stash" >&2 + exit 1 + fi + ;; + active) + if legacy_init_exists; then + stash_candidate "$legacy_init" + state_content=stashed + else + echo "Active xcatd init transition has no recoverable script" >&2 + exit 1 + fi + ;; + esac + elif legacy_init_exists; then + stash_candidate "$legacy_init" + state_content=stashed + if [ "$from_systemd" = no ]; then + known_legacy=yes + transition_origin=legacy + fi + elif [ "$from_systemd" = no ] && find_recovery_candidate; then + stash_candidate "$recovery_candidate" + state_content=stashed + known_legacy=yes + transition_origin=legacy + elif [ "$previous_mode" = legacy ]; then + rm -f "$state_stash" + state_content=deleted + known_legacy=yes + transition_origin=legacy + elif [ "$from_systemd" = no ] && [ "$transition_context" = fresh ]; then + rm -f "$state_stash" + state_content=package-default + transition_origin=fresh + elif [ "$from_systemd" = no ]; then + rm -f "$state_stash" + if [ "$detected_systemd_enabled" = yes ]; then + state_content=package-default + else + state_content=unknown + fi + fi + + if [ "$detected_systemd_enabled" = unregistered ]; then + state_enabled=unregistered + transition_origin=systemd + elif [ "$transition_context" = fresh ]; then + state_enabled=yes + transition_origin=fresh + elif [ "$known_legacy" = yes ]; then + if [ -r "$pending_enabled" ]; then + pending_enabled_value=$(sed -n '1p' "$pending_enabled") + else + pending_enabled_value=unknown + fi + case "$pending_enabled_value" in + yes|no|unregistered) legacy_enabled=$pending_enabled_value ;; + *) legacy_enabled=$(legacy_enable_state) ;; + esac + + if [ "$legacy_enabled" = yes ]; then + state_enabled=yes + elif [ "$had_state" = no ]; then + if [ "$detected_systemd_enabled" = yes ]; then + state_enabled=yes + else + state_enabled=$legacy_enabled + fi + else + state_enabled=$legacy_enabled + fi + elif { [ "$previous_mode" = transition-systemd ] || + [ "$previous_mode" = transition-legacy ]; } && \ + [ "$state_enabled" != unknown ]; then + : + else + case "$detected_systemd_enabled" in + yes) state_enabled=yes ;; + no) + if [ "$state_enabled" != unregistered ]; then + state_enabled=no + fi + ;; + unregistered) state_enabled=unregistered ;; + esac + fi + + write_state transition-systemd "$state_content" "$state_enabled" "$transition_origin" + rm -f "$pending_stash" "$pending_deleted" "$pending_enabled" +} + +commit_systemd() +{ + read_state + write_state systemd "$state_content" "$state_enabled" unknown + rm -f "$old_systemd_marker" +} + +apply_legacy_registration() +{ + command -v update-rc.d >/dev/null 2>&1 || return 0 + + case "$state_content" in + deleted|unknown) + update-rc.d -f xcatd remove + return + ;; + esac + if [ -x "$legacy_init" ]; then + case "$state_enabled" in + yes) + update-rc.d xcatd defaults + ;; + no) + update-rc.d xcatd defaults + if ! update-rc.d xcatd disable; then + # defaults preserves any existing registration, including + # links outside xcatd's declared start runlevels that the + # native disable operation cannot toggle. Canonicalize + # only after that unusable layout is rejected. + update-rc.d -f xcatd remove + update-rc.d xcatd defaults + update-rc.d xcatd disable + fi + ;; + unregistered|unknown) + update-rc.d -f xcatd remove + ;; + esac + else + update-rc.d -f xcatd remove + fi +} + +configure_legacy() +{ + transition_context=$1 + validate_context "$transition_context" + + returning_from_systemd=no + refresh_systemd_enabled=no + resuming_legacy_transition=no + resuming_stashed_transition=no + stable_legacy_upgrade=no + detected_systemd_enabled=unknown + transition_origin=legacy + if [ -e "$state_file" ]; then + read_state + had_state=yes + if [ "$state_mode" = legacy ] && \ + [ "$transition_context" = upgrade ]; then + stable_legacy_upgrade=yes + else + detected_systemd_enabled=$(systemd_enable_state) + fi + if [ "$state_mode" = transition-legacy ]; then + resuming_legacy_transition=yes + if [ "$state_content" = stashed ] && + { [ -e "$state_stash" ] || [ -L "$state_stash" ]; }; then + resuming_stashed_transition=yes + fi + fi + case "$state_mode" in + systemd) + returning_from_systemd=yes + refresh_systemd_enabled=yes + transition_origin=systemd + ;; + transition-systemd) + returning_from_systemd=yes + transition_origin=$state_origin + ;; + transition-legacy) + transition_origin=$state_origin + returning_from_systemd=yes + ;; + legacy) + transition_origin=legacy + if legacy_init_exists; then + state_content=active + else + state_content=deleted + fi + if [ "$transition_context" = upgrade ]; then + state_enabled=$(legacy_enable_state) + fi + ;; + esac + else + had_state=no + detected_systemd_enabled=$(systemd_enable_state) + state_mode=unknown + state_content=unknown + state_enabled=unknown + state_origin=unknown + if [ -e "$pending_stash" ] || [ -L "$pending_stash" ]; then + stash_candidate "$pending_stash" + state_content=stashed + transition_origin=legacy + elif [ -f "$pending_deleted" ]; then + rm -f "$state_stash" + state_content=deleted + transition_origin=legacy + elif [ -f "$old_systemd_marker" ]; then + returning_from_systemd=yes + refresh_systemd_enabled=yes + transition_origin=systemd + state_content=package-default + elif find_recovery_candidate; then + if [ "$recovery_candidate" = "$legacy_init" ]; then + state_content=active + transition_origin=legacy + else + stash_candidate "$recovery_candidate" + state_content=stashed + returning_from_systemd=yes + refresh_systemd_enabled=yes + transition_origin=systemd + fi + elif [ "$transition_context" = fresh ]; then + state_content=package-default + state_enabled=yes + transition_origin=fresh + else + returning_from_systemd=yes + refresh_systemd_enabled=yes + transition_origin=systemd + state_enabled=$detected_systemd_enabled + if [ "$detected_systemd_enabled" = yes ]; then + state_content=package-default + fi + fi + fi + + if legacy_init_exists && [ "$resuming_legacy_transition" = no ]; then + case "$returning_from_systemd:$state_content" in + yes:*|*:active) + stash_candidate "$legacy_init" + state_content=stashed + ;; + esac + fi + + if [ "$stable_legacy_upgrade" = yes ]; then + : + elif [ "$detected_systemd_enabled" = unregistered ]; then + state_enabled=unregistered + transition_origin=systemd + elif [ "$transition_context" = fresh ]; then + state_enabled=yes + transition_origin=fresh + elif [ "$had_state" = no ]; then + if [ -r "$pending_enabled" ]; then + pending_enabled_value=$(sed -n '1p' "$pending_enabled") + else + pending_enabled_value=unknown + fi + case "$pending_enabled_value" in + yes|no|unregistered) legacy_enabled=$pending_enabled_value ;; + *) legacy_enabled=$(legacy_enable_state) ;; + esac + if [ "$legacy_enabled" = yes ] || \ + [ "$detected_systemd_enabled" = yes ]; then + state_enabled=yes + elif [ "$transition_origin" = legacy ] && \ + { [ "$legacy_enabled" = no ] || \ + [ "$legacy_enabled" = unregistered ]; }; then + state_enabled=$legacy_enabled + elif [ "$detected_systemd_enabled" = no ]; then + state_enabled=no + elif [ "$detected_systemd_enabled" = unregistered ]; then + state_enabled=unregistered + else + state_enabled=$legacy_enabled + fi + elif [ "$refresh_systemd_enabled" = yes ]; then + case "$detected_systemd_enabled" in + yes) state_enabled=yes ;; + no) + if [ "$state_enabled" != unregistered ]; then + state_enabled=no + fi + ;; + unregistered) state_enabled=unregistered ;; + esac + fi + + write_state transition-legacy "$state_content" "$state_enabled" "$transition_origin" + + case "$state_content" in + stashed) + if [ ! -e "$state_stash" ] && [ ! -L "$state_stash" ]; then + echo "Stashed xcatd init script is missing: $state_stash" >&2 + exit 1 + fi + if [ "$resuming_stashed_transition" = yes ]; then + rm -f "$legacy_init" + mkdir -p "$compat_root/etc/init.d" + cp -a "$state_stash" "$legacy_init" + elif ! legacy_init_exists; then + mkdir -p "$compat_root/etc/init.d" + cp -a "$state_stash" "$legacy_init" + fi + ucf "$legacy_template" "$legacy_init" + ;; + active) + if legacy_init_exists; then + ucf "$legacy_template" "$legacy_init" + fi + ;; + package-default) + mkdir -p "$compat_root/etc/init.d" + if [ "$resuming_legacy_transition" = yes ]; then + rm -f "$legacy_init" + fi + UCF_FORCE_CONFFMISS=1 ucf "$legacy_template" "$legacy_init" + ;; + deleted) + rm -f "$legacy_init" + ;; + unknown) + echo "xcat-server: prior xcatd init state is ambiguous; preserving its absence" >&2 + ;; + esac + + ucfr xcat-server "$legacy_init" + + if [ "$transition_context" = fresh ] || \ + [ "$returning_from_systemd" = yes ] || [ "$had_state" = no ]; then + apply_legacy_registration + fi + + if legacy_init_exists; then + final_content=active + else + final_content=deleted + fi + final_enabled=$(legacy_enable_state) + write_state legacy "$final_content" "$final_enabled" unknown + rm -f "$state_stash" "$old_systemd_marker" \ + "$pending_stash" "$pending_deleted" "$pending_enabled" \ + "$legacy_init.dpkg-bak" "$legacy_init.dpkg-backup" \ + "$legacy_init.dpkg-remove" +} + +get_state() +{ + requested_field=$1 + read_state + case "$requested_field" in + mode) printf '%s\n' "$state_mode" ;; + content) printf '%s\n' "$state_content" ;; + enabled) printf '%s\n' "$state_enabled" ;; + origin) printf '%s\n' "$state_origin" ;; + *) + echo "Usage: $0 get {mode|content|enabled|origin}" >&2 + exit 2 + ;; + esac +} + +case "${1:-}" in + prepare-systemd) + prepare_systemd "${2:-}" + ;; + commit-systemd) + [ "$#" -eq 1 ] || exit 2 + commit_systemd + ;; + configure-legacy) + configure_legacy "${2:-}" + ;; + get) + get_state "${2:-}" + ;; + *) + echo "Usage: $0 {prepare-systemd {fresh|upgrade}|commit-systemd|configure-legacy {fresh|upgrade}|get {mode|content|enabled|origin}}" >&2 + exit 2 + ;; +esac diff --git a/xCAT-server/debian/xcatd.md5sum b/xCAT-server/debian/xcatd.md5sum new file mode 100644 index 000000000..f68bc4327 --- /dev/null +++ b/xCAT-server/debian/xcatd.md5sum @@ -0,0 +1,9 @@ +be74fa94c7cae192b0522b094ab13f88 2.7.8 +106298b7314976e37b61359ac0639f47 2.8.3 +22b3d638134b58d249337c7193466f91 2.8.4 +ec7700a6317954bd773bb07bc3d352f4 2.9.2_release +7f7b7bf464770c6ed6c6433c340de757 2.12 +78730e365b6861c45fedb51f8359bb37 2.12.1 +d0f83472c8ad6d9a5aa9a64e50bb5dc5 2.12.2 +0b1eea60994ff79faa9a8d0bcd53c558 2.17.0 +8797eef6731719e0eacff59612b3e916 2.18.0 diff --git a/xCAT-server/share/xcat/scripts/xcatd-init-compat b/xCAT-server/share/xcat/scripts/xcatd-init-compat index f9beb42d2..00f8b5bf4 100755 --- a/xCAT-server/share/xcat/scripts/xcatd-init-compat +++ b/xCAT-server/share/xcat/scripts/xcatd-init-compat @@ -130,22 +130,32 @@ can_use_systemctl() legacy_state() { - for legacy_link in \ - "$compat_root"/etc/rc.d/rc?.d/S??xcatd \ - "$compat_root"/etc/rc?.d/S??xcatd \ - "$compat_root"/etc/init.d/rc?.d/S??xcatd - do + detection_mode=${1:-compat} + + if [ "$detection_mode" = debian-layout ]; then + set -- "$compat_root"/etc/rc?.d/S??xcatd + else + set -- \ + "$compat_root"/etc/rc.d/rc?.d/S??xcatd \ + "$compat_root"/etc/rc?.d/S??xcatd \ + "$compat_root"/etc/init.d/rc?.d/S??xcatd + fi + for legacy_link in "$@"; do if [ -e "$legacy_link" ] || [ -L "$legacy_link" ]; then echo enabled return fi done - for legacy_link in \ - "$compat_root"/etc/rc.d/rc?.d/K??xcatd \ - "$compat_root"/etc/rc?.d/K??xcatd \ - "$compat_root"/etc/init.d/rc?.d/K??xcatd - do + if [ "$detection_mode" = debian-layout ]; then + set -- "$compat_root"/etc/rc?.d/K??xcatd + else + set -- \ + "$compat_root"/etc/rc.d/rc?.d/K??xcatd \ + "$compat_root"/etc/rc?.d/K??xcatd \ + "$compat_root"/etc/init.d/rc?.d/K??xcatd + fi + for legacy_link in "$@"; do if [ -e "$legacy_link" ] || [ -L "$legacy_link" ]; then echo disabled return @@ -184,7 +194,7 @@ legacy_transition_state() esac } -systemd_state() +systemd_is_masked() { for systemd_mask in \ "$compat_root/etc/systemd/system/xcatd.service" \ @@ -194,27 +204,17 @@ systemd_state() systemd_mask_target=$(readlink "$systemd_mask" 2>/dev/null || true) case "$systemd_mask_target" in /dev/null|*/dev/null) - echo masked - return + return 0 ;; esac fi done - if [ -z "$compat_root" ] && command -v systemctl >/dev/null 2>&1; then - detected_systemd_state=$(systemctl is-enabled xcatd.service 2>/dev/null || true) - case "$detected_systemd_state" in - enabled*) - echo enabled - return - ;; - masked*) - echo masked - return - ;; - esac - fi + return 1 +} +systemd_links_enabled() +{ for systemd_link in \ "$compat_root"/etc/systemd/system/*.wants/xcatd.service \ "$compat_root"/etc/systemd/system/*.requires/xcatd.service \ @@ -222,11 +222,90 @@ systemd_state() "$compat_root"/run/systemd/system/*.requires/xcatd.service do if [ -e "$systemd_link" ] || [ -L "$systemd_link" ]; then - echo enabled - return + return 0 fi done + return 1 +} + +systemctl_enable_state() +{ + detection_mode=${1:-compat} + + command -v systemctl >/dev/null 2>&1 || return 1 + + if [ -n "$compat_root" ]; then + detected_systemd_state=$(systemctl --root="$compat_root" is-enabled xcatd.service 2>/dev/null || true) + else + detected_systemd_state=$(systemctl is-enabled xcatd.service 2>/dev/null || true) + fi + if [ "$detection_mode" = precise ]; then + case "$detected_systemd_state" in + enabled|enabled-runtime) echo enabled ;; + masked|masked-runtime) echo masked ;; + disabled|linked|linked-runtime|alias|static|indirect|generated|transient) + echo disabled + ;; + *) return 1 ;; + esac + else + case "$detected_systemd_state" in + enabled*) echo enabled ;; + masked*) echo masked ;; + disabled|linked|linked-runtime|alias|static|indirect|generated|transient) + echo disabled + ;; + *) return 1 ;; + esac + fi +} + +systemd_state() +{ + detection_mode=${1:-compat} + detected_systemd_state= + + if systemd_is_masked; then + echo masked + return + fi + + if [ "$detection_mode" = allow-unknown ]; then + detected_systemd_state=$(systemctl_enable_state precise || true) + if [ "$detected_systemd_state" = masked ]; then + echo masked + return + fi + fi + + # Preserve the historical host-first order for existing callers. + if [ "$detection_mode" = compat ] && [ -z "$compat_root" ]; then + detected_systemd_state=$(systemctl_enable_state || true) + case "$detected_systemd_state" in + enabled|masked) + echo "$detected_systemd_state" + return + ;; + esac + fi + + if systemd_links_enabled; then + echo enabled + return + fi + + if [ "$detection_mode" = allow-unknown ]; then + case "$detected_systemd_state" in + enabled|disabled) + echo "$detected_systemd_state" + return + ;; + esac + echo unknown + return + fi + echo disabled } @@ -412,11 +491,22 @@ case "${1:-}" in legacy-state) legacy_state ;; + debian-legacy-state) + if [ "$#" -ne 1 ]; then + echo "Usage: $0 debian-legacy-state" >&2 + exit 2 + fi + legacy_state debian-layout + ;; legacy-transition-state) legacy_transition_state ;; systemd-state) - systemd_state + if [ "$#" -eq 2 ] && [ "${2:-}" = --allow-unknown ]; then + systemd_state allow-unknown + else + systemd_state + fi ;; register-legacy) case "${2:-}" in @@ -455,7 +545,7 @@ case "${1:-}" in remove_managed_legacy_init ;; *) - echo "Usage: $0 {uses-systemd [--explicit-target]|can-use-systemctl|legacy-state|legacy-transition-state|systemd-state|register-legacy {default|enabled|disabled}|unregister-legacy|disable-systemd [--links-only]|unregister-all|configure [--replace] [--explicit-target] [--track-managed]|remove|remove-managed}" >&2 + echo "Usage: $0 {uses-systemd [--explicit-target]|can-use-systemctl|legacy-state|debian-legacy-state|legacy-transition-state|systemd-state [--allow-unknown]|register-legacy {default|enabled|disabled}|unregister-legacy|disable-systemd [--links-only]|unregister-all|configure [--replace] [--explicit-target] [--track-managed]|remove|remove-managed}" >&2 exit 2 ;; esac diff --git a/xCAT-test/unit/xcatd_debian_conffile_lifecycle.t b/xCAT-test/unit/xcatd_debian_conffile_lifecycle.t new file mode 100644 index 000000000..5a242b475 --- /dev/null +++ b/xCAT-test/unit/xcatd_debian_conffile_lifecycle.t @@ -0,0 +1,102 @@ +#!/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_init_state.t b/xCAT-test/unit/xcatd_debian_init_state.t new file mode 100644 index 000000000..a3d0388d3 --- /dev/null +++ b/xCAT-test/unit/xcatd_debian_init_state.t @@ -0,0 +1,1243 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Copy qw(copy); +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, '..', '..' ); +my $state_helper = File::Spec->catfile( + $repo_root, 'xCAT-server', 'debian', 'xcatd-init-state' +); +my $compat_helper = File::Spec->catfile( + $repo_root, 'xCAT-server', 'share', 'xcat', 'scripts', 'xcatd-init-compat' +); +my $template = File::Spec->catfile( + $repo_root, 'xCAT-server', 'etc', 'init.d', 'xcatd' +); + +sub write_file { + my ( $path, $contents, $mode ) = @_; + open( my $fh, '>', $path ) or die "Unable to write $path: $!"; + print {$fh} $contents; + close($fh); + chmod $mode, $path if defined $mode; +} + +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 path_mode { + my ($path) = @_; + return ( stat($path) )[2] & 07777; +} + +sub stage_root { + my $root = tempdir( CLEANUP => 1 ); + my $scripts = File::Spec->catdir( + $root, 'opt', 'xcat', 'share', 'xcat', 'scripts' + ); + my $fake_bin = File::Spec->catdir( $root, 'test-bin' ); + make_path( $scripts, $fake_bin, File::Spec->catdir( $root, 'sbin' ) ); + copy( $template, File::Spec->catfile( $scripts, 'xcatd' ) ) + or die "Unable to stage xcatd template: $!"; + copy( $compat_helper, File::Spec->catfile( $scripts, 'xcatd-init-compat' ) ) + or die "Unable to stage xcatd compatibility helper: $!"; + chmod 0755, File::Spec->catfile( $scripts, 'xcatd-init-compat' ); + + write_file( + File::Spec->catfile( $fake_bin, 'ucf' ), + <<'SH', 0755 +#!/bin/sh +set -eu +root=${XCAT_COMPAT_ROOT:?} +[ ! -f "$root/fail-ucf" ] || exit 9 +new_file=$1 +destination=$2 +if [ ! -e "$destination" ] && [ ! -L "$destination" ]; then + mkdir -p "$(dirname "$destination")" + cp "$new_file" "$destination" + chmod 755 "$destination" +fi +if [ -f "$root/mutate-ucf" ]; then + printf '%s\n' '# ucf mutation' >> "$destination" +fi +SH + ); + write_file( + File::Spec->catfile( $fake_bin, 'ucfr' ), + <<'SH', 0755 +#!/bin/sh +set -eu +root=${XCAT_COMPAT_ROOT:?} +[ ! -f "$root/fail-ucfr" ] || exit 8 +exit 0 +SH + ); + write_file( + File::Spec->catfile( $fake_bin, 'update-rc.d' ), + <<'SH', 0755 +#!/bin/sh +set -eu +root=${XCAT_COMPAT_ROOT:?} +remove_links() +{ + rm -f "$root"/etc/rc?.d/[SK]??xcatd +} +registered() +{ + for link in "$root"/etc/rc?.d/[SK]??xcatd; do + if [ -e "$link" ] || [ -L "$link" ]; then + return 0 + fi + done + return 1 +} +create_default_links() +{ + for runlevel in 3 4 5; do + mkdir -p "$root/etc/rc$runlevel.d" + ln -sfn ../init.d/xcatd "$root/etc/rc$runlevel.d/S01xcatd" + done +} +defaults() +{ + registered && return 0 + create_default_links +} +toggle_links() +{ + new_prefix=$1 + old_prefix=$2 + found=no + for runlevel in 3 4 5; do + for link in "$root/etc/rc$runlevel.d"/[SK]??xcatd; do + if [ -e "$link" ] || [ -L "$link" ]; then + found=yes + name=${link##*/} + case "$name" in + "$old_prefix"??xcatd) + sequence=${name#?} + mv -f "$link" \ + "$root/etc/rc$runlevel.d/${new_prefix}${sequence}" + ;; + esac + fi + done + done + [ "$found" = yes ] +} +case "${1:-}:${2:-}" in + -f:xcatd) + [ "${3:-}" = remove ] + remove_links + ;; + xcatd:defaults) + defaults + ;; + xcatd:disable) + toggle_links K S + [ ! -f "$root/fail-update-rc-disable" ] || exit 9 + ;; + xcatd:enable) + toggle_links S K + ;; + *) exit 2 ;; +esac +SH + ); + write_file( + File::Spec->catfile( $fake_bin, 'systemctl' ), + <<'SH', 0755 +#!/bin/sh +set -eu +root=${XCAT_COMPAT_ROOT:?} +case "${1:-}" in + --root=*) shift ;; +esac +[ "${1:-}" = is-enabled ] || exit 0 +if [ -r "$root/systemctl-state" ]; then + status=$(sed -n '1p' "$root/systemctl-state") +else + status=unknown +fi +printf '%s\n' "$status" +[ "$status" = enabled ] && exit 0 +exit 1 +SH + ); + write_file( + File::Spec->catfile( $fake_bin, 'mv' ), + <<'SH', 0755 +#!/bin/sh +set -eu +root=${XCAT_COMPAT_ROOT:?} +destination= +for argument in "$@"; do + destination=$argument +done +if [ -f "$root/fail-stash-move" ] && + [ "$destination" = "$root/var/lib/xcat/xcatd-init-state/xcatd" ]; then + exit 7 +fi +exec /bin/mv "$@" +SH + ); + + return $root; +} + +sub run_script { + my ( $root, $script, @arguments ) = @_; + local $ENV{XCAT_COMPAT_ROOT} = $root; + local $ENV{XCATROOT} = '/opt/xcat'; + local $ENV{PATH} = File::Spec->catdir( $root, 'test-bin' ) . ':/usr/bin:/bin'; + system( '/bin/sh', $script, @arguments ); + return $? >> 8; +} + +sub run_state { + my ( $root, @arguments ) = @_; + return run_script( $root, $state_helper, @arguments ); +} + +sub run_compat { + my ( $root, @arguments ) = @_; + return run_script( $root, $compat_helper, @arguments ); +} + +sub run_update_rc_status { + my ( $root, @arguments ) = @_; + my $update_rc = File::Spec->catfile( $root, 'test-bin', 'update-rc.d' ); + return run_script( $root, $update_rc, @arguments ); +} + +sub run_update_rc { + my ( $root, @arguments ) = @_; + my $status = run_update_rc_status( $root, @arguments ); + die "Fake update-rc.d failed with status $status" if $status; +} + +sub live_init { + my ($root) = @_; + return File::Spec->catfile( $root, 'etc', 'init.d', 'xcatd' ); +} + +sub state_dir { + my ($root) = @_; + return File::Spec->catdir( $root, 'var', 'lib', 'xcat', 'xcatd-init-state' ); +} + +sub staged_compat_helper { + my ($root) = @_; + return File::Spec->catfile( + $root, 'opt', 'xcat', 'share', 'xcat', 'scripts', + 'xcatd-init-compat' + ); +} + +sub old_systemd_marker { + my ($root) = @_; + return File::Spec->catfile( $root, 'var', 'lib', 'xcat', 'xcatd-systemd-mode' ); +} + +sub state_value { + my ( $root, $field ) = @_; + my $state = read_file( File::Spec->catfile( state_dir($root), 'state' ) ); + return $1 if $state =~ /^\Q$field\E=(.+)$/m; + return ''; +} + +sub set_init_target { + my ( $root, $target ) = @_; + my $init = File::Spec->catfile( $root, 'sbin', 'init' ); + unlink($init) if -e $init || -l $init; + symlink( $target, $init ) or die "Unable to stage init target: $!"; +} + +sub set_systemd_state { + my ( $root, $state ) = @_; + write_file( File::Spec->catfile( $root, 'systemctl-state' ), "$state\n" ); +} + +sub rc_link { + my ($root) = @_; + return File::Spec->catfile( $root, 'etc', 'rc3.d', 'S01xcatd' ); +} + +sub rc_kill_link { + my ($root) = @_; + return File::Spec->catfile( $root, 'etc', 'rc3.d', 'K01xcatd' ); +} + +sub registration_link_count { + my ($root) = @_; + my @links = glob( File::Spec->catfile( + $root, 'etc', 'rc?.d', '[SK]??xcatd' + ) ); + return scalar @links; +} + +my $update_rc_root = stage_root(); +is( run_update_rc_status( $update_rc_root, 'xcatd', 'enable' ), 1, + 'native enable rejects an unregistered service' ); +is( registration_link_count($update_rc_root), 0, + 'failed native enable does not invent registration links' ); +run_update_rc( $update_rc_root, 'xcatd', 'defaults' ); +is( registration_link_count($update_rc_root), 3, + 'native defaults creates only the three declared start links' ); +for my $runlevel ( 3, 4, 5 ) { + ok( -l File::Spec->catfile( + $update_rc_root, 'etc', "rc$runlevel.d", 'S01xcatd' + ), + "native defaults creates the runlevel $runlevel start link" ); +} +unlink File::Spec->catfile( $update_rc_root, 'etc', 'rc4.d', 'S01xcatd' ); +unlink File::Spec->catfile( $update_rc_root, 'etc', 'rc5.d', 'S01xcatd' ); +run_update_rc( $update_rc_root, 'xcatd', 'defaults' ); +is( registration_link_count($update_rc_root), 1, + 'native defaults preserves a partial existing registration' ); +run_update_rc( $update_rc_root, 'xcatd', 'disable' ); +ok( -l rc_kill_link($update_rc_root), + 'native disable converts only the existing start link' ); +is( registration_link_count($update_rc_root), 1, + 'native disable does not synthesize missing runlevels' ); +run_update_rc( $update_rc_root, '-f', 'xcatd', 'remove' ); +my $priority_link = File::Spec->catfile( + $update_rc_root, 'etc', 'rc4.d', 'K17xcatd' +); +symlink( '../init.d/xcatd', $priority_link ) + or die "Unable to stage custom-priority registration: $!"; +run_update_rc( $update_rc_root, 'xcatd', 'enable' ); +ok( -l File::Spec->catfile( + $update_rc_root, 'etc', 'rc4.d', 'S17xcatd' + ), + 'native enable preserves the existing link priority' ); +run_update_rc( $update_rc_root, '-f', 'xcatd', 'remove' ); +my $outside_link = File::Spec->catfile( + $update_rc_root, 'etc', 'rc2.d', 'S42xcatd' +); +make_path( File::Spec->catdir( $update_rc_root, 'etc', 'rc2.d' ) ); +symlink( '../init.d/xcatd', $outside_link ) + or die "Unable to stage outside-runlevel registration: $!"; +is( run_update_rc_status( $update_rc_root, 'xcatd', 'disable' ), 1, + 'native disable rejects links outside the declared start runlevels' ); +ok( -l $outside_link, + 'a rejected native toggle preserves the outside-runlevel link' ); + +my $round_trip_root = stage_root(); +set_init_target( $round_trip_root, 'upstart' ); +my $saved_umask = umask 0022; +my $fresh_configure_status = + run_state( $round_trip_root, 'configure-legacy', 'fresh' ); +umask $saved_umask; +is( $fresh_configure_status, 0, + 'fresh legacy configuration succeeds' ); +is( path_mode( state_dir($round_trip_root) ), 0700, + 'persistent init state remains private' ); +is( path_mode( File::Spec->catfile( state_dir($round_trip_root), 'state' ) ), + 0600, 'the init state file remains private' ); +is( state_value( $round_trip_root, 'format' ), '2', + 'new init state is written in registration-aware format 2' ); +is( path_mode( File::Spec->catdir( $round_trip_root, 'etc', 'init.d' ) ), + 0755, 'state writes do not leak their private umask into init directories' ); +ok( -x live_init($round_trip_root), + 'fresh legacy configuration materializes the init script' ); +ok( -l rc_link($round_trip_root), + 'fresh legacy configuration enables the init script' ); + +write_file( live_init($round_trip_root), "administrator customization\n", 0755 ); +is( run_state( $round_trip_root, 'prepare-systemd', 'upgrade' ), 0, + 'legacy to systemd preparation succeeds' ); +is( state_value( $round_trip_root, 'content' ), 'stashed', + 'a customized init script is stashed before removal' ); +is( state_value( $round_trip_root, 'enabled' ), 'yes', + 'SysV enablement is captured before removal' ); +set_init_target( $round_trip_root, '../lib/systemd/systemd' ); +is( run_compat( $round_trip_root, 'configure', '--explicit-target' ), 0, + 'systemd compatibility configuration succeeds' ); +is( run_state( $round_trip_root, 'commit-systemd' ), 0, + 'systemd transition state commits' ); +ok( !-e live_init($round_trip_root), + 'systemd mode omits the live legacy script' ); +like( read_file( File::Spec->catfile( state_dir($round_trip_root), 'xcatd' ) ), + qr/administrator customization/, + 'the exact customized script remains in the durable stash' ); + +set_systemd_state( $round_trip_root, 'enabled' ); +set_init_target( $round_trip_root, 'upstart' ); +is( run_state( $round_trip_root, 'configure-legacy', 'upgrade' ), 0, + 'systemd to legacy restoration succeeds' ); +is( read_file( live_init($round_trip_root) ), "administrator customization\n", + 'the customized init script survives a mode round trip' ); +ok( -l rc_link($round_trip_root), + 'systemd enablement maps back to SysV registration' ); +is( state_value( $round_trip_root, 'mode' ), 'legacy', + 'the completed restoration records legacy mode' ); + +unlink( live_init($round_trip_root) ) + or die "Unable to stage administrator deletion: $!"; +is( run_state( $round_trip_root, 'prepare-systemd', 'upgrade' ), 0, + 'deleted legacy state prepares for systemd' ); +is( state_value( $round_trip_root, 'content' ), 'deleted', + 'administrator deletion is recorded separately from package omission' ); +set_init_target( $round_trip_root, '../lib/systemd/systemd' ); +is( run_state( $round_trip_root, 'commit-systemd' ), 0, + 'deleted state commits in systemd mode' ); +set_systemd_state( $round_trip_root, 'disabled' ); +set_init_target( $round_trip_root, 'upstart' ); +is( run_state( $round_trip_root, 'configure-legacy', 'upgrade' ), 0, + 'deleted state returns to legacy mode' ); +ok( !-e live_init($round_trip_root), + 'administrator deletion survives a mode round trip' ); +ok( !-e rc_link($round_trip_root), + 'a missing legacy script is not registered' ); + +my $disabled_root = stage_root(); +set_init_target( $disabled_root, 'upstart' ); +is( run_state( $disabled_root, 'configure-legacy', 'fresh' ), 0, + 'disabled fixture starts in legacy mode' ); +run_update_rc( $disabled_root, 'xcatd', 'disable' ); +ok( -l rc_kill_link($disabled_root), + 'disabled fixture remains registered through a kill link' ); +write_file( live_init($disabled_root), "disabled customization\n", 0755 ); +is( run_state( $disabled_root, 'prepare-systemd', 'upgrade' ), 0, + 'disabled legacy fixture prepares for systemd' ); +is( state_value( $disabled_root, 'enabled' ), 'no', + 'disabled SysV state is captured' ); +set_init_target( $disabled_root, '../lib/systemd/systemd' ); +is( run_compat( $disabled_root, 'configure', '--explicit-target' ), 0, + 'disabled systemd fixture removes the live script' ); +is( run_state( $disabled_root, 'commit-systemd' ), 0, + 'disabled systemd state commits' ); +set_systemd_state( $disabled_root, 'disabled' ); +set_init_target( $disabled_root, 'upstart' ); +is( run_state( $disabled_root, 'configure-legacy', 'upgrade' ), 0, + 'disabled systemd fixture returns to legacy' ); +is( read_file( live_init($disabled_root) ), "disabled customization\n", + 'disabled customization is restored' ); +ok( !-e rc_link($disabled_root), + 'disabled systemd state stays disabled under SysV' ); +ok( -l rc_kill_link($disabled_root), + 'registered-disabled SysV state restores its kill link' ); +run_update_rc( $disabled_root, 'xcatd', 'enable' ); +ok( -l rc_link($disabled_root) && !-e rc_kill_link($disabled_root), + 'a restored registered-disabled service can be enabled later' ); + +my $outside_disabled_root = stage_root(); +set_init_target( $outside_disabled_root, 'upstart' ); +is( run_state( $outside_disabled_root, 'configure-legacy', 'fresh' ), 0, + 'outside-link fixture starts in legacy mode' ); +run_update_rc( $outside_disabled_root, 'xcatd', 'disable' ); +write_file( live_init($outside_disabled_root), + "outside-link customization\n", 0755 ); +is( run_state( $outside_disabled_root, 'prepare-systemd', 'upgrade' ), 0, + 'outside-link fixture prepares for systemd' ); +set_init_target( $outside_disabled_root, '../lib/systemd/systemd' ); +is( run_compat( $outside_disabled_root, 'configure', '--explicit-target' ), 0, + 'outside-link fixture configures systemd mode' ); +is( run_state( $outside_disabled_root, 'commit-systemd' ), 0, + 'outside-link systemd state commits' ); +set_systemd_state( $outside_disabled_root, 'disabled' ); +run_update_rc( $outside_disabled_root, '-f', 'xcatd', 'remove' ); +my $outside_disabled_link = File::Spec->catfile( + $outside_disabled_root, 'etc', 'rc2.d', 'K42xcatd' +); +make_path( File::Spec->catdir( $outside_disabled_root, 'etc', 'rc2.d' ) ); +symlink( '../init.d/xcatd', $outside_disabled_link ) + or die "Unable to stage rejected disabled registration: $!"; +set_init_target( $outside_disabled_root, 'upstart' ); +my $disable_failure = File::Spec->catfile( + $outside_disabled_root, 'fail-update-rc-disable' +); +write_file( $disable_failure, "fail rebuilt disable\n", 0644 ); +is( run_state( $outside_disabled_root, 'configure-legacy', 'upgrade' ), 9, + 'a failed rebuilt disable propagates from restoration' ); +is( state_value( $outside_disabled_root, 'mode' ), 'transition-legacy', + 'a failed rebuilt disable leaves retryable transition state' ); +ok( -e File::Spec->catfile( state_dir($outside_disabled_root), 'xcatd' ), + 'a failed rebuilt disable retains the durable script stash' ); +unlink($disable_failure) + or die "Unable to clear rebuilt-disable failure: $!"; +is( run_state( $outside_disabled_root, 'configure-legacy', 'upgrade' ), 0, + 'a rejected outside-runlevel layout is rebuilt during restoration' ); +ok( !-e $outside_disabled_link, + 'the unusable outside-runlevel link is removed' ); +is( registration_link_count($outside_disabled_root), 3, + 'the rejected layout becomes a complete registered-disabled layout' ); +ok( -l rc_kill_link($outside_disabled_root), + 'the rebuilt layout contains a declared-runlevel kill link' ); +is( state_value( $outside_disabled_root, 'enabled' ), 'no', + 'the rebuilt layout retains registered-disabled state' ); + +my $unregistered_root = stage_root(); +set_init_target( $unregistered_root, 'upstart' ); +is( run_state( $unregistered_root, 'configure-legacy', 'fresh' ), 0, + 'unregistered fixture starts in legacy mode' ); +run_update_rc( $unregistered_root, '-f', 'xcatd', 'remove' ); +write_file( live_init($unregistered_root), "unregistered customization\n", 0755 ); +is( run_state( $unregistered_root, 'prepare-systemd', 'upgrade' ), 0, + 'unregistered legacy fixture prepares for systemd' ); +is( state_value( $unregistered_root, 'enabled' ), 'unregistered', + 'an absent SysV registration is distinct from registered-disabled' ); +set_init_target( $unregistered_root, '../lib/systemd/systemd' ); +is( run_compat( $unregistered_root, 'configure', '--explicit-target' ), 0, + 'unregistered systemd fixture removes the live script' ); +is( run_state( $unregistered_root, 'commit-systemd' ), 0, + 'unregistered systemd state commits' ); +set_systemd_state( $unregistered_root, 'disabled' ); +is( run_state( $unregistered_root, 'prepare-systemd', 'upgrade' ), 0, + 'unregistered state survives a same-systemd upgrade preparation' ); +is( state_value( $unregistered_root, 'enabled' ), 'unregistered', + 'same-systemd disablement does not invent registered-disabled provenance' ); +is( run_state( $unregistered_root, 'commit-systemd' ), 0, + 'unregistered same-systemd upgrade commits' ); +set_init_target( $unregistered_root, 'upstart' ); +is( run_state( $unregistered_root, 'configure-legacy', 'upgrade' ), 0, + 'unregistered systemd fixture returns to legacy' ); +is( read_file( live_init($unregistered_root) ), + "unregistered customization\n", + 'unregistered customization is restored' ); +is( registration_link_count($unregistered_root), 0, + 'unregistered state returns without start or kill links' ); +is( state_value( $unregistered_root, 'enabled' ), 'unregistered', + 'completed state retains unregistered metadata' ); + +my $rpm_layout_root = stage_root(); +set_init_target( $rpm_layout_root, 'upstart' ); +is( run_state( $rpm_layout_root, 'configure-legacy', 'fresh' ), 0, + 'RPM-layout fixture starts in legacy mode' ); +run_update_rc( $rpm_layout_root, '-f', 'xcatd', 'remove' ); +my $rpm_runlevel_dir = + File::Spec->catdir( $rpm_layout_root, 'etc', 'rc.d', 'rc3.d' ); +make_path($rpm_runlevel_dir); +symlink( '/etc/init.d/xcatd', + File::Spec->catfile( $rpm_runlevel_dir, 'S85xcatd' ) ) + or die "Unable to stage RPM-only registration: $!"; +is( run_state( $rpm_layout_root, 'prepare-systemd', 'upgrade' ), 0, + 'RPM-only registration prepares for a Debian systemd transition' ); +is( state_value( $rpm_layout_root, 'enabled' ), 'unregistered', + 'RPM-only links do not become Debian SysV enablement evidence' ); + +my $missing_update_rc_root = stage_root(); +unlink( File::Spec->catfile( + $missing_update_rc_root, 'test-bin', 'update-rc.d' + ) ) + or die "Unable to remove fake update-rc.d: $!"; +set_init_target( $missing_update_rc_root, 'upstart' ); +is( run_state( $missing_update_rc_root, 'configure-legacy', 'fresh' ), 0, + 'fresh legacy configuration tolerates a missing update-rc.d' ); +ok( -x live_init($missing_update_rc_root), + 'missing registration tooling does not prevent script materialization' ); +is( registration_link_count($missing_update_rc_root), 0, + 'missing registration tooling leaves runlevel links untouched' ); + +my $failed_stash_root = stage_root(); +set_init_target( $failed_stash_root, 'upstart' ); +is( run_state( $failed_stash_root, 'configure-legacy', 'fresh' ), 0, + 'failed-stash fixture starts in legacy mode' ); +write_file( live_init($failed_stash_root), "failed-stash customization\n", 0755 ); +write_file( File::Spec->catfile( $failed_stash_root, 'fail-stash-move' ), + "fail\n" ); +isnt( run_state( $failed_stash_root, 'prepare-systemd', 'upgrade' ), 0, + 'a failed atomic stash move is reported' ); +my @failed_stash_temps = glob( File::Spec->catfile( + state_dir($failed_stash_root), 'xcatd.tmp.*' +) ); +is( scalar @failed_stash_temps, 0, + 'a failed atomic stash move removes its temporary file' ); +is( read_file( live_init($failed_stash_root) ), + "failed-stash customization\n", + 'a failed stash leaves the live customization untouched' ); +is( state_value( $failed_stash_root, 'mode' ), 'legacy', + 'a failed stash does not advance durable transition state' ); + +my $missing_helper_root = stage_root(); +unlink( staged_compat_helper($missing_helper_root) ) + or die "Unable to remove staged compatibility helper: $!"; +isnt( run_state( $missing_helper_root, 'prepare-systemd', 'fresh' ), 0, + 'fresh systemd preparation fails without the shared detector' ); +ok( !-e File::Spec->catfile( state_dir($missing_helper_root), 'state' ), + 'missing shared detection cannot advance durable systemd state' ); +ok( !-e live_init($missing_helper_root), + 'missing shared detection cannot materialize legacy content' ); + +my $failing_helper_root = stage_root(); +write_file( staged_compat_helper($failing_helper_root), + "#!/bin/sh\nexit 7\n", 0755 ); +set_init_target( $failing_helper_root, 'upstart' ); +isnt( run_state( $failing_helper_root, 'configure-legacy', 'fresh' ), 0, + 'fresh legacy configuration propagates shared detector failure' ); +ok( !-e File::Spec->catfile( state_dir($failing_helper_root), 'state' ), + 'failing shared detection cannot advance durable legacy state' ); +ok( !-e live_init($failing_helper_root), + 'failing shared detection cannot materialize the init script' ); +is( registration_link_count($failing_helper_root), 0, + 'failing shared detection cannot create registration links' ); + +my $malformed_systemd_helper_root = stage_root(); +write_file( staged_compat_helper($malformed_systemd_helper_root), + "#!/bin/sh\nprintf '%s\\n' malformed\n", 0755 ); +isnt( run_state( $malformed_systemd_helper_root, 'prepare-systemd', 'fresh' ), + 0, 'successful malformed systemd detection fails closed' ); +ok( !-e File::Spec->catfile( + state_dir($malformed_systemd_helper_root), 'state' + ), + 'malformed systemd detection cannot advance durable state' ); +ok( !-e live_init($malformed_systemd_helper_root), + 'malformed systemd detection cannot materialize legacy content' ); +is( registration_link_count($malformed_systemd_helper_root), 0, + 'malformed systemd detection cannot create registration links' ); + +my $malformed_legacy_helper_root = stage_root(); +set_init_target( $malformed_legacy_helper_root, 'upstart' ); +is( run_state( $malformed_legacy_helper_root, 'configure-legacy', 'fresh' ), 0, + 'malformed legacy fixture starts in stable legacy mode' ); +write_file( live_init($malformed_legacy_helper_root), + "malformed helper survivor\n", 0755 ); +my $legacy_state_before = read_file( File::Spec->catfile( + state_dir($malformed_legacy_helper_root), 'state' +) ); +my $legacy_content_before = read_file( live_init($malformed_legacy_helper_root) ); +my $legacy_links_before = registration_link_count($malformed_legacy_helper_root); +write_file( staged_compat_helper($malformed_legacy_helper_root), <<'SH', 0755 ); +#!/bin/sh +case "${1:-}" in + systemd-state) printf '%s\n' disabled ;; + debian-legacy-state) printf '%s\n' malformed ;; + *) exit 7 ;; +esac +SH +isnt( run_state( $malformed_legacy_helper_root, 'configure-legacy', 'upgrade' ), + 0, 'successful malformed legacy detection fails closed' ); +is( read_file( File::Spec->catfile( + state_dir($malformed_legacy_helper_root), 'state' + ) ), + $legacy_state_before, 'malformed legacy detection preserves durable state' ); +is( read_file( live_init($malformed_legacy_helper_root) ), + $legacy_content_before, 'malformed legacy detection preserves live content' ); +is( registration_link_count($malformed_legacy_helper_root), + $legacy_links_before, 'malformed legacy detection preserves registration' ); + +my $legacy_reinstall_root = stage_root(); +set_init_target( $legacy_reinstall_root, 'upstart' ); +is( run_state( $legacy_reinstall_root, 'configure-legacy', 'fresh' ), 0, + 'legacy reinstall fixture starts enabled' ); +run_update_rc( $legacy_reinstall_root, '-f', 'xcatd', 'remove' ); +is( run_state( $legacy_reinstall_root, 'configure-legacy', 'fresh' ), 0, + 'legacy remove and reinstall configures as fresh' ); +ok( -l rc_link($legacy_reinstall_root), + 'legacy remove and reinstall restores default enablement' ); + +my $masked_transition_root = stage_root(); +set_init_target( $masked_transition_root, 'upstart' ); +is( run_state( $masked_transition_root, 'configure-legacy', 'fresh' ), 0, + 'masked transition fixture starts enabled in legacy mode' ); +my $masked_transition_unit_dir = + File::Spec->catdir( $masked_transition_root, 'etc', 'systemd', 'system' ); +make_path($masked_transition_unit_dir); +symlink( '/dev/null', + File::Spec->catfile( $masked_transition_unit_dir, 'xcatd.service' ) ) + or die "Unable to stage transition mask: $!"; +is( run_state( $masked_transition_root, 'prepare-systemd', 'upgrade' ), 0, + 'enabled legacy state prepares against a systemd mask' ); +is( state_value( $masked_transition_root, 'enabled' ), 'unregistered', + 'an explicit systemd mask overrides legacy registration' ); +is( state_value( $masked_transition_root, 'origin' ), 'systemd', + 'masked transition suppresses postinst enablement mutation' ); + +my $late_mask_root = stage_root(); +set_init_target( $late_mask_root, 'upstart' ); +is( run_state( $late_mask_root, 'configure-legacy', 'fresh' ), 0, + 'late-mask fixture starts enabled in legacy mode' ); +is( run_state( $late_mask_root, 'prepare-systemd', 'upgrade' ), 0, + 'late-mask fixture begins a systemd transition' ); +my $late_mask_unit_dir = + File::Spec->catdir( $late_mask_root, 'etc', 'systemd', 'system' ); +make_path($late_mask_unit_dir); +symlink( '/dev/null', + File::Spec->catfile( $late_mask_unit_dir, 'xcatd.service' ) ) + or die "Unable to stage mask during transition: $!"; +is( run_state( $late_mask_root, 'prepare-systemd', 'upgrade' ), 0, + 'systemd transition retries after a mask is added' ); +is( state_value( $late_mask_root, 'enabled' ), 'unregistered', + 'a newly added mask overrides preserved transition enablement' ); +is( state_value( $late_mask_root, 'origin' ), 'systemd', + 'a newly added mask suppresses retry enablement mutation' ); + +my $fresh_systemd_root = stage_root(); +set_systemd_state( $fresh_systemd_root, 'disabled' ); +is( run_state( $fresh_systemd_root, 'prepare-systemd', 'fresh' ), 0, + 'fresh systemd preparation succeeds for a newly unpacked disabled unit' ); +is( state_value( $fresh_systemd_root, 'enabled' ), 'yes', + 'fresh systemd installs remain enabled by default' ); +is( state_value( $fresh_systemd_root, 'origin' ), 'fresh', + 'fresh systemd state records a fresh transition' ); + +for my $link_case ( + [ 'persistent wants', qw(etc wants) ], + [ 'persistent requires', qw(etc requires) ], + [ 'runtime wants', qw(run wants) ], + [ 'runtime requires', qw(run requires) ], +) { + my ( $label, $scope, $relation ) = @{$link_case}; + my $link_root = stage_root(); + my $link_dir = File::Spec->catdir( + $link_root, $scope, 'systemd', 'system', + "multi-user.target.$relation" + ); + make_path($link_dir); + symlink( + '/usr/lib/systemd/system/xcatd.service', + File::Spec->catfile( $link_dir, 'xcatd.service' ) + ) or die "Unable to stage $label link: $!"; + set_systemd_state( $link_root, 'unknown' ); + is( run_state( $link_root, 'prepare-systemd', 'upgrade' ), 0, + "$label enablement prepares successfully" ); + is( state_value( $link_root, 'enabled' ), 'yes', + "$label enablement is preserved" ); +} + +my $masked_root = stage_root(); +my $masked_unit_dir = File::Spec->catdir( $masked_root, 'etc', 'systemd', 'system' ); +make_path($masked_unit_dir); +symlink( '/dev/null', File::Spec->catfile( $masked_unit_dir, 'xcatd.service' ) ) + or die "Unable to stage masked unit: $!"; +make_path( File::Spec->catdir( $masked_root, 'var', 'lib', 'xcat' ) ); +write_file( old_systemd_marker($masked_root), "marker\n" ); +is( run_state( $masked_root, 'prepare-systemd', 'upgrade' ), 0, + 'masked systemd preparation succeeds' ); +is( state_value( $masked_root, 'content' ), 'package-default', + 'an obsolete package conffile marker retains package omission semantics' ); +is( state_value( $masked_root, 'enabled' ), 'unregistered', + 'a masked unit is not treated as boot-enabled' ); + +my $fresh_masked_root = stage_root(); +my $fresh_masked_unit_dir = + File::Spec->catdir( $fresh_masked_root, 'etc', 'systemd', 'system' ); +make_path($fresh_masked_unit_dir); +symlink( '/dev/null', + File::Spec->catfile( $fresh_masked_unit_dir, 'xcatd.service' ) ) + or die "Unable to stage fresh masked unit: $!"; +is( run_state( $fresh_masked_root, 'prepare-systemd', 'fresh' ), 0, + 'fresh masked systemd preparation succeeds' ); +is( state_value( $fresh_masked_root, 'enabled' ), 'unregistered', + 'a pre-existing mask is not overridden on fresh install' ); + +my $runtime_masked_root = stage_root(); +my $runtime_masked_unit_dir = + File::Spec->catdir( $runtime_masked_root, 'run', 'systemd', 'system' ); +make_path($runtime_masked_unit_dir); +symlink( '/dev/null', + File::Spec->catfile( $runtime_masked_unit_dir, 'xcatd.service' ) ) + or die "Unable to stage runtime masked unit: $!"; +set_systemd_state( $runtime_masked_root, 'unknown' ); +is( run_state( $runtime_masked_root, 'prepare-systemd', 'fresh' ), 0, + 'runtime masked systemd preparation succeeds' ); +is( state_value( $runtime_masked_root, 'enabled' ), 'unregistered', + 'a runtime mask is not treated as enabled offline' ); +is( state_value( $runtime_masked_root, 'origin' ), 'systemd', + 'a runtime mask overrides fresh-install enablement' ); + +my $pending_deleted_root = stage_root(); +make_path( state_dir($pending_deleted_root) ); +write_file( File::Spec->catfile( state_dir($pending_deleted_root), 'pending-deleted' ), "\n" ); +write_file( File::Spec->catfile( state_dir($pending_deleted_root), 'pending-enabled' ), "no\n" ); +set_systemd_state( $pending_deleted_root, 'enabled' ); +is( run_state( $pending_deleted_root, 'prepare-systemd', 'upgrade' ), 0, + 'explicit preinst deletion evidence prepares successfully' ); +is( state_value( $pending_deleted_root, 'content' ), 'deleted', + 'a still-owned missing conffile remains an administrator deletion' ); +is( state_value( $pending_deleted_root, 'enabled' ), 'yes', + 'existing native-systemd enablement survives legacy deletion evidence' ); +is( state_value( $pending_deleted_root, 'origin' ), 'legacy', + 'explicit conffile deletion retains legacy provenance' ); + +my $pending_enabled_root = stage_root(); +make_path( state_dir($pending_enabled_root) ); +write_file( + File::Spec->catfile( state_dir($pending_enabled_root), 'pending-xcatd' ), + "legacy content\n", 0755 +); +write_file( + File::Spec->catfile( state_dir($pending_enabled_root), 'pending-enabled' ), + "yes\n" +); +set_systemd_state( $pending_enabled_root, 'disabled' ); +is( run_state( $pending_enabled_root, 'prepare-systemd', 'upgrade' ), 0, + 'enabled legacy evidence prepares with disabled systemd state' ); +is( state_value( $pending_enabled_root, 'enabled' ), 'yes', + 'first migration preserves enablement from either service manager' ); + +my $both_disabled_root = stage_root(); +make_path( state_dir($both_disabled_root) ); +write_file( + File::Spec->catfile( state_dir($both_disabled_root), 'pending-xcatd' ), + "legacy content\n", 0755 +); +write_file( + File::Spec->catfile( state_dir($both_disabled_root), 'pending-enabled' ), + "no\n" +); +set_systemd_state( $both_disabled_root, 'disabled' ); +is( run_state( $both_disabled_root, 'prepare-systemd', 'upgrade' ), 0, + 'fully disabled first migration prepares successfully' ); +is( state_value( $both_disabled_root, 'enabled' ), 'no', + 'first migration remains disabled when both managers are disabled' ); + +my $masked_pending_root = stage_root(); +make_path( + state_dir($masked_pending_root), + File::Spec->catdir( $masked_pending_root, 'etc', 'systemd', 'system' ) +); +write_file( + File::Spec->catfile( state_dir($masked_pending_root), 'pending-xcatd' ), + "legacy content\n", 0755 +); +write_file( + File::Spec->catfile( state_dir($masked_pending_root), 'pending-enabled' ), + "yes\n" +); +symlink( '/dev/null', + File::Spec->catfile( $masked_pending_root, 'etc', 'systemd', 'system', + 'xcatd.service' ) ) + or die "Unable to stage masked first migration: $!"; +is( run_state( $masked_pending_root, 'prepare-systemd', 'upgrade' ), 0, + 'masked first migration prepares successfully' ); +is( state_value( $masked_pending_root, 'enabled' ), 'unregistered', + 'an explicit systemd mask wins over stale legacy enablement' ); + +my $obsolete_disabled_root = stage_root(); +make_path( File::Spec->catdir( $obsolete_disabled_root, 'var', 'lib', 'xcat' ) ); +write_file( old_systemd_marker($obsolete_disabled_root), "marker\n" ); +set_systemd_state( $obsolete_disabled_root, 'disabled' ); +is( run_state( $obsolete_disabled_root, 'prepare-systemd', 'upgrade' ), 0, + 'disabled obsolete-conffile migration prepares successfully' ); +is( state_value( $obsolete_disabled_root, 'content' ), 'package-default', + 'package-driven omission is not confused with administrator deletion' ); +is( state_value( $obsolete_disabled_root, 'enabled' ), 'no', + 'disabled markerless systemd state remains disabled' ); +is( run_state( $obsolete_disabled_root, 'commit-systemd' ), 0, + 'disabled markerless systemd state commits' ); +set_init_target( $obsolete_disabled_root, 'upstart' ); +is( run_state( $obsolete_disabled_root, 'configure-legacy', 'upgrade' ), 0, + 'disabled package omission returns to legacy mode' ); +ok( -x live_init($obsolete_disabled_root), + 'package omission restores the packaged legacy script' ); +ok( !-e rc_link($obsolete_disabled_root), + 'disabled package omission remains off under SysV' ); +ok( -l rc_kill_link($obsolete_disabled_root), + 'native systemd disablement maps to registered-disabled SysV state' ); + +my $same_systemd_root = stage_root(); +set_systemd_state( $same_systemd_root, 'disabled' ); +is( run_state( $same_systemd_root, 'prepare-systemd', 'fresh' ), 0, + 'same-systemd fixture prepares as fresh' ); +is( run_state( $same_systemd_root, 'commit-systemd' ), 0, + 'same-systemd fixture commits its initial state' ); +set_systemd_state( $same_systemd_root, 'enabled' ); +is( run_state( $same_systemd_root, 'prepare-systemd', 'upgrade' ), 0, + 'same-systemd upgrade preparation succeeds' ); +is( state_value( $same_systemd_root, 'origin' ), 'systemd', + 'same-systemd upgrades are distinguishable from mode changes' ); + +my $reinstall_state_root = stage_root(); +set_init_target( $reinstall_state_root, 'upstart' ); +is( run_state( $reinstall_state_root, 'configure-legacy', 'fresh' ), 0, + 'reinstall-state fixture starts in legacy mode' ); +write_file( live_init($reinstall_state_root), "reinstall customization\n", 0755 ); +is( run_state( $reinstall_state_root, 'prepare-systemd', 'upgrade' ), 0, + 'reinstall-state fixture stashes its customization' ); +set_init_target( $reinstall_state_root, '../lib/systemd/systemd' ); +is( run_compat( $reinstall_state_root, 'configure', '--explicit-target' ), 0, + 'reinstall-state fixture enters systemd mode' ); +is( run_state( $reinstall_state_root, 'commit-systemd' ), 0, + 'reinstall-state fixture commits durable systemd state' ); +write_file( + File::Spec->catfile( state_dir($reinstall_state_root), 'pending-deleted' ), + "stale\n" +); +write_file( + File::Spec->catfile( state_dir($reinstall_state_root), 'pending-enabled' ), + "no\n" +); +set_systemd_state( $reinstall_state_root, 'disabled' ); +is( run_state( $reinstall_state_root, 'prepare-systemd', 'fresh' ), 0, + 'remove and reinstall preparation uses durable state' ); +is( state_value( $reinstall_state_root, 'content' ), 'stashed', + 'stale fallback deletion evidence cannot replace a durable stash' ); +is( state_value( $reinstall_state_root, 'enabled' ), 'yes', + 'a genuine reinstall restores the default enabled state' ); +is( read_file( File::Spec->catfile( state_dir($reinstall_state_root), 'xcatd' ) ), + "reinstall customization\n", + 'remove and reinstall preserves the exact administrator customization' ); + +my $stale_pending_root = stage_root(); +set_init_target( $stale_pending_root, 'upstart' ); +is( run_state( $stale_pending_root, 'configure-legacy', 'fresh' ), 0, + 'stale-pending fixture starts in legacy mode' ); +write_file( live_init($stale_pending_root), "current live content\n", 0755 ); +write_file( + File::Spec->catfile( state_dir($stale_pending_root), 'pending-xcatd' ), + "stale pending content\n", 0755 +); +is( run_state( $stale_pending_root, 'prepare-systemd', 'upgrade' ), 0, + 'stable legacy state ignores stale pending evidence' ); +is( read_file( File::Spec->catfile( state_dir($stale_pending_root), 'xcatd' ) ), + "current live content\n", + 'current live content wins over stale pending content' ); +ok( !-e File::Spec->catfile( state_dir($stale_pending_root), 'pending-xcatd' ), + 'successful systemd preparation clears stale pending evidence' ); + +my $pending_cleanup_root = stage_root(); +make_path( state_dir($pending_cleanup_root) ); +for my $pending_name (qw(pending-xcatd pending-deleted pending-enabled)) { + write_file( + File::Spec->catfile( state_dir($pending_cleanup_root), $pending_name ), + "stale\n", 0755 + ); +} +set_init_target( $pending_cleanup_root, 'upstart' ); +is( run_state( $pending_cleanup_root, 'configure-legacy', 'fresh' ), 0, + 'legacy convergence succeeds with stale pending evidence' ); +for my $pending_name (qw(pending-xcatd pending-deleted pending-enabled)) { + ok( !-e File::Spec->catfile( state_dir($pending_cleanup_root), $pending_name ), + "legacy convergence clears $pending_name" ); +} + +my $legacy_retry_root = stage_root(); +set_init_target( $legacy_retry_root, 'upstart' ); +is( run_state( $legacy_retry_root, 'configure-legacy', 'fresh' ), 0, + 'same-mode retry fixture starts in legacy mode' ); +run_update_rc( $legacy_retry_root, 'xcatd', 'disable' ); +is( run_state( $legacy_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'disabled same-mode state is recorded' ); +set_systemd_state( $legacy_retry_root, 'enabled' ); +write_file( live_init($legacy_retry_root), "same-mode original\n", 0755 ); +write_file( File::Spec->catfile( $legacy_retry_root, 'mutate-ucf' ), "mutate\n" ); +write_file( File::Spec->catfile( $legacy_retry_root, 'fail-ucfr' ), "fail\n" ); +isnt( run_state( $legacy_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'same-mode failure after UCF mutation is reported' ); +is( state_value( $legacy_retry_root, 'origin' ), 'legacy', + 'failed same-mode configuration retains its legacy origin' ); +is( read_file( File::Spec->catfile( state_dir($legacy_retry_root), 'xcatd' ) ), + "same-mode original\n", + 'same-mode failure retains an exact transactional stash' ); +unlink( File::Spec->catfile( $legacy_retry_root, 'mutate-ucf' ) ) + or die "Unable to clear same-mode UCF mutation: $!"; +unlink( File::Spec->catfile( $legacy_retry_root, 'fail-ucfr' ) ) + or die "Unable to clear same-mode ucfr failure: $!"; +is( run_state( $legacy_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'same-mode legacy retry succeeds' ); +is( read_file( live_init($legacy_retry_root) ), "same-mode original\n", + 'same-mode retry restores exact pre-failure content' ); +ok( !-e rc_link($legacy_retry_root), + 'same-mode retry does not import stale systemd enablement' ); + +my $admin_disabled_retry_root = stage_root(); +set_init_target( $admin_disabled_retry_root, 'upstart' ); +is( run_state( $admin_disabled_retry_root, 'configure-legacy', 'fresh' ), 0, + 'admin-disabled retry fixture starts enabled' ); +run_update_rc( $admin_disabled_retry_root, 'xcatd', 'disable' ); +write_file( + File::Spec->catfile( $admin_disabled_retry_root, 'fail-ucfr' ), + "fail\n" +); +isnt( run_state( $admin_disabled_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'same-mode upgrade fails after current disablement is sampled' ); +is( state_value( $admin_disabled_retry_root, 'enabled' ), 'no', + 'failed same-mode upgrade records current disabled rc state' ); +unlink( File::Spec->catfile( $admin_disabled_retry_root, 'fail-ucfr' ) ) + or die "Unable to clear admin-disabled retry failure: $!"; +is( run_state( $admin_disabled_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'admin-disabled same-mode upgrade retries' ); +ok( !-e rc_link($admin_disabled_retry_root), + 'retry does not undo a current administrator disablement' ); + +my $admin_enabled_retry_root = stage_root(); +set_init_target( $admin_enabled_retry_root, 'upstart' ); +is( run_state( $admin_enabled_retry_root, 'configure-legacy', 'fresh' ), 0, + 'admin-enabled retry fixture starts enabled' ); +run_update_rc( $admin_enabled_retry_root, 'xcatd', 'disable' ); +is( run_state( $admin_enabled_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'disabled baseline is recorded' ); +make_path( File::Spec->catdir( $admin_enabled_retry_root, 'etc', 'rc2.d' ) ); +symlink( '../init.d/xcatd', rc_link($admin_enabled_retry_root) ) + or die "Unable to stage current administrator enablement: $!"; +write_file( + File::Spec->catfile( $admin_enabled_retry_root, 'fail-ucfr' ), + "fail\n" +); +isnt( run_state( $admin_enabled_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'same-mode upgrade fails after current enablement is sampled' ); +is( state_value( $admin_enabled_retry_root, 'enabled' ), 'yes', + 'failed same-mode upgrade records current enabled rc state' ); +unlink( File::Spec->catfile( $admin_enabled_retry_root, 'fail-ucfr' ) ) + or die "Unable to clear admin-enabled retry failure: $!"; +is( run_state( $admin_enabled_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'admin-enabled same-mode upgrade retries' ); +ok( -l rc_link($admin_enabled_retry_root), + 'retry does not undo a current administrator enablement' ); + +my $markerless_enabled_root = stage_root(); +set_systemd_state( $markerless_enabled_root, 'enabled' ); +set_init_target( $markerless_enabled_root, 'upstart' ); +is( run_state( $markerless_enabled_root, 'configure-legacy', 'upgrade' ), 0, + 'enabled markerless migration succeeds' ); +ok( -x live_init($markerless_enabled_root), + 'strong systemd enablement evidence restores the packaged script' ); +ok( -l rc_link($markerless_enabled_root), + 'strong systemd enablement evidence restores SysV registration' ); + +my $markerless_unknown_root = stage_root(); +set_systemd_state( $markerless_unknown_root, 'unknown' ); +set_init_target( $markerless_unknown_root, 'upstart' ); +is( run_state( $markerless_unknown_root, 'configure-legacy', 'upgrade' ), 0, + 'ambiguous markerless migration converges safely' ); +ok( !-e live_init($markerless_unknown_root), + 'ambiguous absence is not silently resurrected' ); +ok( !-e rc_link($markerless_unknown_root), + 'ambiguous enablement defaults to disabled' ); + +my $legacy_target_obsolete_root = stage_root(); +make_path( File::Spec->catdir( $legacy_target_obsolete_root, 'var', 'lib', 'xcat' ) ); +write_file( old_systemd_marker($legacy_target_obsolete_root), "marker\n" ); +set_systemd_state( $legacy_target_obsolete_root, 'disabled' ); +set_init_target( $legacy_target_obsolete_root, 'upstart' ); +is( run_state( $legacy_target_obsolete_root, 'configure-legacy', 'upgrade' ), 0, + 'obsolete conffile migrates directly to a legacy target' ); +ok( -x live_init($legacy_target_obsolete_root), + 'direct legacy migration restores package-driven omission' ); +ok( !-e rc_link($legacy_target_obsolete_root), + 'direct legacy migration preserves disabled systemd state' ); +ok( -l rc_kill_link($legacy_target_obsolete_root), + 'direct legacy migration records disabled systemd state with kill links' ); + +my $legacy_target_live_root = stage_root(); +make_path( File::Spec->catdir( $legacy_target_live_root, 'etc', 'init.d' ) ); +write_file( live_init($legacy_target_live_root), "live first-migration content\n", 0755 ); +set_systemd_state( $legacy_target_live_root, 'enabled' ); +set_init_target( $legacy_target_live_root, 'upstart' ); +is( run_state( $legacy_target_live_root, 'configure-legacy', 'upgrade' ), 0, + 'live conffile migrates directly to a legacy target' ); +is( read_file( live_init($legacy_target_live_root) ), + "live first-migration content\n", + 'direct legacy migration preserves live administrator content' ); +ok( -l rc_link($legacy_target_live_root), + 'native systemd enablement maps to SysV on first legacy migration' ); + +my $legacy_target_deleted_root = stage_root(); +make_path( state_dir($legacy_target_deleted_root) ); +write_file( + File::Spec->catfile( state_dir($legacy_target_deleted_root), 'pending-deleted' ), + "deleted\n" +); +set_systemd_state( $legacy_target_deleted_root, 'enabled' ); +set_init_target( $legacy_target_deleted_root, 'upstart' ); +is( run_state( $legacy_target_deleted_root, 'configure-legacy', 'upgrade' ), 0, + 'deleted conffile migrates directly to a legacy target' ); +ok( !-e live_init($legacy_target_deleted_root), + 'direct legacy migration preserves administrator deletion' ); +ok( !-e rc_link($legacy_target_deleted_root), + 'deleted conffile is never registered under SysV' ); +is( registration_link_count($legacy_target_deleted_root), 0, + 'deleted conffile gains neither start nor kill links' ); + +my $backup_root = stage_root(); +make_path( File::Spec->catdir( $backup_root, 'etc', 'init.d' ) ); +write_file( live_init($backup_root) . '.dpkg-bak', "recoverable customization\n", 0755 ); +set_systemd_state( $backup_root, 'disabled' ); +set_init_target( $backup_root, 'upstart' ); +is( run_state( $backup_root, 'configure-legacy', 'upgrade' ), 0, + 'dpkg backup migration succeeds' ); +is( read_file( live_init($backup_root) ), "recoverable customization\n", + 'a dpkg conffile backup is recovered through ucf' ); + +my $origin_reversal_root = stage_root(); +set_init_target( $origin_reversal_root, 'upstart' ); +is( run_state( $origin_reversal_root, 'configure-legacy', 'fresh' ), 0, + 'origin-reversal fixture starts in legacy mode' ); +write_file( live_init($origin_reversal_root), "origin customization\n", 0755 ); +is( run_state( $origin_reversal_root, 'prepare-systemd', 'upgrade' ), 0, + 'origin-reversal fixture begins a systemd transition' ); +is( state_value( $origin_reversal_root, 'origin' ), 'legacy', + 'legacy origin is recorded before systemd enablement runs' ); +write_file( File::Spec->catfile( $origin_reversal_root, 'mutate-ucf' ), "mutate\n" ); +write_file( File::Spec->catfile( $origin_reversal_root, 'fail-ucfr' ), "fail\n" ); +isnt( run_state( $origin_reversal_root, 'configure-legacy', 'upgrade' ), 0, + 'reversed pre-enable transition can fail during legacy restoration' ); +is( state_value( $origin_reversal_root, 'origin' ), 'legacy', + 'failed reversal preserves the original legacy provenance' ); +set_init_target( $origin_reversal_root, '../lib/systemd/systemd' ); +is( run_state( $origin_reversal_root, 'prepare-systemd', 'upgrade' ), 0, + 'failed reversal can return toward systemd' ); +is( state_value( $origin_reversal_root, 'origin' ), 'legacy', + 'second systemd attempt still requires legacy-origin enablement' ); +is( state_value( $origin_reversal_root, 'enabled' ), 'yes', + 'second systemd attempt retains intended enablement' ); + +my $registration_retry_root = stage_root(); +set_init_target( $registration_retry_root, 'upstart' ); +is( run_state( $registration_retry_root, 'configure-legacy', 'fresh' ), 0, + 'registration retry fixture starts enabled in legacy mode' ); +is( run_state( $registration_retry_root, 'prepare-systemd', 'upgrade' ), 0, + 'registration retry fixture begins a systemd transition' ); +run_update_rc( $registration_retry_root, '-f', 'xcatd', 'remove' ); +write_file( File::Spec->catfile( $registration_retry_root, 'fail-ucfr' ), "fail\n" ); +isnt( run_state( $registration_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'legacy reversal fails before registration is restored' ); +unlink( File::Spec->catfile( $registration_retry_root, 'fail-ucfr' ) ) + or die "Unable to clear registration retry failure: $!"; +is( run_state( $registration_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'legacy reversal retries successfully' ); +ok( -l rc_link($registration_retry_root), + 'transition retry restores missing SysV registration' ); + +my $retry_root = stage_root(); +set_init_target( $retry_root, 'upstart' ); +is( run_state( $retry_root, 'configure-legacy', 'fresh' ), 0, + 'retry fixture starts in legacy mode' ); +write_file( live_init($retry_root), "retry customization\n", 0755 ); +is( run_state( $retry_root, 'prepare-systemd', 'upgrade' ), 0, + 'retry fixture is durably stashed' ); +set_init_target( $retry_root, '../lib/systemd/systemd' ); +is( run_compat( $retry_root, 'configure', '--explicit-target' ), 0, + 'retry fixture enters systemd mode' ); +is( run_state( $retry_root, 'commit-systemd' ), 0, + 'retry fixture commits systemd state' ); +set_systemd_state( $retry_root, 'enabled' ); +set_init_target( $retry_root, 'upstart' ); +write_file( File::Spec->catfile( $retry_root, 'mutate-ucf' ), "mutate\n" ); +write_file( File::Spec->catfile( $retry_root, 'fail-ucfr' ), "fail\n" ); +isnt( run_state( $retry_root, 'configure-legacy', 'upgrade' ), 0, + 'a failure after ucf mutation reports failure' ); +ok( -e File::Spec->catfile( state_dir($retry_root), 'xcatd' ), + 'failed restoration retains the durable stash' ); +is( read_file( File::Spec->catfile( state_dir($retry_root), 'xcatd' ) ), + "retry customization\n", + 'partial restoration cannot overwrite the original stash' ); +is( state_value( $retry_root, 'mode' ), 'transition-legacy', + 'failed restoration remains explicitly transitional' ); +unlink( File::Spec->catfile( $retry_root, 'mutate-ucf' ) ) + or die "Unable to clear injected ucf mutation: $!"; +unlink( File::Spec->catfile( $retry_root, 'fail-ucfr' ) ) + or die "Unable to clear injected ucfr failure: $!"; +is( run_state( $retry_root, 'configure-legacy', 'upgrade' ), 0, + 'a repeated configure completes the interrupted transition' ); +is( read_file( live_init($retry_root) ), "retry customization\n", + 'retry preserves the customized content' ); + +my $reversal_retry_root = stage_root(); +set_init_target( $reversal_retry_root, 'upstart' ); +is( run_state( $reversal_retry_root, 'configure-legacy', 'fresh' ), 0, + 'reversal retry fixture starts in legacy mode' ); +write_file( live_init($reversal_retry_root), "reversal customization\n", 0755 ); +is( run_state( $reversal_retry_root, 'prepare-systemd', 'upgrade' ), 0, + 'reversal retry fixture is durably stashed' ); +set_init_target( $reversal_retry_root, '../lib/systemd/systemd' ); +is( run_compat( $reversal_retry_root, 'configure', '--explicit-target' ), 0, + 'reversal retry fixture enters systemd mode' ); +is( run_state( $reversal_retry_root, 'commit-systemd' ), 0, + 'reversal retry fixture commits systemd state' ); +set_systemd_state( $reversal_retry_root, 'enabled' ); +set_init_target( $reversal_retry_root, 'upstart' ); +write_file( File::Spec->catfile( $reversal_retry_root, 'mutate-ucf' ), "mutate\n" ); +write_file( File::Spec->catfile( $reversal_retry_root, 'fail-ucfr' ), "fail\n" ); +isnt( run_state( $reversal_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'reversal retry fixture fails after UCF mutation' ); +set_init_target( $reversal_retry_root, '../lib/systemd/systemd' ); +is( run_state( $reversal_retry_root, 'prepare-systemd', 'upgrade' ), 0, + 'a reversed transition prepares for systemd again' ); +is( read_file( + File::Spec->catfile( state_dir($reversal_retry_root), 'xcatd' ) + ), + "reversal customization\n", + 'reversing a failed restoration cannot overwrite the durable stash' ); +is( run_compat( $reversal_retry_root, 'configure', '--explicit-target' ), 0, + 'reversed transition removes the partial live script' ); +is( run_state( $reversal_retry_root, 'commit-systemd' ), 0, + 'reversed transition commits systemd state' ); +unlink( File::Spec->catfile( $reversal_retry_root, 'mutate-ucf' ) ) + or die "Unable to clear reversed UCF mutation: $!"; +unlink( File::Spec->catfile( $reversal_retry_root, 'fail-ucfr' ) ) + or die "Unable to clear reversed ucfr failure: $!"; +set_init_target( $reversal_retry_root, 'upstart' ); +is( run_state( $reversal_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'reversed transition can later return to legacy mode' ); +is( read_file( live_init($reversal_retry_root) ), "reversal customization\n", + 'reversed transition eventually restores exact administrator content' ); + +my $default_retry_root = stage_root(); +set_systemd_state( $default_retry_root, 'disabled' ); +is( run_state( $default_retry_root, 'prepare-systemd', 'fresh' ), 0, + 'package-default retry fixture prepares as a fresh systemd install' ); +is( run_state( $default_retry_root, 'commit-systemd' ), 0, + 'package-default retry fixture commits systemd state' ); +set_systemd_state( $default_retry_root, 'enabled' ); +set_init_target( $default_retry_root, 'upstart' ); +write_file( File::Spec->catfile( $default_retry_root, 'mutate-ucf' ), "mutate\n" ); +write_file( File::Spec->catfile( $default_retry_root, 'fail-ucfr' ), "fail\n" ); +isnt( run_state( $default_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'package-default restoration can fail after materialization' ); +is( state_value( $default_retry_root, 'content' ), 'package-default', + 'partial package materialization is not promoted to customization' ); +unlink( File::Spec->catfile( $default_retry_root, 'mutate-ucf' ) ) + or die "Unable to clear package-default UCF mutation: $!"; +unlink( File::Spec->catfile( $default_retry_root, 'fail-ucfr' ) ) + or die "Unable to clear package-default ucfr failure: $!"; +is( run_state( $default_retry_root, 'configure-legacy', 'upgrade' ), 0, + 'package-default restoration retries successfully' ); +is( read_file( live_init($default_retry_root) ), read_file($template), + 'package-default retry rematerializes the exact packaged template' ); +ok( !-e File::Spec->catfile( state_dir($default_retry_root), 'xcatd' ), + 'package-default retry does not create a durable customization stash' ); + +my $format1_root = stage_root(); +make_path( state_dir($format1_root) ); +write_file( File::Spec->catfile( state_dir($format1_root), 'state' ), + "format=1\nmode=systemd\ncontent=package-default\nenabled=no\norigin=unknown\n" ); +set_systemd_state( $format1_root, 'disabled' ); +set_init_target( $format1_root, '../lib/systemd/systemd' ); +is( run_state( $format1_root, 'prepare-systemd', 'upgrade' ), 0, + 'format-1 disabled state survives a same-systemd upgrade preparation' ); +is( state_value( $format1_root, 'enabled' ), 'unregistered', + 'same-systemd preparation retains recovered format-1 provenance' ); +is( run_state( $format1_root, 'commit-systemd' ), 0, + 'recovered format-1 same-systemd state commits' ); +set_init_target( $format1_root, 'upstart' ); +is( run_state( $format1_root, 'configure-legacy', 'upgrade' ), 0, + 'format-1 disabled state converges safely' ); +is( state_value( $format1_root, 'format' ), '2', + 'format-1 state is rewritten in registration-aware format 2' ); +is( state_value( $format1_root, 'enabled' ), 'unregistered', + 'format-1 no preserves its historical remove-all-links behavior' ); +is( registration_link_count($format1_root), 0, + 'format-1 no does not invent registered-disabled metadata' ); + +my $malformed_root = stage_root(); +make_path( state_dir($malformed_root), File::Spec->catdir( $malformed_root, 'etc', 'init.d' ) ); +write_file( live_init($malformed_root), "must survive\n", 0755 ); +write_file( File::Spec->catfile( state_dir($malformed_root), 'state' ), + "format=9\nmode=legacy\ncontent=active\nenabled=yes\n" ); +isnt( run_state( $malformed_root, 'prepare-systemd', 'upgrade' ), 0, + 'malformed persistent state fails closed' ); +is( read_file( live_init($malformed_root) ), "must survive\n", + 'malformed state cannot delete the live init script' ); + +write_file( File::Spec->catfile( state_dir($malformed_root), 'state' ), + "format=1\nmode=legacy\ncontent=active\nenabled=yes\n" ); +isnt( run_state( $malformed_root, 'prepare-systemd', 'upgrade' ), 0, + 'incomplete persistent state fails closed' ); +is( read_file( live_init($malformed_root) ), "must survive\n", + 'incomplete state cannot delete the live init script' ); + +done_testing(); diff --git a/xCAT-test/unit/xcatd_debian_service_packaging.t b/xCAT-test/unit/xcatd_debian_service_packaging.t index e2e209918..ced13a8a2 100644 --- a/xCAT-test/unit/xcatd_debian_service_packaging.t +++ b/xCAT-test/unit/xcatd_debian_service_packaging.t @@ -2,7 +2,9 @@ use strict; use warnings; +use File::Path qw(make_path); use File::Spec; +use File::Temp qw(tempdir); use FindBin; use Test::More; @@ -16,6 +18,69 @@ sub read_file { 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' ) ); @@ -45,31 +110,230 @@ my $deb_postinst = read_file( File::Spec->catfile( $repo_root, 'xCAT-server', 'debian', 'postinst' ) ); like( $deb_postinst, - qr{if "\$xcatd_init_compat" uses-systemd; then}, + 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{\[ -x /etc/init\.d/xcatd \].*?update-rc\.d xcatd defaults}s, - 'Debian postinst registers SysV only after materializing an executable script' ); + 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; then.*?update-rc\.d -f xcatd remove}s, + 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(); diff --git a/xCAT-test/unit/xcatd_rpm_service_packaging.t b/xCAT-test/unit/xcatd_rpm_service_packaging.t index 2af06e242..68b6a9b20 100644 --- a/xCAT-test/unit/xcatd_rpm_service_packaging.t +++ b/xCAT-test/unit/xcatd_rpm_service_packaging.t @@ -30,9 +30,29 @@ sub stage_root { my $scripts = File::Spec->catdir( $root, 'opt', 'xcat', 'share', 'xcat', 'scripts' ); - make_path($scripts); + my $fake_bin = File::Spec->catdir( $root, 'test-bin' ); + make_path( $scripts, $fake_bin ); copy( $template, File::Spec->catfile( $scripts, 'xcatd' ) ) or die "Unable to stage legacy init template: $!"; + my $systemctl = File::Spec->catfile( $fake_bin, 'systemctl' ); + open( my $systemctl_fh, '>', $systemctl ) + or die "Unable to stage fake systemctl: $!"; + print {$systemctl_fh} <<'SH'; +#!/bin/sh +set -eu +root=${XCAT_COMPAT_ROOT:?} +case "${1:-}" in + --root=*) shift ;; +esac +[ "${1:-}" = is-enabled ] || exit 1 +if [ -r "$root/systemctl-state" ]; then + sed -n '1p' "$root/systemctl-state" +else + echo unknown +fi +SH + close($systemctl_fh); + chmod 0755, $systemctl; return $root; } @@ -49,10 +69,20 @@ sub stage_systemd_binary { return $binary; } +sub set_systemd_state { + my ( $root, $state ) = @_; + my $path = File::Spec->catfile( $root, 'systemctl-state' ); + open( my $fh, '>', $path ) + or die "Unable to stage systemd state: $!"; + print {$fh} "$state\n"; + close($fh); +} + sub run_helper { my ( $root, @arguments ) = @_; local $ENV{XCAT_COMPAT_ROOT} = $root; local $ENV{XCATROOT} = '/opt/xcat'; + local $ENV{PATH} = File::Spec->catdir( $root, 'test-bin' ) . ':/usr/bin:/bin'; system( '/bin/sh', $helper, @arguments ); return $? >> 8; } @@ -61,6 +91,7 @@ sub helper_output { my ( $root, @arguments ) = @_; local $ENV{XCAT_COMPAT_ROOT} = $root; local $ENV{XCATROOT} = '/opt/xcat'; + local $ENV{PATH} = File::Spec->catdir( $root, 'test-bin' ) . ':/usr/bin:/bin'; open( my $pipe, '-|', '/bin/sh', $helper, @arguments ) or die "Unable to run compatibility helper: $!"; my $output = do { local $/; <$pipe> }; @@ -255,6 +286,34 @@ is( helper_output( $legacy_state_root, 'legacy-state' ), 'enabled', is( helper_output( $legacy_state_root, 'legacy-transition-state' ), 'enabled', 'enabled SysV state takes precedence during a legacy transition' ); +my $rpm_layout_root = stage_root(); +stage_symlink( + $rpm_layout_root, '/etc/init.d/xcatd', + qw(etc rc.d rc3.d S85xcatd) +); +is( helper_output( $rpm_layout_root, 'legacy-state' ), 'enabled', + 'default legacy state continues to recognize RPM runlevel layouts' ); +is( helper_output( $rpm_layout_root, 'debian-legacy-state' ), + 'unregistered', 'Debian legacy state ignores RPM-only runlevel links' ); +is( helper_output( $rpm_layout_root, 'legacy-state', '--debian-layout' ), + 'enabled', 'legacy state preserves ignored historical extra arguments' ); +is( run_helper( $rpm_layout_root, 'debian-legacy-state', 'extra' ), 2, + 'Debian legacy state rejects extra arguments' ); + +my $debian_layout_root = stage_root(); +stage_symlink( + $debian_layout_root, '/etc/init.d/xcatd', + qw(etc rc3.d K60xcatd) +); +is( helper_output( $debian_layout_root, 'debian-legacy-state' ), + 'disabled', 'Debian legacy state recognizes native kill links' ); +stage_symlink( + $debian_layout_root, '/etc/init.d/xcatd', + qw(etc rc3.d S85xcatd) +); +is( helper_output( $debian_layout_root, 'debian-legacy-state' ), + 'enabled', 'Debian legacy state gives native start links precedence' ); + my $unregistered_legacy_root = stage_root(); make_path( File::Spec->catdir( $unregistered_legacy_root, 'etc', 'init.d' ) ); copy( $template, legacy_init($unregistered_legacy_root) ) @@ -275,6 +334,11 @@ is( helper_output( $deleted_managed_root, 'legacy-transition-state' ), my $systemd_state_root = stage_root(); is( helper_output( $systemd_state_root, 'systemd-state' ), 'disabled', 'systemd state reports disabled when enablement links are absent' ); +is( helper_output( $systemd_state_root, 'systemd-state', '--allow-unknown' ), + 'unknown', 'precise systemd state preserves indeterminate evidence' ); +set_systemd_state( $systemd_state_root, 'disabled' ); +is( helper_output( $systemd_state_root, 'systemd-state', '--allow-unknown' ), + 'disabled', 'precise systemd state recognizes an explicit disablement' ); stage_symlink( $systemd_state_root, '/usr/lib/systemd/system/xcatd.service', qw(etc systemd system multi-user.target.wants xcatd.service) @@ -283,6 +347,63 @@ is( helper_output( $systemd_state_root, 'systemd-state' ), 'enabled', 'systemd state recognizes persistent wants links' ); is( helper_output( $systemd_state_root, 'legacy-transition-state' ), 'enabled', 'enabled systemd state transfers to an enabled SysV service' ); +is( helper_output( $systemd_state_root, 'systemd-state', '--allow-unknown' ), + 'enabled', 'enablement links override stale disabled manager output' ); + +my $manager_masked_link_root = stage_root(); +set_systemd_state( $manager_masked_link_root, 'masked' ); +stage_symlink( + $manager_masked_link_root, '/usr/lib/systemd/system/xcatd.service', + qw(etc systemd system multi-user.target.wants xcatd.service) +); +is( helper_output( $manager_masked_link_root, 'systemd-state' ), 'enabled', + 'compatibility state keeps historical link precedence for target roots' ); +is( helper_output( + $manager_masked_link_root, 'systemd-state', '--allow-unknown' + ), + 'masked', 'precise systemd state gives a manager mask precedence over links' ); + +for my $manager_state_case ( + [ 'enabled', 'enabled' ], + [ 'enabled-runtime', 'enabled' ], + [ 'masked', 'masked' ], + [ 'masked-runtime', 'masked' ], + [ 'disabled', 'disabled' ], + [ 'linked', 'disabled' ], + [ 'linked-runtime', 'disabled' ], + [ 'alias', 'disabled' ], + [ 'static', 'disabled' ], + [ 'indirect', 'disabled' ], + [ 'generated', 'disabled' ], + [ 'transient', 'disabled' ], + [ 'enabled-garbage', 'unknown' ], + [ 'masked-garbage', 'unknown' ], +) { + my ( $manager_state, $expected_state ) = @{$manager_state_case}; + my $manager_state_root = stage_root(); + set_systemd_state( $manager_state_root, $manager_state ); + is( helper_output( + $manager_state_root, 'systemd-state', '--allow-unknown' + ), + $expected_state, "precise systemd state parses $manager_state exactly" ); +} + +for my $precise_link_case ( + [ qw(etc wants) ], + [ qw(etc requires) ], + [ qw(run wants) ], + [ qw(run requires) ], +) { + my ( $scope, $relation ) = @{$precise_link_case}; + my $precise_link_root = stage_root(); + stage_symlink( + $precise_link_root, '/usr/lib/systemd/system/xcatd.service', + $scope, 'systemd', 'system', "multi-user.target.$relation", + 'xcatd.service' + ); + is( helper_output( $precise_link_root, 'systemd-state', '--allow-unknown' ), + 'enabled', "precise systemd state recognizes $scope $relation links" ); +} my $systemd_requires_root = stage_root(); stage_symlink( @@ -311,6 +432,24 @@ is( helper_output( $systemd_masked_root, 'systemd-state' ), 'masked', 'systemd state preserves an administrator mask' ); is( helper_output( $systemd_masked_root, 'legacy-transition-state' ), 'masked', 'a systemd mask prevents SysV registration during a legacy transition' ); +is( helper_output( $systemd_masked_root, 'systemd-state', '--allow-unknown' ), + 'masked', 'precise systemd state recognizes persistent masks' ); + +my $systemd_runtime_masked_root = stage_root(); +stage_symlink( + $systemd_runtime_masked_root, '/dev/null', + qw(run systemd system xcatd.service) +); +is( helper_output( $systemd_runtime_masked_root, + 'systemd-state', '--allow-unknown' ), + 'masked', 'precise systemd state recognizes runtime masks' ); +my $systemd_argv_root = stage_root(); +is( helper_output( $systemd_argv_root, 'systemd-state', '--invalid' ), + 'disabled', 'systemd state preserves ignored historical extra arguments' ); +is( helper_output( + $systemd_argv_root, 'systemd-state', '--allow-unknown', 'extra' + ), + 'disabled', 'only the exact precision option activates precise state' ); my $cleanup_root = stage_root(); my @cleanup_links = (