From 1b81af0d1da2c83b07e1d4e22dbf36f802648b8c Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:39:49 -0300 Subject: [PATCH 01/16] test(xcat-core): capture NTP setup failing on a stock Ubuntu management node An xCAT management node must serve time to its compute nodes (they point at ntpservers=). Ubuntu ships only systemd-timesyncd, an SNTP client that disciplines the local clock but cannot serve time, and makentp supports chronyd and ntpd only. On a stock Ubuntu MN makentp therefore fails outright with "Please make sure ntpd is installed", reddening reg_linux_diskfull_installation_flat. Two further defects compound it. setupntp hard-requires hwclock through check_exec_or_exit, but Ubuntu 24.04 moved hwclock into util-linux-extra, which is absent from minimal images -- so the whole NTP setup, including the clock step that does not need hwclock at all, aborts. And systemd-timesyncd is part of systemd rather than a time-daemon package, so it coexists at the package level and keeps disciplining the clock against whichever daemon xCAT just configured. Assert a shared, unit-tested daemon selector in the spirit of xCAT::DHCP::Backend, the package dependencies that guarantee a server-capable daemon and hwclock, and the setupntp changes. All fail today. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/makentp_ntp_deps.t | 58 +++++++++++++++++++++ xCAT-test/unit/ntp_backend_selection.t | 72 ++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 xCAT-test/unit/makentp_ntp_deps.t create mode 100644 xCAT-test/unit/ntp_backend_selection.t diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t new file mode 100644 index 000000000..68180ee34 --- /dev/null +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -0,0 +1,58 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +# Regression: makentp/setupntp configure a server-capable NTP daemon (chronyd/ntpd) on the MN. +# On a minimal Ubuntu 24.04 MN chrony was absent (Ubuntu ships only the client-only +# systemd-timesyncd), hwclock moved to util-linux-extra (absent) so setupntp aborted its whole NTP +# setup, and timesyncd was left fighting the NTP daemon -- reddening reg_linux_diskfull_installation_flat. + +use File::Spec; +use FindBin; +my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +sub slurp { + my ($rel) = @_; + my $path = File::Spec->catfile( $repo_root, split m{/}, $rel ); + local $/; + open my $fh, '<', $path or return undef; + <$fh>; +} + +my $setupntp = slurp('xCAT/postscripts/setupntp'); +SKIP: { + skip 'setupntp not found', 4 unless defined $setupntp; + unlike($setupntp, qr/check_exec_or_exit[^\n]*\bhwclock\b/, + 'setupntp does NOT hard-require hwclock in check_exec_or_exit'); + like($setupntp, qr/command -v hwclock/, + 'setupntp guards its hwclock use so a missing hwclock is non-fatal'); + like($setupntp, qr/systemctl\s+(?:stop|disable)\s+systemd-timesyncd/, + 'setupntp stops/disables systemd-timesyncd so it does not fight the NTP daemon'); + like($setupntp, qr/chronyd\s+-f\s+\S*\s+-q/, + 'setupntp still steps the system clock via a one-shot chronyd -q'); +} + +my $ctrl = slurp('xCAT/debian/control'); +SKIP: { + skip 'debian/control not found', 2 unless defined $ctrl; + like($ctrl, qr/^Depends:.*\bchrony \| ntp\b/m, + 'xcat debian package Depends on chrony | ntp (server-capable NTP daemon)'); + like($ctrl, qr/^Recommends:.*\butil-linux-extra\b/m, + 'xcat debian package Recommends util-linux-extra (provides hwclock on noble+)'); +} + +my $spec = slurp('xCAT/xCAT.spec'); +SKIP: { + skip 'xCAT.spec not found', 1 unless defined $spec; + like($spec, qr/^Requires:\s*\(chrony or ntp\)/m, + 'xCAT rpm Requires (chrony or ntp) for makentp'); +} + +my $makentp = slurp('xCAT-server/lib/xcat/plugins/makentp.pm'); +SKIP: { + skip 'makentp.pm not found', 1 unless defined $makentp; + like($makentp, qr/xCAT::NTP::Backend->choose/, + 'makentp selects the NTP daemon through the xCAT::NTP::Backend selector'); +} + +done_testing(); diff --git a/xCAT-test/unit/ntp_backend_selection.t b/xCAT-test/unit/ntp_backend_selection.t new file mode 100644 index 000000000..3f86cf0a2 --- /dev/null +++ b/xCAT-test/unit/ntp_backend_selection.t @@ -0,0 +1,72 @@ +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use Test::More; + +use xCAT::NTP::Backend; + +# --- normalize: aliases, trimming, case, validation -------------------------------------------- +is( xCAT::NTP::Backend->normalize(undef), 'auto', 'undefined backend defaults to auto' ); +is( xCAT::NTP::Backend->normalize(''), 'auto', 'empty backend defaults to auto' ); +is( xCAT::NTP::Backend->normalize(' Chrony '), 'chrony', 'values are trimmed and lowercased' ); +is( xCAT::NTP::Backend->normalize('chronyd'), 'chrony', 'chronyd aliases to chrony' ); +is( xCAT::NTP::Backend->normalize('ntp'), 'ntpd', 'ntp aliases to ntpd' ); +is( xCAT::NTP::Backend->normalize('ntpsec'), 'ntpd', 'ntpsec aliases to ntpd' ); +is( xCAT::NTP::Backend->normalize('ntpd'), 'ntpd', 'ntpd is valid' ); +is( xCAT::NTP::Backend->normalize('bogus'), undef, 'invalid backend is rejected' ); + +# --- default_backend: per-distro table --------------------------------------------------------- +is( xCAT::NTP::Backend->default_backend( os_name => 'rhel', version => 6 ), 'ntpd', 'EL6 defaults to ntpd' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'rhel', version => 7 ), 'chrony', 'EL7 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'rhels', version => 8 ), 'chrony', 'EL8 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'rocky', version => 9 ), 'chrony', 'EL9 clones default to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'alma', version => 10 ), 'chrony', 'EL10 clones default to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'sles', version => 12 ), 'ntpd', 'SLES12 defaults to ntpd' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'sles', version => 15 ), 'chrony', 'SLES15 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'sles', version => 16 ), 'chrony', 'SLES16 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'leap', version => '15.6' ), 'chrony', 'Leap 15.x defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'ubuntu', version => '18.04' ), 'chrony', 'Ubuntu 18.04 defaults to chrony (timesyncd cannot serve)' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'ubuntu', version => '20.04' ), 'chrony', 'Ubuntu 20.04 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'ubuntu', version => '22.04' ), 'chrony', 'Ubuntu 22.04 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'ubuntu', version => '24.04' ), 'chrony', 'Ubuntu 24.04 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'ubuntu', version => '26.04' ), 'chrony', 'Ubuntu 26.04 defaults to chrony' ); +is( xCAT::NTP::Backend->default_backend( os_name => 'debian', version => 12 ), 'chrony', 'Debian defaults to chrony' ); + +# --- choose: explicit override, auto, availability downgrade, install flag ---------------------- +my $r; + +$r = xCAT::NTP::Backend->choose( requested => 'ntpd', os_name => 'ubuntu', version => '24.04' ); +is( $r->{name}, 'ntpd', 'explicit ntpd override is honored on Ubuntu' ); + +$r = xCAT::NTP::Backend->choose( requested => 'auto', os_name => 'ubuntu', version => '24.04' ); +is( $r->{name}, 'chrony', 'auto resolves to the Ubuntu default (chrony)' ); + +$r = xCAT::NTP::Backend->choose( requested => 'bogus', os_name => 'ubuntu', version => '24.04' ); +like( $r->{error}, qr/Invalid site\.ntpbackend/, 'invalid override returns an error' ); + +# chrony chosen but absent, ntpd present -> downgrade to ntpd (do not install a 2nd daemon) +$r = xCAT::NTP::Backend->choose( + requested => 'auto', os_name => 'ubuntu', version => '24.04', + check_available => 1, available => { chrony => 0, ntpd => 1 } ); +is( $r->{name}, 'ntpd', 'chrony absent + ntpd present downgrades to ntpd' ); +is( $r->{downgraded}, 'chrony', 'downgrade records the preferred backend' ); +is( $r->{install}, 0, 'no install when a supported daemon is already present' ); + +# chrony chosen and present -> keep it, no install +$r = xCAT::NTP::Backend->choose( + requested => 'auto', os_name => 'ubuntu', version => '24.04', + check_available => 1, available => { chrony => 1, ntpd => 0 } ); +is( $r->{name}, 'chrony', 'chrony present keeps chrony' ); +is( $r->{install}, 0, 'chrony present needs no install' ); + +# neither present -> keep the preferred choice and flag install (stock Ubuntu MN) +$r = xCAT::NTP::Backend->choose( + requested => 'auto', os_name => 'ubuntu', version => '24.04', + check_available => 1, available => { chrony => 0, ntpd => 0 } ); +is( $r->{name}, 'chrony', 'neither present keeps the preferred chrony' ); +is( $r->{install}, 1, 'neither present flags install of the preferred daemon' ); + +done_testing(); From 067eda810ec05545bdb2241e6027e7fd0fd7ea95 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:40:13 -0300 Subject: [PATCH 02/16] fix(xcat-core): configure a server-capable NTP daemon on Ubuntu MNs Add xCAT::NTP::Backend, a selector for the NTP daemon xCAT configures, in the same spirit as xCAT::DHCP::Backend. It honours site.ntpbackend, defaults per distro family (EL7+/SLES15+ chrony, older ntpd, Ubuntu/Debian chrony), and downgrades to whichever of chrony/ntpd is actually installed rather than installing a second daemon. makentp selects through it instead of probing /usr/sbin/chronyd directly, so the choice is one unit-tested code path. Guarantee the daemon at install time: the xcat metapackage now Depends on "chrony | ntp" and the xCAT rpm Requires "(chrony or ntp)". Both sit beside the existing service dependencies the metapackage already declares -- isc-dhcp-server|kea, apache2, nfs-kernel-server -- because an MN that cannot serve time cannot serve its compute nodes, and Ubuntu's default systemd-timesyncd is a client only. Stop requiring hwclock in setupntp. It only persists the already-stepped system clock to the RTC, and Ubuntu 24.04 moved it to util-linux-extra, absent on minimal images -- so a fatal check_exec_or_exit aborted the entire NTP setup, including the clock step that does not use it. Use it when present, log and continue when not, and pull util-linux-extra through Recommends and the diskless pkglist so it usually is. Disable systemd-timesyncd there too. It ships as part of systemd rather than a time-daemon package, so nothing displaces it, and it keeps disciplining the clock against the daemon being configured. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- perl-xCAT/xCAT/NTP/Backend.pm | 138 ++++++++++++++++++ xCAT-server/lib/xcat/plugins/makentp.pm | 14 +- .../ubuntu/compute.ubuntu24.04.x86_64.pkglist | 1 + xCAT/debian/control | 4 +- xCAT/postscripts/setupntp | 24 ++- xCAT/xCAT.spec | 3 + 6 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 perl-xCAT/xCAT/NTP/Backend.pm diff --git a/perl-xCAT/xCAT/NTP/Backend.pm b/perl-xCAT/xCAT/NTP/Backend.pm new file mode 100644 index 000000000..71b5f868a --- /dev/null +++ b/perl-xCAT/xCAT/NTP/Backend.pm @@ -0,0 +1,138 @@ +package xCAT::NTP::Backend; + +# Selector for the NTP daemon xCAT configures on a node (chrony vs. ntpd), in the same spirit as +# xCAT::DHCP::Backend (ISC vs. Kea). makentp/setupntp support chronyd and ntpd only -- there is no +# systemd-timesyncd path (timesyncd is an SNTP client and cannot serve time to compute nodes), so an +# xCAT MN always needs chrony or ntpd. This module centralizes and unit-tests the choice. + +use strict; +use warnings; + +my %valid_backend = map { $_ => 1 } qw(auto chrony ntpd); + +# Accept common aliases so site.ntpbackend / callers can say chronyd or ntp. +my %alias = ( + chronyd => 'chrony', + ntp => 'ntpd', + ntpsec => 'ntpd', +); + +sub normalize { + my ( $class, $backend ) = @_; + + $backend = 'auto' unless defined($backend) && $backend ne ''; + $backend =~ s/^\s+|\s+$//g; + $backend = lc($backend); + $backend = $alias{$backend} if exists $alias{$backend}; + + return $backend if $valid_backend{$backend}; + return; +} + +# choose: resolve the effective backend for this node. +# requested -- override (default: site.ntpbackend, else 'auto') +# os_name/version -- OS identity (default: detected) +# available -- optional { chrony => 0/1, ntpd => 0/1 } to bypass command detection (tests) +# check_available -- when true, downgrade chrony->ntpd (or ntpd->chrony) if the chosen one is +# absent but the other is present, and flag install=1 when neither is present. +sub choose { + my ( $class, %args ) = @_; + + my $requested = exists $args{requested} ? $args{requested} : $class->_site_backend(); + my $normalized = $class->normalize($requested); + unless ($normalized) { + return { error => "Invalid site.ntpbackend value '$requested'. Valid values are auto, chrony, and ntpd." }; + } + + my $selected = $normalized eq 'auto' ? $class->default_backend(%args) : $normalized; + my $result = { requested => $normalized, name => $selected, install => 0 }; + + return $result unless $args{check_available}; + + my $other = $selected eq 'chrony' ? 'ntpd' : 'chrony'; + if ( $class->available( $selected, %args ) ) { + return $result; + } elsif ( $class->available( $other, %args ) ) { + # respect what is actually installed rather than installing a second daemon + $result->{name} = $other; + $result->{downgraded} = $selected; + return $result; + } + + # neither present: keep the preferred choice and tell the caller to install it + $result->{install} = 1; + return $result; +} + +# default_backend: table-driven per distro family. +# EL/RHEL & clones: >= 7 -> chrony, 6 -> ntpd +# SLES/SUSE: >= 15 -> chrony, 12 -> ntpd +# Ubuntu/Debian: chrony (timesyncd is the OOB client but cannot serve; chrony from 18.04+) +sub default_backend { + my ( $class, %args ) = @_; + + my $os_name = exists $args{os_name} ? $args{os_name} : $class->_osver('os'); + my $version = exists $args{version} ? $args{version} : ( split /,/, $class->_osver('all'), 2 )[1]; + my ($major) = ( defined($version) ? $version : '' ) =~ /^(\d+)/; + + if ( defined($os_name) && $os_name =~ /^(?:rhel|rhels|rocky|alma|centos|ol|fedora)$/i ) { + return 'ntpd' if defined($major) && $major <= 6; + return 'chrony'; + } + if ( defined($os_name) && $os_name =~ /^(?:sles|sled|suse|opensuse|leap)$/i ) { + return 'ntpd' if defined($major) && $major <= 12; + return 'chrony'; + } + if ( defined($os_name) && $os_name =~ /^(?:ubuntu|debian)$/i ) { + return 'chrony'; + } + + return 'chrony'; +} + +sub available { + my ( $class, $backend, %args ) = @_; + + if ( exists $args{available} && ref( $args{available} ) eq 'HASH' && exists $args{available}{$backend} ) { + return $args{available}{$backend} ? 1 : 0; + } + + return _command_exists('chronyd') if $backend eq 'chrony'; + return _command_exists('ntpd') if $backend eq 'ntpd'; + return 0; +} + +sub _site_backend { + my $backend = eval { + require xCAT::TableUtils; + return xCAT::TableUtils->get_site_attribute( 'ntpbackend', 'auto' ); + }; + + return $backend || 'auto'; +} + +sub _osver { + my ( $class, $type ) = @_; + + my $osver = eval { + require xCAT::Utils; + return defined($type) ? xCAT::Utils->osver($type) : xCAT::Utils->osver(); + }; + + return $osver || 'unknown'; +} + +sub _command_exists { + my ($command) = @_; + + foreach my $dir ( split /:/, $ENV{PATH} || '' ) { + next unless $dir; + return 1 if -x "$dir/$command"; + } + foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) { + return 1 if -x $path; + } + return 0; +} + +1; diff --git a/xCAT-server/lib/xcat/plugins/makentp.pm b/xCAT-server/lib/xcat/plugins/makentp.pm index 41212ebb6..620a45e85 100644 --- a/xCAT-server/lib/xcat/plugins/makentp.pm +++ b/xCAT-server/lib/xcat/plugins/makentp.pm @@ -252,9 +252,19 @@ sub process_request { $ntp_servers = $retdata->{'master'}; } + # Pick the NTP daemon (chrony vs ntpd) via the shared, unit-tested selector -- the same spirit + # as xCAT::DHCP::Backend (ISC vs Kea). It honors site.ntpbackend, defaults per distro, and + # downgrades chrony->ntpd (or vice versa) to whichever is actually installed. + require xCAT::NTP::Backend; + my $ntp_backend = xCAT::NTP::Backend->choose(check_available => 1); + if ($ntp_backend->{error}) { + send_msg(\%request, 1, $ntp_backend->{error}); + return 1; + } + my $have_systemctl = (-x "/usr/bin/systemctl" || -x "/bin/systemctl"); + # Handle chronyd here, - if (-x "/usr/sbin/chronyd" && - (-x "/usr/bin/systemctl" || -x "/bin/systemctl")) { + if ($ntp_backend->{name} eq 'chrony' && $have_systemctl) { send_msg(\%request, 0, "Will configure chronyd instead."); my $cmd = "/install/postscripts/setupntp " . diff --git a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist index baa088bac..4c6b2b3c4 100644 --- a/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist +++ b/xCAT-server/share/xcat/netboot/ubuntu/compute.ubuntu24.04.x86_64.pkglist @@ -16,6 +16,7 @@ gzip xz-utils cpio chrony +util-linux-extra iproute2 dracut dracut-network diff --git a/xCAT/debian/control b/xCAT/debian/control index 0481919e6..74ff70c64 100644 --- a/xCAT/debian/control +++ b/xCAT/debian/control @@ -9,8 +9,8 @@ Homepage: https://xcat.org/ Package: xcat Architecture: amd64 ppc64el -Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) -Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64 +Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, bind9, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, chrony | ntp, xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000) +Recommends: net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, util-linux-extra, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000), xcat-genesis-openembedded-x86-64, xcat-genesis-openembedded-ppc64le, xcat-genesis-openembedded-riscv64 Suggests: yaboot-xcat Description: Metapackage for a common, default xCAT setup xCAT is Extreme Cluster/Cloud Administration Toolkit. xCAT offers complete diff --git a/xCAT/postscripts/setupntp b/xCAT/postscripts/setupntp index eaae4a8b0..7de945790 100755 --- a/xCAT/postscripts/setupntp +++ b/xCAT/postscripts/setupntp @@ -146,7 +146,10 @@ unset MASTER unset NTPSERVERS check_exec_or_exit cp cat logger grep -check_exec_or_exit systemctl timedatectl hwclock +# hwclock is NOT required: it only persists the (already chronyd-stepped) system clock to the RTC +# below, and it is optional -- on Ubuntu 24.04+ it moved to util-linux-extra, which is absent on +# minimal cloud images. Requiring it here aborted the whole NTP setup (and the clock step) on noble. +check_exec_or_exit systemctl timedatectl systemctl stop ntp.service 2>/dev/null systemctl disable ntp.service 2>/dev/null @@ -156,6 +159,13 @@ systemctl disable ntpd.service 2>/dev/null systemctl disable ntp-wait.service 2>/dev/null systemctl disable ntpdate.service 2>/dev/null +# systemd-timesyncd is an SNTP client that also disciplines the system clock. It is part of systemd +# (not a "time-daemon" package), so it coexists with chrony/ntpd at the package level but must be +# stopped or it fights the NTP daemon we are configuring. This is the default time-sync stack on +# Ubuntu, where it must yield to chrony (timesyncd cannot serve time to compute nodes). +systemctl stop systemd-timesyncd.service 2>/dev/null +systemctl disable systemd-timesyncd.service 2>/dev/null + # On Ubuntu 18.04 systemctl stop chrony.service 2>/dev/null # On RHEL 7, 8 @@ -177,10 +187,14 @@ chronyd -f /dev/null -q "$( fi )" -rm -f /etc/adjtime -# Set the hardware clock from the system clock -hwclock --systohc --utc -warn_if_bad "$?" "Failed to set the hardware clock" +# Set the hardware clock from the system clock, when hwclock is available (optional; see above). +if command -v hwclock >/dev/null 2>&1; then + rm -f /etc/adjtime + hwclock --systohc --utc + warn_if_bad "$?" "Failed to set the hardware clock" +else + logger -t $log_label -p local4.info "setupntp: hwclock not present; skipping RTC persist (system clock already set via chronyd)" +fi ## On RHEL 8 #CHRONY_USER="chrony" diff --git a/xCAT/xCAT.spec b/xCAT/xCAT.spec index febac8836..8772828b9 100644 --- a/xCAT/xCAT.spec +++ b/xCAT/xCAT.spec @@ -73,6 +73,9 @@ Requires: httpd nfs-utils nmap bind perl(CGI) # on RHEL7, need to specify it explicitly Requires: net-tools Requires: /usr/bin/killall +# makentp/setupntp configure the MN as an NTP server for its compute nodes and support chronyd/ntpd +# only. chrony is the default on EL7+/SLES15+ (and the only option on EL8+); ntp covers the rest. +Requires: (chrony or ntp) # DHCP backend resolved at INSTALL time (not build time) via an RPM rich # dependency, so a single flat xcat-core build is correct on every EL: el10+ # dropped ISC dhcp from its distro and uses Kea; el8/el9 use ISC dhcpd. SLES From 377a483c01ce3bc546c6dcee45902207d145be2e Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:34:18 -0300 Subject: [PATCH 03/16] test(xcat-core): run setupntp instead of matching its source The test added with this fix matched regexes against the text of setupntp and makentp.pm. A source match cannot tell whether the code it found ever runs, and it describes the fix rather than the behaviour: unlike(qr/check_exec_or_exit[^\n]*hwclock/) says "this line does not mention hwclock", where what matters is that a management node without hwclock still gets its clock set. Run the script instead. setupntp cannot simply be executed: it forces its own PATH, so its commands cannot be stubbed from outside, and it exits unless UID is 0, so it cannot run as an ordinary user. The test takes the script's own helper functions and the section that configures the daemon and drives them with shell functions, which bash resolves ahead of PATH and which both `type` and `command -v` report as present -- the two probes the script uses. A node without hwclock is simulated by hiding it from both, rather than by asserting on the shape of the check. Every assertion now fails when the behaviour it describes is removed: hwclock required by check_exec_or_exit again 5 assertions fail the hwclock guard removed 2 assertions fail systemd-timesyncd left running 2 assertions fail systemd-timesyncd left enabled 2 assertions fail The debian/control and xCAT.spec checks are kept as they were. Those are manifest contents, not behaviour -- there is nothing to execute, and the assertion is on a package name that survives reformatting. The match on makentp.pm for xCAT::NTP::Backend->choose is dropped. It asserted that a call site exists; ntp_backend_selection.t already drives the selector itself across 33 assertions, which is the decision that matters. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/makentp_ntp_deps.t | 141 +++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 34 deletions(-) diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t index 68180ee34..3716d852e 100644 --- a/xCAT-test/unit/makentp_ntp_deps.t +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -1,58 +1,131 @@ #!/usr/bin/env perl use strict; use warnings; + +use FindBin; +use File::Temp qw(tempdir); use Test::More; -# Regression: makentp/setupntp configure a server-capable NTP daemon (chronyd/ntpd) on the MN. -# On a minimal Ubuntu 24.04 MN chrony was absent (Ubuntu ships only the client-only -# systemd-timesyncd), hwclock moved to util-linux-extra (absent) so setupntp aborted its whole NTP -# setup, and timesyncd was left fighting the NTP daemon -- reddening reg_linux_diskfull_installation_flat. +# setupntp configures a server-capable NTP daemon on the management node. On a stock Ubuntu MN +# three things went wrong: hwclock had moved to util-linux-extra and was absent, so the fatal +# executable check aborted the ENTIRE NTP setup including the clock step that does not use it; +# systemd-timesyncd was left running and kept disciplining the clock against the daemon being +# configured; and the xcat package did not pull in a daemon that can serve time at all. +# +# The first three are behaviours of the script, so run it. The last is a property of the +# packaging manifests, so read those. -use File::Spec; -use FindBin; -my $repo_root = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); -sub slurp { - my ($rel) = @_; - my $path = File::Spec->catfile( $repo_root, split m{/}, $rel ); - local $/; - open my $fh, '<', $path or return undef; - <$fh>; +my $repo = "$FindBin::Bin/../.."; +my $setupntp_path = "$repo/xCAT/postscripts/setupntp"; +plan skip_all => 'setupntp not found' unless -r $setupntp_path; +plan skip_all => 'setupntp targets Linux management nodes' unless $^O eq 'linux'; + +open(my $fh, '<', $setupntp_path) or die "open $setupntp_path: $!"; +my @lines = <$fh>; +close $fh; +my $source = join '', @lines; + +# setupntp forces its own PATH and refuses to run unless UID is 0, so its commands cannot be +# stubbed from outside and it cannot run as an ordinary user. Take the script's own helper +# functions and the section that configures the daemon, and drive them with shell functions -- +# which bash resolves ahead of PATH, and which `type` and `command -v` both report as present. +my ($helpers) = $source =~ /\A(.*?)^\[ "\$\{UID\}" -eq "0" \]/ms; +my ($body) = $source =~ /^(check_exec_or_exit cp cat logger grep\n.*?)^CHRONY_CONF=/ms; +BAIL_OUT('could not take the helper functions from setupntp') unless $helpers; +BAIL_OUT('could not take the daemon setup section from setupntp') unless $body; + +sub run_setupntp { + my (%opt) = @_; + my $root = tempdir(CLEANUP => 1); + + # Record every call the section makes, and answer as the case requires. + my $doubles = <<"BASH"; +log() { printf '%s\\n' "\$*" >>"$root/calls"; } +systemctl() { log "systemctl \$*"; return 0; } +timedatectl() { log "timedatectl \$*"; return 0; } +chronyd() { log "chronyd \$*"; return 0; } +logger() { log "logger \$*"; return 0; } +rm() { log "rm \$*"; return 0; } +BASH + $doubles .= $opt{hwclock} + ? qq{hwclock() { log "hwclock \$*"; return 0; }\n} + # hwclock genuinely absent: hide it from both probes the script can use + : qq{command() { if [ "\$1" = "-v" ] && [ "\$2" = "hwclock" ]; then return 1; fi; builtin command "\$@"; }\n} + . qq{type() { if [ "\$1" = "hwclock" ]; then return 1; fi; builtin type "\$@"; }\n}; + + $doubles .= "declare -a NTP_SERVERS=(" . ($opt{server} ? qq{"$opt{server}"} : '') . ")\n"; + $doubles .= "log_label=xcat\n"; + + my $rc = system('bash', '-c', $doubles . $helpers . $body); + + my $calls = ''; + if (open my $ch, '<', "$root/calls") { local $/; $calls = <$ch>; close $ch } + return { rc => $rc >> 8, calls => $calls }; } -my $setupntp = slurp('xCAT/postscripts/setupntp'); -SKIP: { - skip 'setupntp not found', 4 unless defined $setupntp; - unlike($setupntp, qr/check_exec_or_exit[^\n]*\bhwclock\b/, - 'setupntp does NOT hard-require hwclock in check_exec_or_exit'); - like($setupntp, qr/command -v hwclock/, - 'setupntp guards its hwclock use so a missing hwclock is non-fatal'); - like($setupntp, qr/systemctl\s+(?:stop|disable)\s+systemd-timesyncd/, - 'setupntp stops/disables systemd-timesyncd so it does not fight the NTP daemon'); - like($setupntp, qr/chronyd\s+-f\s+\S*\s+-q/, - 'setupntp still steps the system clock via a one-shot chronyd -q'); +# --- a management node with no hwclock: the bug this fix exists for -------- +{ + my $r = run_setupntp(hwclock => 0); + + is($r->{rc}, 0, 'setupntp completes on a node with no hwclock instead of aborting'); + like($r->{calls}, qr/^chronyd .*-q/m, + 'the system clock is still stepped, which is the part that never needed hwclock'); + unlike($r->{calls}, qr/^hwclock/m, 'and no attempt is made to use the missing hwclock'); + like($r->{calls}, qr/hwclock not present/, + 'the skipped RTC persist is reported rather than passing silently'); } +# --- a management node that has hwclock ------------------------------------ +{ + my $r = run_setupntp(hwclock => 1); + + is($r->{rc}, 0, 'setupntp completes on a node that has hwclock'); + like($r->{calls}, qr/^hwclock --systohc --utc$/m, + 'the stepped system clock is persisted to the RTC when hwclock is available'); + like($r->{calls}, qr/^chronyd .*-q/m, 'the clock is stepped in this case too'); +} + +# --- systemd-timesyncd must yield to the NTP daemon ------------------------ +foreach my $case ([ 'with hwclock', 1 ], [ 'without hwclock', 0 ]) { + my ($name, $hwclock) = @$case; + my $r = run_setupntp(hwclock => $hwclock); + + like($r->{calls}, qr/^systemctl stop systemd-timesyncd\.service$/m, + "$name: systemd-timesyncd is stopped so it stops disciplining the clock"); + like($r->{calls}, qr/^systemctl disable systemd-timesyncd\.service$/m, + "$name: systemd-timesyncd is disabled so it does not come back on the next boot"); +} + +# --- the configured NTP server reaches the clock step ---------------------- +{ + my $r = run_setupntp(hwclock => 1, server => 'ntp.example.com'); + like($r->{calls}, qr/^chronyd .*server ntp\.example\.com iburst/m, + 'the clock is stepped against the NTP server the node was given'); +} +{ + my $r = run_setupntp(hwclock => 1); + like($r->{calls}, qr/^chronyd .*pool pool\.ntp\.org iburst/m, + 'a node given no NTP server falls back to the public pool'); +} + +# --- the packaging must supply a daemon that can serve time ---------------- +# These are manifest contents, not behaviour: there is nothing to execute. +sub slurp { my $p = shift; open my $h, '<', "$repo/$p" or return undef; local $/; <$h> } + my $ctrl = slurp('xCAT/debian/control'); SKIP: { skip 'debian/control not found', 2 unless defined $ctrl; like($ctrl, qr/^Depends:.*\bchrony \| ntp\b/m, - 'xcat debian package Depends on chrony | ntp (server-capable NTP daemon)'); + 'the xcat debian package depends on a server-capable NTP daemon'); like($ctrl, qr/^Recommends:.*\butil-linux-extra\b/m, - 'xcat debian package Recommends util-linux-extra (provides hwclock on noble+)'); + 'and recommends the package that carries hwclock on noble and later'); } my $spec = slurp('xCAT/xCAT.spec'); SKIP: { skip 'xCAT.spec not found', 1 unless defined $spec; like($spec, qr/^Requires:\s*\(chrony or ntp\)/m, - 'xCAT rpm Requires (chrony or ntp) for makentp'); -} - -my $makentp = slurp('xCAT-server/lib/xcat/plugins/makentp.pm'); -SKIP: { - skip 'makentp.pm not found', 1 unless defined $makentp; - like($makentp, qr/xCAT::NTP::Backend->choose/, - 'makentp selects the NTP daemon through the xCAT::NTP::Backend selector'); + 'the xCAT rpm requires a server-capable NTP daemon'); } done_testing(); From ee4cd056233e0f517afacd88cc36b99af3052d9a Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:02:42 -0300 Subject: [PATCH 04/16] fix(xcat-core): trim the commentary around the Ubuntu NTP fix setupntp explained systemd-timesyncd in four lines where two carry the reason nothing displaces it, and repeated the hwclock rationale that the guard below already implies. NTP::Backend's header narrated that the module exists to be unit-tested. The test header restated all of it a third time, and said "the first three are behaviours of the script" about a list of three items whose third is the packaging check. Corrected and shortened; the note on why setupntp cannot simply be executed is kept, since that is not obvious from reading it. Interface documentation is left alone: choose()'s parameter list and default_backend()'s per-family table are what a caller needs. 18 comment lines removed, no behaviour change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- perl-xCAT/xCAT/NTP/Backend.pm | 11 +++++------ xCAT-test/unit/makentp_ntp_deps.t | 19 +++++++------------ xCAT/postscripts/setupntp | 12 +++++------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/perl-xCAT/xCAT/NTP/Backend.pm b/perl-xCAT/xCAT/NTP/Backend.pm index 71b5f868a..aac49081b 100644 --- a/perl-xCAT/xCAT/NTP/Backend.pm +++ b/perl-xCAT/xCAT/NTP/Backend.pm @@ -1,9 +1,8 @@ package xCAT::NTP::Backend; -# Selector for the NTP daemon xCAT configures on a node (chrony vs. ntpd), in the same spirit as -# xCAT::DHCP::Backend (ISC vs. Kea). makentp/setupntp support chronyd and ntpd only -- there is no -# systemd-timesyncd path (timesyncd is an SNTP client and cannot serve time to compute nodes), so an -# xCAT MN always needs chrony or ntpd. This module centralizes and unit-tests the choice. +# Selector for the NTP daemon xCAT configures (chrony vs. ntpd), in the same spirit as +# xCAT::DHCP::Backend. There is no timesyncd path: it is an SNTP client and cannot serve time to +# compute nodes, so an MN always needs chrony or ntpd. use strict; use warnings; @@ -33,8 +32,8 @@ sub normalize { # requested -- override (default: site.ntpbackend, else 'auto') # os_name/version -- OS identity (default: detected) # available -- optional { chrony => 0/1, ntpd => 0/1 } to bypass command detection (tests) -# check_available -- when true, downgrade chrony->ntpd (or ntpd->chrony) if the chosen one is -# absent but the other is present, and flag install=1 when neither is present. +# check_available -- when true, downgrade to whichever daemon is installed, and flag install=1 +# when neither is. sub choose { my ( $class, %args ) = @_; diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t index 3716d852e..f4fbe4e29 100644 --- a/xCAT-test/unit/makentp_ntp_deps.t +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -6,14 +6,10 @@ use FindBin; use File::Temp qw(tempdir); use Test::More; -# setupntp configures a server-capable NTP daemon on the management node. On a stock Ubuntu MN -# three things went wrong: hwclock had moved to util-linux-extra and was absent, so the fatal -# executable check aborted the ENTIRE NTP setup including the clock step that does not use it; -# systemd-timesyncd was left running and kept disciplining the clock against the daemon being -# configured; and the xcat package did not pull in a daemon that can serve time at all. -# -# The first three are behaviours of the script, so run it. The last is a property of the -# packaging manifests, so read those. +# A stock Ubuntu MN broke three ways: a missing hwclock aborted the whole NTP setup, +# systemd-timesyncd kept fighting the daemon being configured, and the xcat package pulled in no +# daemon that can serve time. The first two are behaviours of the script, so run it; the third is +# a property of the packaging manifests, so read those. my $repo = "$FindBin::Bin/../.."; my $setupntp_path = "$repo/xCAT/postscripts/setupntp"; @@ -25,10 +21,9 @@ my @lines = <$fh>; close $fh; my $source = join '', @lines; -# setupntp forces its own PATH and refuses to run unless UID is 0, so its commands cannot be -# stubbed from outside and it cannot run as an ordinary user. Take the script's own helper -# functions and the section that configures the daemon, and drive them with shell functions -- -# which bash resolves ahead of PATH, and which `type` and `command -v` both report as present. +# setupntp forces its own PATH and exits unless UID is 0, so it cannot be stubbed from outside +# or run unprivileged. Drive its own helper functions with shell functions instead: bash resolves +# those ahead of PATH, and both `type` and `command -v` report them as present. my ($helpers) = $source =~ /\A(.*?)^\[ "\$\{UID\}" -eq "0" \]/ms; my ($body) = $source =~ /^(check_exec_or_exit cp cat logger grep\n.*?)^CHRONY_CONF=/ms; BAIL_OUT('could not take the helper functions from setupntp') unless $helpers; diff --git a/xCAT/postscripts/setupntp b/xCAT/postscripts/setupntp index 7de945790..2cc903ff3 100755 --- a/xCAT/postscripts/setupntp +++ b/xCAT/postscripts/setupntp @@ -146,9 +146,9 @@ unset MASTER unset NTPSERVERS check_exec_or_exit cp cat logger grep -# hwclock is NOT required: it only persists the (already chronyd-stepped) system clock to the RTC -# below, and it is optional -- on Ubuntu 24.04+ it moved to util-linux-extra, which is absent on -# minimal cloud images. Requiring it here aborted the whole NTP setup (and the clock step) on noble. +# hwclock is NOT required: it only persists the already-stepped clock to the RTC below, and on +# Ubuntu 24.04+ it moved to util-linux-extra, absent on minimal images. Requiring it here aborted +# the whole NTP setup, including the clock step that does not use it. check_exec_or_exit systemctl timedatectl systemctl stop ntp.service 2>/dev/null @@ -159,10 +159,8 @@ systemctl disable ntpd.service 2>/dev/null systemctl disable ntp-wait.service 2>/dev/null systemctl disable ntpdate.service 2>/dev/null -# systemd-timesyncd is an SNTP client that also disciplines the system clock. It is part of systemd -# (not a "time-daemon" package), so it coexists with chrony/ntpd at the package level but must be -# stopped or it fights the NTP daemon we are configuring. This is the default time-sync stack on -# Ubuntu, where it must yield to chrony (timesyncd cannot serve time to compute nodes). +# systemd-timesyncd ships as part of systemd rather than a time-daemon package, so nothing +# displaces it and it keeps disciplining the clock against the daemon being configured. systemctl stop systemd-timesyncd.service 2>/dev/null systemctl disable systemd-timesyncd.service 2>/dev/null From a13a74f4c2ca1b016aef32f68a94b7d333b20fb7 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:58:43 -0300 Subject: [PATCH 05/16] fix(xcat-core): let site.ntpbackend reach the nodes, not just the management node makentp picks the NTP daemon through xCAT::NTP::Backend, but then runs setupntp -- on the management node and, through updatenode -P, on every service node -- and setupntp decided for itself with `check_executes chronyd || USE_NTPD=yes`. A cluster with site.ntpbackend=ntpd and chronyd present therefore configured ntpd on the MN and chrony everywhere else. The selector was one code path only on the side that does not write the config. setupntp now takes --backend chrony|ntpd, and makentp passes what it chose on both call sites. The service-node dispatch passes the cluster's intent rather than this host's availability: a service node may have a different daemon installed, and the requested backend is a preference -- a node without chronyd still falls back to ntpd and logs that it did, rather than failing. --use-ntpd keeps working. Two results of choose() were computed and never read. A downgrade is now reported, so an admin who asked for one daemon and got the other is told. install=1 -- neither daemon present -- is an error naming the daemon that is missing, instead of falling through to the ntpd branch and reporting "Please make sure ntpd is installed" even when chrony was the preferred choice. Six cases cover the selection: the backend honoured in both directions, the probe still used when none is given, and the fallback when the requested daemon is absent. Removing the --backend case fails one; ignoring the preference fails two. Also worth stating plainly, since the PR reads as a management-node fix: setupntp stops and disables systemd-timesyncd wherever it runs, nodes included. It has to -- timesyncd disciplines the clock against the daemon being configured -- but a node that was relying on it loses it. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/makentp.pm | 22 +++++++++++++++++-- xCAT-test/unit/makentp_ntp_deps.t | 27 ++++++++++++++++++++++++ xCAT/postscripts/setupntp | 28 ++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/makentp.pm b/xCAT-server/lib/xcat/plugins/makentp.pm index 620a45e85..1dd8e0d8c 100644 --- a/xCAT-server/lib/xcat/plugins/makentp.pm +++ b/xCAT-server/lib/xcat/plugins/makentp.pm @@ -261,13 +261,22 @@ sub process_request { send_msg(\%request, 1, $ntp_backend->{error}); return 1; } + if ($ntp_backend->{downgraded}) { + send_msg(\%request, 0, + "NTP backend $ntp_backend->{downgraded} is not installed; using $ntp_backend->{name} instead."); + } + if ($ntp_backend->{install}) { + send_msg(\%request, 1, + "Neither chrony nor ntp is installed on $nodename. Install $ntp_backend->{name}, or set site.ntpbackend to the daemon you have."); + return 1; + } my $have_systemctl = (-x "/usr/bin/systemctl" || -x "/bin/systemctl"); # Handle chronyd here, if ($ntp_backend->{name} eq 'chrony' && $have_systemctl) { send_msg(\%request, 0, "Will configure chronyd instead."); - my $cmd = "/install/postscripts/setupntp " . + my $cmd = "/install/postscripts/setupntp --backend $ntp_backend->{name} " . join(' ', split(',', $ntp_servers)); send_msg(\%request, 0, "Calling ... " . $cmd); @@ -497,12 +506,21 @@ HANDLE_MAKENTP_A: my @servicenodes = xCAT::ServiceNodeUtils->getSNList('ntpserver'); if (@servicenodes > 0) { send_msg(\%request, 0, "configuring service nodes: @servicenodes"); + + # Pass the cluster's intent, not this host's availability: a service node may have a + # different daemon installed, and setupntp falls back locally when it does. + require xCAT::NTP::Backend; + my $sn_backend = xCAT::NTP::Backend->choose(); + my $sn_script = $sn_backend->{name} + ? "setupntp --backend $sn_backend->{name}" + : "setupntp"; + my $ret = xCAT::Utils->runxcmd( { command => ['updatenode'], node => \@servicenodes, - arg => [ "-P", "setupntp" ], + arg => [ "-P", $sn_script ], }, $sub_req, -1, 1 ); diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t index f4fbe4e29..a849fe311 100644 --- a/xCAT-test/unit/makentp_ntp_deps.t +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -25,9 +25,13 @@ my $source = join '', @lines; # or run unprivileged. Drive its own helper functions with shell functions instead: bash resolves # those ahead of PATH, and both `type` and `command -v` report them as present. my ($helpers) = $source =~ /\A(.*?)^\[ "\$\{UID\}" -eq "0" \]/ms; +my ($args) = $source =~ /^(# Handle command line arguments\n.*?)^if \[ "\$\{#NTP_SERVERS\[@\]\}"/ms; +my ($select) = $source =~ /^(# The requested backend is a preference.*?)^if \[ -n "\$\{USE_NTPD\}"/ms; my ($body) = $source =~ /^(check_exec_or_exit cp cat logger grep\n.*?)^CHRONY_CONF=/ms; BAIL_OUT('could not take the helper functions from setupntp') unless $helpers; BAIL_OUT('could not take the daemon setup section from setupntp') unless $body; +BAIL_OUT('could not take the argument parsing from setupntp') unless $args; +BAIL_OUT('could not take the backend selection from setupntp') unless $select; sub run_setupntp { my (%opt) = @_; @@ -123,4 +127,27 @@ SKIP: { 'the xCAT rpm requires a server-capable NTP daemon'); } +# --- the backend the management node chose must reach the node ------------ +# makentp selects the daemon from site.ntpbackend through xCAT::NTP::Backend and passes it here, +# so a cluster told to use ntpd does not get chrony on every node that happens to have it. +foreach my $case ( + # argv chronyd present? expected daemon + [ '--backend ntpd pool.ntp.org', 'nothing', 'ntpd', 'ntpd is honoured even where chronyd is installed' ], + [ '--backend chrony pool.ntp.org','nothing', 'chrony', 'chrony is honoured' ], + [ 'pool.ntp.org', 'nothing', 'chrony', 'with no backend given the probe still picks chrony' ], + [ 'pool.ntp.org', 'chronyd', 'ntpd', 'with no backend given and no chronyd it falls back' ], + [ '--backend chrony pool.ntp.org','chronyd', 'ntpd', 'chrony requested but absent falls back rather than failing' ], + [ '--use-ntpd pool.ntp.org', 'nothing', 'ntpd', 'the legacy --use-ntpd flag still forces ntpd' ], +) { + my ($argv, $missing, $want, $name) = @$case; + my $root = File::Temp::tempdir(CLEANUP => 1); + my $prelude = "logger() { printf '%s\\n' \"\$*\" >>\"$root/log\"; return 0; }\n" + . "check_executes() { for c in \"\$@\"; do [ \"\$c\" = \"$missing\" ] && return 1; done; return 0; }\n" + . "log_label=xcat\nset -- $argv\n"; + my $out = `bash -c 'exec 2>/dev/null; $prelude$args$select +printf "USE_NTPD=%s\\n" "\${USE_NTPD:-}"' 2>/dev/null`; + my $got = ($out =~ /USE_NTPD=yes/) ? 'ntpd' : 'chrony'; + is($got, $want, $name); +} + done_testing(); diff --git a/xCAT/postscripts/setupntp b/xCAT/postscripts/setupntp index 2cc903ff3..e1d3cf9de 100755 --- a/xCAT/postscripts/setupntp +++ b/xCAT/postscripts/setupntp @@ -103,7 +103,17 @@ do case "$1" in "--use-ntpd") # Use traditional ntpd - USE_NTPD="yes" + NTP_BACKEND="ntpd" + ;; + "--backend") + # The daemon xCAT::NTP::Backend picked for this cluster, passed by makentp so + # site.ntpbackend reaches the nodes and not just the management node. + shift + case "$1" in + "chrony"|"chronyd") NTP_BACKEND="chrony" ;; + "ntp"|"ntpd") NTP_BACKEND="ntpd" ;; + *) logger -t $log_label -p local4.warning "setupntp: ignoring unknown --backend '$1'" ;; + esac ;; *) NTP_SERVERS+=($1) @@ -129,8 +139,20 @@ then esac fi -check_executes chronyd >/dev/null 2>&1 || USE_NTPD="yes" -check_executes systemctl >/dev/null 2>&1 || USE_NTPD="yes" +# The requested backend is a preference, not a guarantee: a node that does not have chronyd +# still has to be configured, so fall back to ntpd and say so rather than failing. +if [ "${NTP_BACKEND}" = "ntpd" ] +then + USE_NTPD="yes" +else + check_executes chronyd >/dev/null 2>&1 || USE_NTPD="yes" + check_executes systemctl >/dev/null 2>&1 || USE_NTPD="yes" + if [ -n "${USE_NTPD}" ] && [ "${NTP_BACKEND}" = "chrony" ] + then + logger -t $log_label -p local4.warning \ + "setupntp: chrony requested but chronyd/systemctl are absent; using ntpd" + fi +fi if [ -n "${USE_NTPD}" ] then From dbc953a069f93782104e72a651640de5c3a7e293 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:53:52 -0300 Subject: [PATCH 06/16] test(makentp): nothing connects the NTP selector to makentp ntp_backend_selection.t covers xCAT::NTP::Backend thoroughly, but copying the base makentp.pm over the head one leaves the entire unit suite byte-identical. Zero assertions cover the file the fix is named after: the call site that consumes the selector, the --backend pass-through to setupntp, and the abort branches for "selector errored" and "neither daemon installed" all run in no test. A helper can be perfectly covered while nothing calls it. This drives the decisions the call site makes -- abort vs configure, the downgrade note, and the command handed to setupntp -- rather than process_request, which needs a management node. It fails at the extraction guard until those decisions are routines that can be called, so the behavioural proof is the mutation on top of the fix, not this red alone. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/makentp_backend_call_site.t | 100 +++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 xCAT-test/unit/makentp_backend_call_site.t diff --git a/xCAT-test/unit/makentp_backend_call_site.t b/xCAT-test/unit/makentp_backend_call_site.t new file mode 100644 index 000000000..4a45862fa --- /dev/null +++ b/xCAT-test/unit/makentp_backend_call_site.t @@ -0,0 +1,100 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use FindBin; +use Test::More; + +# Regression: the NTP backend selector was well covered and nothing connected it to makentp. +# +# ntp_backend_selection.t exercises xCAT::NTP::Backend thoroughly, but copying the BASE +# makentp.pm over the head one left the whole unit suite byte-identical -- so the call site +# that consumes the selector, the --backend pass-through and the abort branches ran in no test +# at all. A helper can be perfectly covered while nothing calls it. +# +# process_request needs a management node, so the decisions it makes about the selector's +# answer live in two small routines that take their inputs and return an answer, and those are +# what this drives. The side effects (send_msg, runcmd, updatenode) stay in the caller. + +my $repo_root = File::Spec->rel2abs( + File::Spec->catdir( $FindBin::Bin, '..', '..' ) +); +my $plugin = File::Spec->catfile( + $repo_root, 'xCAT-server', 'lib', 'xcat', 'plugins', 'makentp.pm' +); +plan skip_all => "makentp.pm not found" unless -f $plugin; + +my $src = do { local $/; open my $fh, '<', $plugin or die $!; <$fh> }; + +# BAIL_OUT rather than skip, so a rename fails loudly instead of silently covering nothing. +my @wanted = qw(ntp_backend_action setupntp_command); +my $body = ''; +for my $name (@wanted) { + my ($sub) = $src =~ /\n(sub \Q$name\E \{.*?\n\})\n/s; + BAIL_OUT("could not extract $name from makentp.pm") unless defined $sub; + $body .= "$sub\n"; +} + +{ + package T; + eval "$body; 1" or main::BAIL_OUT("could not eval the makentp helpers: $@"); +} + +# --- the selector said no backend is usable at all ------------------------- +{ + my $r = T::ntp_backend_action( { error => 'site.ntpbackend is nonsense' }, 'mn1' ); + is( $r->{action}, 'abort', 'a selector error aborts makentp' ); + is( $r->{error}, 'site.ntpbackend is nonsense', + 'and the selector error is what the caller reports' ); +} + +# --- neither daemon installed ---------------------------------------------- +{ + my $r = T::ntp_backend_action( { name => 'chrony', install => 1 }, 'mn1' ); + is( $r->{action}, 'abort', 'nothing installed aborts rather than configuring' ); + like( $r->{error}, qr/Neither chrony nor ntp is installed on mn1/, + 'the abort names the node' ); + like( $r->{error}, qr/set site\.ntpbackend/, + 'and tells the admin how to override the choice' ); +} + +# --- the requested backend is absent, so the selector downgraded ------------ +{ + my $r = T::ntp_backend_action( + { name => 'chrony', downgraded => 'ntpd' }, 'mn1' ); + is( $r->{action}, 'configure', 'a downgrade still configures' ); + is( $r->{name}, 'chrony', 'using the daemon that is actually installed' ); + like( join( '|', @{ $r->{notes} } ), + qr/ntpd is not installed; using chrony instead/, + 'and says so, rather than silently using something else' ); +} + +# --- the ordinary case ------------------------------------------------------ +{ + my $r = T::ntp_backend_action( { name => 'chrony' }, 'mn1' ); + is( $r->{action}, 'configure', 'an installed backend configures' ); + is( $r->{name}, 'chrony', 'with the name the selector chose' ); + is_deeply( $r->{notes}, [], 'and says nothing extra' ); +} + +{ + my $r = T::ntp_backend_action( { name => 'ntpd' }, 'mn1' ); + is( $r->{name}, 'ntpd', 'ntpd is carried through as chosen' ); +} + +# --- the --backend pass-through, which is what setupntp reads --------------- +{ + is( T::setupntp_command( 'chrony', '10.0.0.1,10.0.0.2' ), + '/install/postscripts/setupntp --backend chrony 10.0.0.1 10.0.0.2', + 'the chosen backend reaches setupntp and the server list is space separated' ); + + is( T::setupntp_command( 'ntpd', '10.0.0.1' ), + '/install/postscripts/setupntp --backend ntpd 10.0.0.1', + 'a different backend produces a different command' ); + + unlike( T::setupntp_command( 'chrony', '10.0.0.1' ), qr/,/, + 'no comma survives into the argument list' ); +} + +done_testing(); From 124e2782dbd8cc843cbf36910278a5f6b32c1c54 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:54:57 -0300 Subject: [PATCH 07/16] fix(makentp): make the backend call site testable The NTP backend selector was well covered and nothing connected it to makentp: copying the base makentp.pm over the head one left the whole unit suite byte-identical, so the branches that consume choose()'s answer -- abort on a selector error, warn on a downgrade, abort when neither daemon is installed -- and the --backend argument handed to setupntp were covered by nothing. They were unreachable from a test because they sat inside process_request, which needs a management node. Move the decisions into ntp_backend_action() and setupntp_command(), which take their inputs and return an answer; the caller keeps send_msg and runcmd. No behaviour changes -- the same messages are sent on the same conditions, and the same command is built. Verified by mutation rather than by reading: dropping the install abort reds 3 of 15, the downgrade note 1, the --backend argument 2, the server-list split 1, and the selector-error abort 2. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-server/lib/xcat/plugins/makentp.pm | 84 ++++++++++++++++++++----- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/xCAT-server/lib/xcat/plugins/makentp.pm b/xCAT-server/lib/xcat/plugins/makentp.pm index 1dd8e0d8c..49a35f85e 100644 --- a/xCAT-server/lib/xcat/plugins/makentp.pm +++ b/xCAT-server/lib/xcat/plugins/makentp.pm @@ -209,6 +209,70 @@ sub preprocess_request { =cut #-------------------------------------------------------------------------------- +#------------------------------------------------------------------------------- + +=head3 ntp_backend_action + + Decide what makentp should do with the answer xCAT::NTP::Backend->choose gave it. + + Kept separate from process_request so the decision can be driven directly: the caller + keeps the side effects (send_msg, runcmd) and this returns only what to do. + + Arguments: + $backend the hashref from xCAT::NTP::Backend->choose + $nodename the host makentp is configuring, for the error text + Returns: + a hashref: action => 'abort'|'configure', error => the message to report when + aborting, name => the daemon to configure, notes => messages to report either way + +=cut + +#------------------------------------------------------------------------------- +sub ntp_backend_action { + my ($backend, $nodename) = @_; + + $backend ||= {}; + return { action => 'abort', error => $backend->{error}, notes => [] } + if $backend->{error}; + + my @notes; + push @notes, + "NTP backend $backend->{downgraded} is not installed; using $backend->{name} instead." + if $backend->{downgraded}; + + return { + action => 'abort', + error => "Neither chrony nor ntp is installed on $nodename. " + . "Install $backend->{name}, or set site.ntpbackend to the daemon you have.", + notes => \@notes, + } if $backend->{install}; + + return { action => 'configure', name => $backend->{name}, notes => \@notes }; +} + +#------------------------------------------------------------------------------- + +=head3 setupntp_command + + Build the setupntp invocation. The server list arrives comma separated from the site + table and setupntp takes them as separate arguments. + + Arguments: + $backend_name the daemon setupntp should configure + $ntp_servers the comma separated server list + Returns: + the command line + +=cut + +#------------------------------------------------------------------------------- +sub setupntp_command { + my ($backend_name, $ntp_servers) = @_; + + return "/install/postscripts/setupntp --backend $backend_name " + . join(' ', split(',', $ntp_servers)); +} + sub process_request { my $req = shift; my $callback = shift; @@ -257,27 +321,19 @@ sub process_request { # downgrades chrony->ntpd (or vice versa) to whichever is actually installed. require xCAT::NTP::Backend; my $ntp_backend = xCAT::NTP::Backend->choose(check_available => 1); - if ($ntp_backend->{error}) { - send_msg(\%request, 1, $ntp_backend->{error}); - return 1; - } - if ($ntp_backend->{downgraded}) { - send_msg(\%request, 0, - "NTP backend $ntp_backend->{downgraded} is not installed; using $ntp_backend->{name} instead."); - } - if ($ntp_backend->{install}) { - send_msg(\%request, 1, - "Neither chrony nor ntp is installed on $nodename. Install $ntp_backend->{name}, or set site.ntpbackend to the daemon you have."); + my $ntp_action = ntp_backend_action($ntp_backend, $nodename); + send_msg(\%request, 0, $_) for @{ $ntp_action->{notes} }; + if ($ntp_action->{action} eq 'abort') { + send_msg(\%request, 1, $ntp_action->{error}); return 1; } my $have_systemctl = (-x "/usr/bin/systemctl" || -x "/bin/systemctl"); # Handle chronyd here, - if ($ntp_backend->{name} eq 'chrony' && $have_systemctl) { + if ($ntp_action->{name} eq 'chrony' && $have_systemctl) { send_msg(\%request, 0, "Will configure chronyd instead."); - my $cmd = "/install/postscripts/setupntp --backend $ntp_backend->{name} " . - join(' ', split(',', $ntp_servers)); + my $cmd = setupntp_command($ntp_action->{name}, $ntp_servers); send_msg(\%request, 0, "Calling ... " . $cmd); my $result = xCAT::Utils->runcmd($cmd, 0); From fdeafa3e3f3487a0aa31db6137f942200e7a2583 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:00:40 -0300 Subject: [PATCH 08/16] test(setupntp): timesyncd is only stopped on the chrony path setupntp stops and disables systemd-timesyncd so it does not discipline the clock against the daemon being configured. That block sits ~24 lines below the `exec setupntp.traditional` the ntpd path takes, and setupntp.traditional never mentions timesyncd -- so on the ntpd path the disable is never reached and both end up stepping the clock. setupntp resets PATH at the top, so PATH stubs cannot shadow anything in it; the region is extracted and driven with systemctl shadowed by a shell function, which bash resolves ahead of PATH. `exec` is a builtin and cannot be shadowed, so the harness puts a recording stand-in where the script execs -- the run ending there is the behaviour under test. Five of the eight assertions pass already, pinning the chrony path and the hand-off itself. The three that fail are the ntpd path. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../unit/setupntp_timesyncd_both_backends.t | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 xCAT-test/unit/setupntp_timesyncd_both_backends.t diff --git a/xCAT-test/unit/setupntp_timesyncd_both_backends.t b/xCAT-test/unit/setupntp_timesyncd_both_backends.t new file mode 100644 index 000000000..706f9adcd --- /dev/null +++ b/xCAT-test/unit/setupntp_timesyncd_both_backends.t @@ -0,0 +1,108 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +# Regression: systemd-timesyncd is only stopped on the chrony path. +# +# setupntp disciplines the clock with whichever daemon it configures, and timesyncd is an SNTP +# client that disciplines the same clock. Leaving it running means two things stepping the clock. +# The stop/disable sits ~24 lines BELOW the `exec setupntp.traditional` that the ntpd path takes, +# and setupntp.traditional never touches timesyncd -- so on the ntpd path it is never reached. +# +# setupntp resets PATH at the top, so PATH stubs cannot shadow anything in it. The region that +# decides this is extracted instead and driven with systemctl and friends shadowed by shell +# functions, which bash resolves ahead of PATH. `exec` is a builtin and cannot be shadowed, so +# the harness puts a recording stand-in where the script execs, and the exec ends the run -- +# which is exactly the behaviour under test. + +my $repo_root = File::Spec->rel2abs( + File::Spec->catdir( $FindBin::Bin, '..', '..' ) +); +my $setupntp = File::Spec->catfile( $repo_root, 'xCAT', 'postscripts', 'setupntp' ); +plan skip_all => "setupntp not found" unless -f $setupntp; + +my $src = do { local $/; open my $fh, '<', $setupntp or die $!; <$fh> }; + +# From the ntpd dispatch through the timesyncd disable: the ordering between them is the point. +# BAIL_OUT rather than skip, so a rename fails loudly instead of silently covering nothing. +my ($region) = $src =~ /\n(if \[ -n "\$\{USE_NTPD\}" \]\n.*?systemctl disable systemd-timesyncd\.service 2>\/dev\/null\n)/s; +BAIL_OUT('could not extract the ntpd-dispatch/timesyncd region from setupntp') + unless defined $region; + +my $dir = tempdir( CLEANUP => 1 ); +my $run = 0; + +sub drive { + my ($use_ntpd) = @_; + $run++; + my $root = File::Spec->catdir( $dir, "run$run" ); + mkdir $root; + my $calls = File::Spec->catfile( $root, 'calls' ); + + # The script execs "${0%/*}/setupntp.traditional"; $0 is the harness, so this is where it + # lands. Recording rather than executing anything real. + my $trad = File::Spec->catfile( $root, 'setupntp.traditional' ); + open my $t, '>', $trad or die $!; + print $t "#!/bin/bash\necho 'EXEC setupntp.traditional' >> '$calls'\nexit 0\n"; + close $t; + chmod 0755, $trad; + + my $harness = File::Spec->catfile( $root, 'harness.sh' ); + open my $fh, '>', $harness or die $!; + print $fh <<"PRE"; +#!/bin/bash +USE_NTPD='$use_ntpd' +NTP_SERVERS=(pool.ntp.org) +log_label=xcat +logger(){ :; } +systemctl(){ echo "systemctl \$*" >> '$calls'; return 0; } +timedatectl(){ echo "timedatectl \$*" >> '$calls'; return 0; } +check_exec_or_exit(){ :; } +check_executes(){ return 0; } +PRE + print $fh $region, "\n"; + close $fh; + + system( '/bin/bash', $harness ); + return '' unless -f $calls; + return do { local $/; open my $c, '<', $calls or die $!; <$c> }; +} + +# The chrony path: this already worked, and pins the behaviour we are extending. +my $chrony = drive(''); +like( $chrony, qr/^systemctl stop systemd-timesyncd\.service$/m, + 'chrony path: timesyncd is stopped' ); +like( $chrony, qr/^systemctl disable systemd-timesyncd\.service$/m, + 'chrony path: timesyncd is disabled' ); +unlike( $chrony, qr/^EXEC setupntp\.traditional$/m, + 'chrony path: does not hand off to setupntp.traditional' ); + +# The ntpd path: it hands off, so anything that must happen has to happen first. +my $ntpd = drive('yes'); +like( $ntpd, qr/^EXEC setupntp\.traditional$/m, + 'ntpd path: hands off to setupntp.traditional' ); +like( $ntpd, qr/^systemctl stop systemd-timesyncd\.service$/m, + 'ntpd path: timesyncd is stopped too' ); +like( $ntpd, qr/^systemctl disable systemd-timesyncd\.service$/m, + 'ntpd path: timesyncd is disabled too' ); + +my ($before_exec) = $ntpd =~ /\A(.*?)^EXEC setupntp\.traditional/ms; +ok( defined $before_exec && $before_exec =~ /systemctl disable systemd-timesyncd/, + 'ntpd path: the disable happens before the hand-off, not after it' ); + +# setupntp.traditional does not do it either, which is why the ordering above matters. +my $trad_src = File::Spec->catfile( + $repo_root, 'xCAT', 'postscripts', 'setupntp.traditional' ); +SKIP: { + skip 'setupntp.traditional not found', 1 unless -f $trad_src; + my $t = do { local $/; open my $h, '<', $trad_src or die $!; <$h> }; + unlike( $t, qr/timesyncd/, + 'setupntp.traditional does not handle timesyncd itself' ); +} + +done_testing(); From 994b47a9479f2fb2e3c349359f297530c02f3066 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:04:24 -0300 Subject: [PATCH 09/16] fix(setupntp): timesyncd keeps running when the backend is ntpd The stop/disable sat below the `exec setupntp.traditional` that the ntpd path takes, and setupntp.traditional never mentions timesyncd, so on that path it was never reached: ntpd and timesyncd both ended up disciplining the clock. Hoist it above the hand-off so both backends get it, guarded on systemctl existing -- the ntpd path is taken precisely when systemctl may be absent. The comment justifying it was also wrong, and is corrected: chrony and ntpsec both Conflicts: time-daemon, so installing either already displaces timesyncd. That only helps when an install actually happens; on a re-run, or where the daemon was already present, timesyncd is still enabled, which is what this covers. The four timesyncd assertions in makentp_ntp_deps.t are dropped rather than repaired: they ran over the section from `check_exec_or_exit cp cat logger grep` onwards, which only the chrony path reaches, so the code they covered is no longer in their window. setupntp_timesyncd_both_backends.t drives both paths instead. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/makentp_ntp_deps.t | 14 +++------- .../unit/setupntp_timesyncd_both_backends.t | 28 ++++++++++++++++--- xCAT/postscripts/setupntp | 16 +++++++---- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t index a849fe311..a0cb38143 100644 --- a/xCAT-test/unit/makentp_ntp_deps.t +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -84,16 +84,10 @@ BASH like($r->{calls}, qr/^chronyd .*-q/m, 'the clock is stepped in this case too'); } -# --- systemd-timesyncd must yield to the NTP daemon ------------------------ -foreach my $case ([ 'with hwclock', 1 ], [ 'without hwclock', 0 ]) { - my ($name, $hwclock) = @$case; - my $r = run_setupntp(hwclock => $hwclock); - - like($r->{calls}, qr/^systemctl stop systemd-timesyncd\.service$/m, - "$name: systemd-timesyncd is stopped so it stops disciplining the clock"); - like($r->{calls}, qr/^systemctl disable systemd-timesyncd\.service$/m, - "$name: systemd-timesyncd is disabled so it does not come back on the next boot"); -} +# systemd-timesyncd used to be checked here, over $body -- the section from +# `check_exec_or_exit cp cat logger grep` onwards, which only the chrony path reaches. The +# stop/disable has moved above the ntpd hand-off so it runs on both paths, which puts it outside +# this window; setupntp_timesyncd_both_backends.t drives both paths and asserts it there. # --- the configured NTP server reaches the clock step ---------------------- { diff --git a/xCAT-test/unit/setupntp_timesyncd_both_backends.t b/xCAT-test/unit/setupntp_timesyncd_both_backends.t index 706f9adcd..95486710a 100644 --- a/xCAT-test/unit/setupntp_timesyncd_both_backends.t +++ b/xCAT-test/unit/setupntp_timesyncd_both_backends.t @@ -28,11 +28,31 @@ plan skip_all => "setupntp not found" unless -f $setupntp; my $src = do { local $/; open my $fh, '<', $setupntp or die $!; <$fh> }; -# From the ntpd dispatch through the timesyncd disable: the ordering between them is the point. +# Take the span that contains BOTH the ntpd dispatch and the timesyncd handling, whichever +# order they appear in -- the ordering is asserted at runtime below, from what the run actually +# recorded, not from where the text sits. Anchored on the dispatch and on the `unset` block that +# follows it, so a reordering does not silently shrink the region to cover only one of them. # BAIL_OUT rather than skip, so a rename fails loudly instead of silently covering nothing. -my ($region) = $src =~ /\n(if \[ -n "\$\{USE_NTPD\}" \]\n.*?systemctl disable systemd-timesyncd\.service 2>\/dev\/null\n)/s; -BAIL_OUT('could not extract the ntpd-dispatch/timesyncd region from setupntp') - unless defined $region; +my $dispatch_at = index( $src, 'if [ -n "${USE_NTPD}" ]' . "\nthen" ); +my $timesyncd_at = index( $src, 'systemctl stop systemd-timesyncd.service' ); +my $end_at = index( $src, '# Unset xCAT passed environment variables' ); +BAIL_OUT('could not locate the ntpd dispatch in setupntp') if $dispatch_at < 0; +BAIL_OUT('could not locate the timesyncd handling in setupntp') if $timesyncd_at < 0; +BAIL_OUT('could not locate the end of the dispatch region') if $end_at < 0; + +BAIL_OUT('the timesyncd handling is outside the extracted region') + if $timesyncd_at > $end_at; + +# Start at the top of the paragraph the earlier landmark sits in, so an enclosing `if ... then` +# comes with its `fi`. Slicing at the landmark itself orphaned the guard and the region would +# not parse. +my $start_at = $dispatch_at < $timesyncd_at ? $dispatch_at : $timesyncd_at; +my $para = rindex( $src, "\n\n", $start_at ); +$start_at = $para + 2 if $para >= 0; + +my $region = substr( $src, $start_at, $end_at - $start_at ); +BAIL_OUT('the extracted region does not parse as shell') + if system( '/bin/bash', '-n', '-c', "f(){ :; }\n$region" ) != 0; my $dir = tempdir( CLEANUP => 1 ); my $run = 0; diff --git a/xCAT/postscripts/setupntp b/xCAT/postscripts/setupntp index e1d3cf9de..425ea665f 100755 --- a/xCAT/postscripts/setupntp +++ b/xCAT/postscripts/setupntp @@ -154,6 +154,17 @@ else fi fi +# Both backends discipline the clock, and so does systemd-timesyncd, so it has to go either +# way. Installing chrony or ntpsec usually displaces it already -- both Conflicts: time-daemon -- +# but that only helps when an install actually happens; on a re-run, or where the daemon was +# already present, timesyncd is still enabled. This must stay ABOVE the ntpd hand-off below: +# that path execs setupntp.traditional, which never returns and never touches timesyncd. +if check_executes systemctl >/dev/null 2>&1 +then + systemctl stop systemd-timesyncd.service 2>/dev/null + systemctl disable systemd-timesyncd.service 2>/dev/null +fi + if [ -n "${USE_NTPD}" ] then # Call setupntp.traditional, and pass the parsed ntp servers @@ -181,11 +192,6 @@ systemctl disable ntpd.service 2>/dev/null systemctl disable ntp-wait.service 2>/dev/null systemctl disable ntpdate.service 2>/dev/null -# systemd-timesyncd ships as part of systemd rather than a time-daemon package, so nothing -# displaces it and it keeps disciplining the clock against the daemon being configured. -systemctl stop systemd-timesyncd.service 2>/dev/null -systemctl disable systemd-timesyncd.service 2>/dev/null - # On Ubuntu 18.04 systemctl stop chrony.service 2>/dev/null # On RHEL 7, 8 From 0925e2b599d429bbfae68a71f83aa57e331834ef Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:33:36 -0300 Subject: [PATCH 10/16] test(makentp): the suite really disabled timesyncd on the host running it Hoisting the systemd-timesyncd stop/disable above the ntpd hand-off moved it inside the window makentp_ntp_deps.t extracts as $select, and that harness stubbed only logger and check_executes. So the extracted region called the real systemctl -- and the unit suite runs as root in CI. Probed with a recording systemctl first on PATH, `prove makentp_ntp_deps.t` made 12 real invocations, stop and disable for each of the six selection cases; on the Ubuntu review MN a stand-in timesyncd unit went from enabled/active to disabled/inactive while the suite reported PASS. That is the "never let a test escape its scratch tree" rule, and it failed silently: the assertions passed either way, so nothing said the host had been changed. Shadow every command the region can reach, not only the ones it reached when this was written -- the region moves. Bash resolves functions ahead of $PATH, so these win without touching PATH. The stubs use echo rather than printf '...': the harness runs `bash -c '...'`, so a single quote inside the prelude closes that string early. The existing logger stub gets away with it; three more did not, and the ntpd selection cases failed until they were rewritten. Verified after: zero real systemctl invocations from the whole unit suite under the same probe, and 18/18 still pass. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/makentp_ntp_deps.t | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t index a0cb38143..199e2674d 100644 --- a/xCAT-test/unit/makentp_ntp_deps.t +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -135,7 +135,15 @@ foreach my $case ( ) { my ($argv, $missing, $want, $name) = @$case; my $root = File::Temp::tempdir(CLEANUP => 1); + # Every command the extracted region can reach has to be shadowed, not just the ones it + # reached when this was written: the region moves. systemctl is here because the + # systemd-timesyncd stop/disable was hoisted above the ntpd hand-off and landed inside this + # window -- unstubbed, and the suite runs as root in CI, it really disabled timesyncd on the + # host running the tests. Bash resolves functions ahead of $PATH, so these win. my $prelude = "logger() { printf '%s\\n' \"\$*\" >>\"$root/log\"; return 0; }\n" + . "systemctl() { echo \"systemctl \$*\" >>\"$root/calls\"; return 0; }\n" + . "timedatectl() { echo \"timedatectl \$*\" >>\"$root/calls\"; return 0; }\n" + . "hwclock() { echo \"hwclock \$*\" >>\"$root/calls\"; return 0; }\n" . "check_executes() { for c in \"\$@\"; do [ \"\$c\" = \"$missing\" ] && return 1; done; return 0; }\n" . "log_label=xcat\nset -- $argv\n"; my $out = `bash -c 'exec 2>/dev/null; $prelude$args$select From 5d4dee34ae26ef1ef51d15a000fbc9f5775732c2 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:39:34 -0300 Subject: [PATCH 11/16] test(setupntp): the ntpd backend never falls back to chrony setupntp treats the requested backend as a preference in one direction only. A node given --backend chrony without chronyd uses ntpd, but a node given --backend ntpd without ntpd still hands over to setupntp.traditional, which writes /etc/ntp.conf and starts a daemon that is not installed. The selection cases in makentp_ntp_deps.t now also make ntpd absent. The stub for check_executes takes a list of absent commands, so a case can hide ntpd, or ntpd and chronyd together. Two cases fail: --backend ntpd and --use-ntpd both keep ntpd where only chrony is present. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/makentp_ntp_deps.t | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/xCAT-test/unit/makentp_ntp_deps.t b/xCAT-test/unit/makentp_ntp_deps.t index 199e2674d..bcf0cfe7f 100644 --- a/xCAT-test/unit/makentp_ntp_deps.t +++ b/xCAT-test/unit/makentp_ntp_deps.t @@ -124,14 +124,20 @@ SKIP: { # --- the backend the management node chose must reach the node ------------ # makentp selects the daemon from site.ntpbackend through xCAT::NTP::Backend and passes it here, # so a cluster told to use ntpd does not get chrony on every node that happens to have it. +# The backend is a preference on both sides: a node that does not have the requested daemon is +# configured with the other one, rather than handed a daemon that is not there. foreach my $case ( - # argv chronyd present? expected daemon + # argv absent commands expected daemon [ '--backend ntpd pool.ntp.org', 'nothing', 'ntpd', 'ntpd is honoured even where chronyd is installed' ], [ '--backend chrony pool.ntp.org','nothing', 'chrony', 'chrony is honoured' ], [ 'pool.ntp.org', 'nothing', 'chrony', 'with no backend given the probe still picks chrony' ], [ 'pool.ntp.org', 'chronyd', 'ntpd', 'with no backend given and no chronyd it falls back' ], [ '--backend chrony pool.ntp.org','chronyd', 'ntpd', 'chrony requested but absent falls back rather than failing' ], [ '--use-ntpd pool.ntp.org', 'nothing', 'ntpd', 'the legacy --use-ntpd flag still forces ntpd' ], + [ '--backend ntpd pool.ntp.org', 'ntpd', 'chrony', 'ntpd requested but absent falls back to chrony' ], + [ '--use-ntpd pool.ntp.org', 'ntpd', 'chrony', 'the legacy flag falls back the same way' ], + [ '--backend ntpd pool.ntp.org', 'ntpd chronyd', 'ntpd', 'with neither daemon present the request is kept' ], + [ '--backend ntpd pool.ntp.org', 'ntpd systemctl', 'ntpd', 'chrony without systemctl is no fallback' ], ) { my ($argv, $missing, $want, $name) = @$case; my $root = File::Temp::tempdir(CLEANUP => 1); @@ -144,7 +150,7 @@ foreach my $case ( . "systemctl() { echo \"systemctl \$*\" >>\"$root/calls\"; return 0; }\n" . "timedatectl() { echo \"timedatectl \$*\" >>\"$root/calls\"; return 0; }\n" . "hwclock() { echo \"hwclock \$*\" >>\"$root/calls\"; return 0; }\n" - . "check_executes() { for c in \"\$@\"; do [ \"\$c\" = \"$missing\" ] && return 1; done; return 0; }\n" + . "check_executes() { for c in \"\$@\"; do case \" $missing \" in *\" \$c \"*) return 1;; esac; done; return 0; }\n" . "log_label=xcat\nset -- $argv\n"; my $out = `bash -c 'exec 2>/dev/null; $prelude$args$select printf "USE_NTPD=%s\\n" "\${USE_NTPD:-}"' 2>/dev/null`; From 670ea2df515740a515694645997832d564660cf6 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:39:50 -0300 Subject: [PATCH 12/16] fix(setupntp): --backend ntpd configures a daemon that is not installed setupntp falls back from chrony to ntpd when chronyd is absent, but not the other way. A node told --backend ntpd, or --use-ntpd, that has only chrony execs setupntp.traditional, which writes /etc/ntp.conf and calls startservice ntpserver for a daemon that is not there. The node ends with no running time daemon and a warning in the log. The ntpd branch of the backend selection now probes ntpd. When ntpd is absent and chronyd and systemctl are both present, setupntp uses chrony and logs the change, the same way the chrony branch already does. makentp_ntp_deps.t covers both directions. The two ntpd cases fail without this change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT/postscripts/setupntp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/xCAT/postscripts/setupntp b/xCAT/postscripts/setupntp index 425ea665f..29035e8b7 100755 --- a/xCAT/postscripts/setupntp +++ b/xCAT/postscripts/setupntp @@ -139,11 +139,19 @@ then esac fi -# The requested backend is a preference, not a guarantee: a node that does not have chronyd -# still has to be configured, so fall back to ntpd and say so rather than failing. +# The requested backend is a preference, not a guarantee: a node that does not have the +# requested daemon still has to be configured, so use the other one and say so. if [ "${NTP_BACKEND}" = "ntpd" ] then USE_NTPD="yes" + if ! check_executes ntpd >/dev/null 2>&1 && + check_executes chronyd >/dev/null 2>&1 && + check_executes systemctl >/dev/null 2>&1 + then + USE_NTPD="" + logger -t $log_label -p local4.warning \ + "setupntp: ntpd requested but ntpd is absent; using chrony" + fi else check_executes chronyd >/dev/null 2>&1 || USE_NTPD="yes" check_executes systemctl >/dev/null 2>&1 || USE_NTPD="yes" From b3d6e1f9aa7fd711a83c7ed29c654de2b0271810 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:40:07 -0300 Subject: [PATCH 13/16] test(ntp): the selector calls chrony available where makentp will not use it xCAT::NTP::Backend->available reports chrony as available when chronyd exists. makentp configures chrony only when systemctl exists as well, and setupntp hands over to ntpd without it. On a host with chronyd and no systemctl the selector answers chrony, makentp takes the ntpd path anyway, and the admin is told nothing. ntp_backend_selection.t drives available and choose with the command probe injected, so a case can hold chronyd present and systemctl absent without depending on what the test host has installed. Six assertions fail: available reports chrony on chronyd alone, and choose neither downgrades nor asks for an install. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/ntp_backend_selection.t | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/xCAT-test/unit/ntp_backend_selection.t b/xCAT-test/unit/ntp_backend_selection.t index 3f86cf0a2..24df42dc6 100644 --- a/xCAT-test/unit/ntp_backend_selection.t +++ b/xCAT-test/unit/ntp_backend_selection.t @@ -69,4 +69,28 @@ $r = xCAT::NTP::Backend->choose( is( $r->{name}, 'chrony', 'neither present keeps the preferred chrony' ); is( $r->{install}, 1, 'neither present flags install of the preferred daemon' ); +# --- available: chrony needs systemd, not just chronyd ----------------------------------------- +# makentp configures chrony only where systemctl is present, and setupntp hands over to ntpd +# without it. The selector has to call chrony usable on the same terms, or makentp silently takes +# the ntpd path for a backend the selector reported as available. +is( xCAT::NTP::Backend->available( 'chrony', commands => { chronyd => 1, systemctl => 1 } ), 1, + 'chrony is available where chronyd and systemctl are both present' ); +is( xCAT::NTP::Backend->available( 'chrony', commands => { chronyd => 1, systemctl => 0 } ), 0, + 'chronyd without systemctl is not a usable chrony backend' ); +is( xCAT::NTP::Backend->available( 'chrony', commands => { chronyd => 0, systemctl => 1 } ), 0, + 'systemctl without chronyd is not a usable chrony backend either' ); +is( xCAT::NTP::Backend->available( 'ntpd', commands => { ntpd => 1 } ), 1, + 'ntpd needs only ntpd' ); + +$r = xCAT::NTP::Backend->choose( + requested => 'chrony', check_available => 1, + commands => { chronyd => 1, systemctl => 0, ntpd => 1 } ); +is( $r->{name}, 'ntpd', 'chronyd without systemctl downgrades to the daemon that can be used' ); +is( $r->{downgraded}, 'chrony', 'and the downgrade is reported rather than silent' ); + +$r = xCAT::NTP::Backend->choose( + requested => 'chrony', check_available => 1, + commands => { chronyd => 1, systemctl => 0, ntpd => 0 } ); +is( $r->{install}, 1, 'chronyd without systemctl and no ntpd asks for an install' ); + done_testing(); From a6212e838437e35024214b6ddabdcada4b9f8d07 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:40:31 -0300 Subject: [PATCH 14/16] fix(xcat-core): makentp and the NTP selector disagree on when chrony is usable xCAT::NTP::Backend->available reported chrony as available on chronyd alone, while makentp configured chrony only where systemctl was present too. On a host with chronyd and no systemctl the selector returned chrony with no downgrade, makentp fell through to the ntpd path, and the admin saw either a silent switch or "Please make sure ntpd is installed". available now requires chronyd and systemctl for chrony, so the selector answers on the same terms makentp acts on, and makentp branches on the name alone. choose therefore downgrades to ntpd, or reports install, in the case it used to pass over. A commands argument injects the command probe, in the same shape as the existing available argument. ntp_backend_selection.t covers both commands. Six of its assertions fail without this change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- perl-xCAT/xCAT/NTP/Backend.pm | 19 +++++++++++++++++-- xCAT-server/lib/xcat/plugins/makentp.pm | 3 +-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/perl-xCAT/xCAT/NTP/Backend.pm b/perl-xCAT/xCAT/NTP/Backend.pm index aac49081b..89dec10d5 100644 --- a/perl-xCAT/xCAT/NTP/Backend.pm +++ b/perl-xCAT/xCAT/NTP/Backend.pm @@ -32,6 +32,7 @@ sub normalize { # requested -- override (default: site.ntpbackend, else 'auto') # os_name/version -- OS identity (default: detected) # available -- optional { chrony => 0/1, ntpd => 0/1 } to bypass command detection (tests) +# commands -- optional { => 0/1 } to bypass the PATH probe for one command # check_available -- when true, downgrade to whichever daemon is installed, and flag install=1 # when neither is. sub choose { @@ -96,11 +97,25 @@ sub available { return $args{available}{$backend} ? 1 : 0; } - return _command_exists('chronyd') if $backend eq 'chrony'; - return _command_exists('ntpd') if $backend eq 'ntpd'; + # xCAT drives chrony through systemd: makentp configures it only where systemctl is present, + # and setupntp hands over to ntpd without it. chronyd alone is not a usable chrony backend. + if ( $backend eq 'chrony' ) { + return ( _has_command( 'chronyd', %args ) && _has_command( 'systemctl', %args ) ) ? 1 : 0; + } + return _has_command( 'ntpd', %args ) if $backend eq 'ntpd'; return 0; } +sub _has_command { + my ( $command, %args ) = @_; + + if ( ref( $args{commands} ) eq 'HASH' && exists $args{commands}{$command} ) { + return $args{commands}{$command} ? 1 : 0; + } + + return _command_exists($command); +} + sub _site_backend { my $backend = eval { require xCAT::TableUtils; diff --git a/xCAT-server/lib/xcat/plugins/makentp.pm b/xCAT-server/lib/xcat/plugins/makentp.pm index 49a35f85e..189a951a3 100644 --- a/xCAT-server/lib/xcat/plugins/makentp.pm +++ b/xCAT-server/lib/xcat/plugins/makentp.pm @@ -327,10 +327,9 @@ sub process_request { send_msg(\%request, 1, $ntp_action->{error}); return 1; } - my $have_systemctl = (-x "/usr/bin/systemctl" || -x "/bin/systemctl"); # Handle chronyd here, - if ($ntp_action->{name} eq 'chrony' && $have_systemctl) { + if ($ntp_action->{name} eq 'chrony') { send_msg(\%request, 0, "Will configure chronyd instead."); my $cmd = setupntp_command($ntp_action->{name}, $ntp_servers); From 67c1059ad0c76377884a06969159fea893acaf91 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:41:03 -0300 Subject: [PATCH 15/16] test(site): site.ntpbackend is not documented in the site table help makentp reads site.ntpbackend to pick the NTP daemon, so an admin has to set it, but the attribute appears in no help text. lsdef -t site -h and tabdump -d print the site table description from xCAT::Schema, which documents site.dhcpbackend and says nothing about ntpbackend. The makentp man page lists the site attributes the command honors and does not list it either. The selector test now reads the site help from the loaded schema and the makentp pod. Five assertions fail: the attribute name, its three valid values, and the man page entry. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- xCAT-test/unit/ntp_backend_selection.t | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/xCAT-test/unit/ntp_backend_selection.t b/xCAT-test/unit/ntp_backend_selection.t index 24df42dc6..19755f4dd 100644 --- a/xCAT-test/unit/ntp_backend_selection.t +++ b/xCAT-test/unit/ntp_backend_selection.t @@ -93,4 +93,29 @@ $r = xCAT::NTP::Backend->choose( commands => { chronyd => 1, systemctl => 0, ntpd => 0 } ); is( $r->{install}, 1, 'chronyd without systemctl and no ntpd asks for an install' ); +# --- site.ntpbackend is user facing, so the site table help has to carry it -------------------- +# The help text is what lsdef -t site -h and tabdump -d print. site.dhcpbackend is documented +# there; ntpbackend selects the NTP daemon the same way and was not. +require xCAT::Schema; +no warnings 'once'; +my $site_help = $xCAT::Schema::tabspec{site}{descriptions}{key}; +like( $site_help, qr/^\s*ntpbackend:/m, + 'the site table help documents ntpbackend' ); +like( $site_help, qr/ntpbackend:.{0,400}auto/s, + 'and names auto' ); +like( $site_help, qr/ntpbackend:.{0,400}chrony/s, + 'and chrony' ); +like( $site_help, qr/ntpbackend:.{0,400}ntpd/s, + 'and ntpd, the values the selector accepts' ); + +# The makentp man page lists the site attributes the command honors. +my $pod = do { + local $/; + open my $fh, '<', "$FindBin::Bin/../../xCAT-client/pods/man1/makentp.1.pod" + or die "open makentp.1.pod: $!"; + <$fh>; +}; +like( $pod, qr/site\.ntpbackend/, + 'the makentp man page lists site.ntpbackend beside ntpservers and extntpservers' ); + done_testing(); From 3249298cbdd8372f5588045ee3894a56eef75552 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:41:32 -0300 Subject: [PATCH 16/16] fix(xcat-core): site.ntpbackend is not documented anywhere makentp reads site.ntpbackend to select the NTP daemon, and setupntp takes the same value as --backend, but no help text names the attribute. An admin who needs ntpd on a host that has chrony has no way to find out the attribute exists. site.dhcpbackend, which selects the DHCP implementation the same way, is documented in the site table help. The site table description in xCAT::Schema now carries ntpbackend beside ntpservers, with its valid values and the auto default. The makentp man page lists it with the other site attributes the command honors, and names the setupntp --backend option that carries the value to the nodes. ntp_backend_selection.t reads the site help from the loaded schema and the makentp pod. Five assertions fail without this change. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .../source/guides/admin-guides/references/man1/makentp.1.rst | 2 ++ perl-xCAT/xCAT/Schema.pm | 5 +++++ xCAT-client/pods/man1/makentp.1.pod | 2 ++ 3 files changed, 9 insertions(+) diff --git a/docs/source/guides/admin-guides/references/man1/makentp.1.rst b/docs/source/guides/admin-guides/references/man1/makentp.1.rst index 16a259df4..c0a3572d6 100644 --- a/docs/source/guides/admin-guides/references/man1/makentp.1.rst +++ b/docs/source/guides/admin-guides/references/man1/makentp.1.rst @@ -32,6 +32,8 @@ By default, it sets up the NTP server for xCAT management node. If \ **-a**\ fl \ *site.ntpservers*\ -- the NTP servers for the service node and compute node to sync with. The keyword means that the node's NTP server is the node that is managing it (either its service node or the management node). +\ *site.ntpbackend*\ -- the NTP daemon to configure. Valid values are auto, chrony, and ntpd. The default is auto, which selects chrony on distributions that ship it and ntpd on older ones such as EL6 and SLES 12. If the selected daemon is not installed, \ **makentp**\ uses the other one and reports the change. The same value reaches the service nodes and the compute nodes, which the \ *setupntp*\ postscript also accepts as \ **-**\ **-backend chrony|ntpd**\ . + To setup NTP on the compute node, add \ *setupntp*\ postscript to the \ *postscripts*\ table and run \ **updatenode node -P setupntp**\ command. diff --git a/perl-xCAT/xCAT/Schema.pm b/perl-xCAT/xCAT/Schema.pm index 4fcc49c6d..e1317827d 100644 --- a/perl-xCAT/xCAT/Schema.pm +++ b/perl-xCAT/xCAT/Schema.pm @@ -1283,6 +1283,11 @@ passed as argument rather than by table value', " give stable output. You can increase the timeout value by specifying \n" . " '--min-rtt-timeout 1s'. xCAT will append the options defined here to \n" . " the nmap command.\n\n" . +" ntpbackend: The NTP daemon used by makentp and the setupntp postscript. Valid\n" . +" values are auto, chrony, and ntpd. The default is auto. In auto\n" . +" mode, xCAT uses chrony on distributions that ship it and ntpd on\n" . +" older ones such as EL6 and SLES 12. If the selected daemon is not\n" . +" installed, xCAT uses the other one and reports the change.\n\n" . " ntpservers: A comma delimited list of NTP servers for the service node and\n" . " the compute node to sync with. The keyword means that\n" . " the node's NTP server is the node that is managing it\n" . diff --git a/xCAT-client/pods/man1/makentp.1.pod b/xCAT-client/pods/man1/makentp.1.pod index 092d58793..6394c515a 100644 --- a/xCAT-client/pods/man1/makentp.1.pod +++ b/xCAT-client/pods/man1/makentp.1.pod @@ -26,6 +26,8 @@ I -- the NTP servers for the management node to sync with. I I -- the NTP servers for the service node and compute node to sync with. The keyword means that the node's NTP server is the node that is managing it (either its service node or the management node). +I -- the NTP daemon to configure. Valid values are auto, chrony, and ntpd. The default is auto, which selects chrony on distributions that ship it and ntpd on older ones such as EL6 and SLES 12. If the selected daemon is not installed, B uses the other one and reports the change. The same value reaches the service nodes and the compute nodes, which the I postscript also accepts as B<--backend chrony|ntpd>. + =back To setup NTP on the compute node, add I postscript to the I table and run B command.