From 2baa56534c6c5dee337ce9486ba3f413ad74ee2d Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:10:19 -0300 Subject: [PATCH] fix(xcat-core): gettimezone returns an error sentence as a timezone name The EL 10 riscv64 compute node installs no package. Its kickstart carries "timezone Could not determine timezone checksum --utc" at line 21, anaconda answers "One or zero arguments are expected for the timezone command", and the node holds status=installing until retry_install.sh reinstalls over it (build 114). gettimezone in Utils.pm compares /etc/localtime against every file under /usr/share/zoneinfo and, when that pipeline exits non-zero, returns the string "Could not determine timezone checksum" to its caller as a name. The Rocky 10.2 riscv64 cloud image ships no /etc/localtime and runs on UTC, so the pipeline prints nothing and exits 1. gettimezone now reads the /etc/localtime symlink first, which is what every current distribution uses and what the scan cannot answer for a host with no /etc/localtime, then the scan, then /etc/timezone, and returns UTC when none of them names a zone. It never returns a value with a space in it, which is all the kickstart timezone command accepts. The paths take an optional root prefix so the test drives them against a scratch tree. xCAT-test/unit/utils_gettimezone.t stops without _zone_from_path, and each of the three sources it asserts fails a separate assertion when it is removed. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- perl-xCAT/xCAT/Utils.pm | 88 ++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/perl-xCAT/xCAT/Utils.pm b/perl-xCAT/xCAT/Utils.pm index 7a4405330..62530eb54 100644 --- a/perl-xCAT/xCAT/Utils.pm +++ b/perl-xCAT/xCAT/Utils.pm @@ -3909,36 +3909,80 @@ sub fullpathbin #-------------------------------------------------------------------------------- sub gettimezone { - my ($class) = @_; + my ($class, %opt) = @_; - my $tz; if (xCAT::Utils->isAIX()) { - $tz = $ENV{'TZ'}; - } else { # all linux - my $localtime = "/etc/localtime"; - my $zoneinfo = "/usr/share/zoneinfo"; + return $ENV{'TZ'}; + } + + # all linux. %opt{root} prefixes every path, so a test drives this against a scratch tree. + my $root = defined($opt{root}) ? $opt{root} : ''; + my $localtime = "$root/etc/localtime"; + my $zoneinfo = "$root/usr/share/zoneinfo"; + + # On every current distribution /etc/localtime is a symlink into the zoneinfo tree and its + # target is the name. Read it before the scan: the scan compares /etc/localtime against every + # file in the tree, and a cloud image running on UTC may ship no /etc/localtime at all. + if (-l $localtime) { + my $zone = _zone_from_path(readlink($localtime)); + return $zone if defined $zone; + } + + if (-e $localtime) { my $cmd = "find $zoneinfo -xtype f -exec cmp -s $localtime {} \\; -print | grep -v posix | grep -v SystemV | grep -v right | grep -v localtime "; my $zone_result = xCAT::Utils->runcmd("$cmd", 0); - if ($::RUNCMD_RC != 0) - { - $tz = "Could not determine timezone checksum"; - return $tz; + if ($::RUNCMD_RC == 0) { + my @zones = split /\n/, $zone_result; + my $zone = _zone_from_path($zones[0]); + return $zone if defined $zone; } - my @zones = split /\n/, $zone_result; - - $zones[0] =~ s/$zoneinfo\///; - if (!$zones[0]) { # if we still did not get one, then default - $tz = `cat /etc/timezone`; - chomp $tz; - } else { - $tz = $zones[0]; - } - - } - return $tz; + if (open(my $tz_fh, '<', "$root/etc/timezone")) { + my $zone = <$tz_fh>; + close($tz_fh); + if (defined $zone) { + chomp $zone; + return $zone if length($zone) and $zone !~ /\s/; + } + } + # The caller writes this value into a kickstart or an autoyast profile, where it must be one + # token. Name the zone the host is actually on rather than a sentence that stops the installer. + return 'UTC'; +} + +#-------------------------------------------------------------------------------- + +=head3 _zone_from_path + Returns the timezone name inside a path under a zoneinfo tree, for a symlink target or a + line of the zoneinfo scan. Both absolute and relative targets carry "/zoneinfo/", so the + name is whatever follows it. + Arguments: + A path, or undef + Returns: + The timezone name, or undef when the path names no zone + Globals: + none + Error: + None + Example: + my $zone = _zone_from_path("../usr/share/zoneinfo/America/Sao_Paulo"); + Comments: + none +=cut + +#-------------------------------------------------------------------------------- +sub _zone_from_path +{ + my ($path) = @_; + + return undef unless defined($path) and length($path); + return undef unless $path =~ m{(?:^|/)zoneinfo/(.+)\z}; + my $zone = $1; + $zone =~ s{^posix/}{}; + return undef if $zone eq '' or $zone eq 'localtime' or $zone =~ /\s/; + return $zone; } #--------------------------------------------------------------------------------