2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-21 16:39:30 +00:00

Merge pull request #7840 from VersatusHPC/fix/gettimezone-error-sentence-as-timezone

fix(xcat-core): gettimezone returns an error sentence as a timezone name
This commit is contained in:
Daniel Hilst
2026-09-16 20:07:40 -03:00
committed by GitHub
2 changed files with 166 additions and 22 deletions
+66 -22
View File
@@ -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;
}
#--------------------------------------------------------------------------------
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env perl
# gettimezone names the timezone that goes into a kickstart or an autoyast profile, where the
# value must be one token. It found the name by comparing /etc/localtime against every file under
# /usr/share/zoneinfo, and when that pipeline failed it returned the sentence "Could not determine
# timezone checksum" as if it were a name.
#
# A cloud image that runs on UTC ships no /etc/localtime, so the scan finds nothing there, and
# anaconda refuses a timezone command that carries more than one argument.
#
# Utils.pm cannot be loaded here, so the routine is extracted and driven against a scratch root
# with the two collaborators it calls replaced.
use strict;
use warnings;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use Test::More;
my $source = "$FindBin::Bin/../../perl-xCAT/xCAT/Utils.pm";
open(my $source_fh, '<', $source) or die "open $source: $!";
my $content = do { local $/; <$source_fh> };
close($source_fh) or die "close $source: $!";
my @routines;
for my $name (qw(gettimezone _zone_from_path)) {
my ($routine) = $content =~ /^(sub \Q$name\E\s*\n?\{.*?^\})/ms;
die("could not extract $name from Utils.pm") unless $routine;
push(@routines, $routine);
}
# The routines call each other unqualified and the caller reaches them through the class, so
# they go back into the package they came from.
eval "package xCAT::Utils;\n" . join("\n", @routines); ## no critic (BuiltinFunctions::ProhibitStringyEval)
die("could not load the timezone routines: $@") if $@;
# The collaborators gettimezone calls. The scan runs `find`, which must never look at the host
# this test runs on, so it answers from the scratch root instead.
our $SCAN_OUT = '';
our $SCAN_RC = 1;
{
no warnings 'once';
*xCAT::Utils::isAIX = sub { return 0 };
*xCAT::Utils::runcmd = sub { $::RUNCMD_RC = $SCAN_RC; return $SCAN_OUT };
}
#-----------------------------------------------------------------------------------------------
=head3 scratch_root
Descriptions:
A root with a zoneinfo tree, and /etc/localtime as a symlink into it when a zone is named.
Arguments:
$zone - the zone to link /etc/localtime to, or undef for a root with no /etc/localtime
Returns:
The root directory.
=cut
#-----------------------------------------------------------------------------------------------
sub scratch_root {
my ($zone) = @_;
my $root = tempdir(CLEANUP => 1);
make_path("$root/etc", "$root/usr/share/zoneinfo/America");
for my $name ('UTC', 'America/Sao_Paulo') {
open(my $fh, '>', "$root/usr/share/zoneinfo/$name") or die "create $name: $!";
print {$fh} "TZif";
close($fh) or die "close $name: $!";
}
symlink("../usr/share/zoneinfo/$zone", "$root/etc/localtime") or die "symlink: $!"
if defined $zone;
return $root;
}
is(xCAT::Utils->gettimezone(root => scratch_root('America/Sao_Paulo')), 'America/Sao_Paulo',
'the /etc/localtime symlink names the zone');
# No /etc/localtime, so the scan reports nothing.
my $none = xCAT::Utils->gettimezone(root => scratch_root(undef));
is($none, 'UTC', 'a root with no /etc/localtime falls back to UTC');
unlike($none, qr/\s/,
'the value is one token, which is all the kickstart timezone command accepts');
# /etc/timezone is consulted before the fallback.
my $root = scratch_root(undef);
open(my $tz_fh, '>', "$root/etc/timezone") or die "create /etc/timezone: $!";
print {$tz_fh} "America/Sao_Paulo\n";
close($tz_fh) or die "close /etc/timezone: $!";
is(xCAT::Utils->gettimezone(root => $root), 'America/Sao_Paulo',
'/etc/timezone names the zone when there is no symlink');
# The scan still answers for a root whose /etc/localtime is a copy rather than a symlink.
$root = scratch_root(undef);
open(my $copy_fh, '>', "$root/etc/localtime") or die "create /etc/localtime: $!";
print {$copy_fh} "TZif";
close($copy_fh) or die "close /etc/localtime: $!";
{
local $SCAN_OUT = "$root/usr/share/zoneinfo/America/Sao_Paulo\n";
local $SCAN_RC = 0;
is(xCAT::Utils->gettimezone(root => $root), 'America/Sao_Paulo',
'the zoneinfo scan names the zone when /etc/localtime is a copy');
}
done_testing();