diff --git a/type/__timezone/explorer/timezone_is b/type/__timezone/explorer/timezone_is
deleted file mode 100755
index 6f72a9248..000000000
--- a/type/__timezone/explorer/timezone_is
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/sh -e
-#
-# 2017 Ander Punnar (ander at kvlt.ee)
-#
-# This file is part of skonfig-base.
-#
-# skonfig-base is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# skonfig-base is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with skonfig-base. If not, see .
-#
-# Prints the contents of /etc/timezone.
-#
-
-[ -f /etc/timezone ] && cat /etc/timezone
-
-exit 0
diff --git a/type/__timezone/explorer/zoneinfo_file b/type/__timezone/explorer/zoneinfo_file
new file mode 100755
index 000000000..e8990774f
--- /dev/null
+++ b/type/__timezone/explorer/zoneinfo_file
@@ -0,0 +1,61 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at riiengineering.ch)
+#
+# This file is part of skonfig-base.
+#
+# skonfig-base is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# skonfig-base is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with skonfig-base. If not, see .
+#
+# Determine and print the path of the --tz zoneinfo file.
+# If the zone is invalid, the explorer produces empty output.
+#
+
+TZ=$(cat "${__object:?}/parameter/tz")
+
+if test -s "${__object:?}/parameter/tzdir"
+then
+ TZDIR=$(cat "${__object:?}/parameter/tzdir")
+
+ test -d "${TZDIR}" || {
+ printf 'Invalid --tzdir: no such directory: %s\n' "${TZDIR}" >&2
+ exit 1
+ }
+else
+ case $(uname -s)
+ in
+ (Darwin)
+ TZDIR=/var/db/timezone/zoneinfo
+ ;;
+ (SunOS)
+ TZDIR=/usr/share/lib/zoneinfo
+ ;;
+ (*)
+ # According to glibc's tzfile(5) there are two typical directories:
+ if test -d /usr/share/zoneinfo
+ then
+ # Linux, FreeBSD, OpenBSD, NetBSD. And others?
+ TZDIR=/usr/share/zoneinfo
+ elif test -d /usr/lib/zoneinfo
+ then
+ # Old libc4, libc5 apparently.
+ TZDIR=/usr/lib/zoneinfo
+ fi
+ ;;
+ esac
+fi
+
+if test -n "${TZDIR-}" && test -e "${TZDIR}/${TZ}"
+then
+ echo "${TZDIR}/${TZ}"
+fi
diff --git a/type/__timezone/gencode-remote b/type/__timezone/gencode-remote
deleted file mode 100755
index a3101122d..000000000
--- a/type/__timezone/gencode-remote
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/sh -e
-#
-# 2012 Steven Armstrong (steven-cdist at armstrong.cc)
-# 2019 Nico Schottelius (nico-cdist at schottelius.org)
-#
-# This file is part of skonfig-base.
-#
-# skonfig-base is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# skonfig-base is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with skonfig-base. If not, see .
-#
-
-timezone_is=$(cat "${__object:?}/explorer/timezone_is")
-timezone_should=$(cat "${__object:?}/parameter/tz")
-os=$(cat "${__global:?}/explorer/os")
-
-if [ "${timezone_is}" = "${timezone_should}" ]
-then
- exit 0
-fi
-
-case ${os}
-in
- (ubuntu|debian|devuan|coreos|alpine)
- echo "echo \"${timezone_should}\" >/etc/timezone"
- ;;
-esac
diff --git a/type/__timezone/man.rst b/type/__timezone/man.rst
index 801671d1a..b4106ad37 100644
--- a/type/__timezone/man.rst
+++ b/type/__timezone/man.rst
@@ -3,13 +3,18 @@ cdist-type__timezone(7)
NAME
----
-cdist-type__timezone - Allows one to configure the desired localtime timezone.
+cdist-type__timezone - Configure the system timezone.
DESCRIPTION
-----------
-This type creates a symlink (/etc/localtime) to the selected timezone
-(which should be available in /usr/share/zoneinfo).
+This type creates a symlink (``/etc/localtime``) to the selected
+timezone (which should be available in ``--tzdir`` which is usually
+``/usr/share/zoneinfo``).
+
+On some operating systems, other system-specific files are modified as well.
+
+If necessary, the zoneinfo database (``tzdata``) will be installed on the target.
REQUIRED PARAMETERS
@@ -17,6 +22,16 @@ REQUIRED PARAMETERS
tz
The name of the timezone to set.
+ It should map to a file present in ``--tzdir``.
+
+
+OPTIONAL PARAMETERS
+-------------------
+tzdir
+ The directory containing the timezone data files on the target.
+
+ Default: detected based on OS, usually ``/usr/share/zoneinfo``.
+
EXAMPLES
--------
@@ -29,6 +44,12 @@ EXAMPLES
# Set up US/Central as our timezone.
__timezone --tz US/Central
+ # Some operating systems (e.g. Debian, RedHat, SuSE) have a separate
+ # directory for POSIX timezones (time values interpreted as seconds since
+ # the epoch, not counting leap seconds).
+ # It can be used by manually specifying the TZDIR.
+ __timezone --tz Europe/Vaduz --tzdir /usr/share/zoneinfo/posix
+
AUTHORS
-------
@@ -40,7 +61,7 @@ AUTHORS
COPYING
-------
-Copyright \(C) 2012-2020 the `AUTHORS`_.
+Copyright \(C) 2012-2025 the `AUTHORS`_.
You can redistribute it and/or modify it under the terms of the GNU General
Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
diff --git a/type/__timezone/manifest b/type/__timezone/manifest
index 05bd90375..7322cfd3e 100755
--- a/type/__timezone/manifest
+++ b/type/__timezone/manifest
@@ -3,6 +3,7 @@
# 2011 Ramon Salvadó (rsalvado at gnuine.com)
# 2012-2015 Steven Armstrong (steven-cdist at armstrong.cc)
# 2012-2019 Nico Schottelius (nico-cdist at schottelius.org)
+# 2020,2025 Dennis Camera (dennis.camera at riiengineering.ch)
#
# This file is part of skonfig-base.
#
@@ -20,47 +21,173 @@
# along with skonfig-base. If not, see .
#
-timezone=$(cat "${__object:?}/parameter/tz")
+TZ=$(cat "${__object:?}/parameter/tz")
os=$(cat "${__global:?}/explorer/os")
+localtime_file=/etc/localtime
+zoneinfo_file=$(cat "${__object:?}/explorer/zoneinfo_file")
+canonicalize_path() {
+ # NOTE: Only works for absolute paths
+ # shellcheck disable=SC2016
+ printf '%s\n' "${1:?}" \
+ | tr '/' '\n' \
+ | sed -n -e '
+ # ignore empty lines (produced by // in original path and leading /)
+ /^$/bq
+ # ignore /./ components
+ /^\.$/bq
+ # drop a component from the "stack" if a .. is met
+ /^\.\.$/{
+ # delete last line from hold space
+ x
+ s/^\(.*\)\n.*$/\1/
+ x
+ bq
+ }
+ # push component to "stack" (append to hold space)
+ H
+ :q
+ # print stack contents (hold space) at the end
+ ${x;p;}' \
+ | sed -n -e 'H;${g;s/^\n//;s/\n/\//g;p;}'
+}
+
+count_path_components() (
+ components=$(canonicalize_path "$1")
+ IFS=/
+ # shellcheck disable=SC2048,SC2086
+ set -f -- ${components#/}
+ echo $#
+)
+
+backdirs() {
+ test "${1:-0}" -gt 0 || { printf './\n'; return 0; }
+ until test "${1:-0}" -le 0
+ do
+ printf '../'
+ set -- $(($1 - 1))
+ done
+ printf '\n'
+}
+
+invalid_timezone() {
+ # NOTE: When the tzdata package was just installed, this might produce an
+ # invalid timezone error because the explorer has been run before the
+ # installation.
+ printf 'Invalid timezone: %s\n' "${TZ}" >&2
+ exit 1
+}
+
+# some OSes require a timezone package to be installed first
+case ${os}
+in
+ (adelie|alpine|archlinux|debian|devuan|ubuntu|centos|redhat|scientific|voidlinux)
+ __package tzdata
+ export require=__package/tzdata
+ ;;
+ (gentoo)
+ __package sys-libs/timezone-data
+ export require=__package/sys-libs/timezone-data
+ ;;
+ (suse)
+ __package timezone
+ export require=__package/timezone
+ ;;
+ (slackware)
+ __package glibc-zoneinfo
+ export require=__package/glibc-zoneinfo
+ ;;
+esac
+
+# configure the timezone
case ${os}
in
- (archlinux|debian|ubuntu|devuan|alpine)
- __package tzdata
- export require='__package/tzdata'
- ;;
- (suse)
- __package timezone
- export require='__package/timezone'
- ;;
- (freebsd|netbsd|openbsd)
- # whitelist
- :
- ;;
- (coreos)
- # whitelist
- :
- ;;
- (scientific|centos)
- __package tzdata --state present
- export require='__package/tzdata'
- __file /etc/sysconfig/clock \
- --owner root --group root --mode 644 \
- --state exists
- require='__file/etc/sysconfig/clock' \
- __key_value ZONE \
- --file /etc/sysconfig/clock \
- --delimiter '=' \
- --value "\"${timezone}\""
- ;;
- (*)
- : "${__type:?}" # make shellcheck happy
- echo "Your operating system (${os}) is currently not supported by this type (${__type##*/})." >&2
- echo 'Please contribute an implementation for it if you can.' >&2
- exit 1
- ;;
+ (adelie|alpine|slackware)
+ test -n "${zoneinfo_file}" || invalid_timezone
+ ;;
+ (coreos|debian|devuan|ubuntu)
+ test -n "${zoneinfo_file}" || invalid_timezone
+
+ __file /etc/timezone \
+ --owner 0 --group 0 --mode 0644 \
+ --source - <<-EOF
+ ${TZ}
+ EOF
+ ;;
+ (centos|redhat|scientific)
+ test -n "${zoneinfo_file}" || invalid_timezone
+
+ __file /etc/sysconfig/clock \
+ --state exists \
+ --owner 0 --group 0 --mode 0644
+ require=__file/etc/sysconfig/clock \
+ __key_value /etc/sysconfig/clock:ZONE \
+ --file /etc/sysconfig/clock \
+ --delimiter '=' --exact_delimiter \
+ --key 'ZONE' \
+ --value "\"${TZ}\""
+ ;;
+ (gentoo)
+ test -n "${zoneinfo_file}" || invalid_timezone
+
+ # XXX: systemd?
+
+ __file /etc/timezone \
+ --owner 0 --group 0 --mode 0644 \
+ --source - <<-EOF
+ ${TZ}
+ EOF
+
+ # XXX: Should /etc/TZ be updated??
+ __file /etc/TZ \
+ --owner 0 --group 0 --mode 0644 \
+ --source - <<-EOF
+ ${TZ}
+ EOF
+ ;;
+ (freebsd)
+ test -n "${zoneinfo_file}" || invalid_timezone
+
+ __file /var/db/zoneinfo \
+ --owner 0 --group 0 --mode 0644 \
+ --source - <<-EOF
+ ${TZ}
+ EOF
+ ;;
+ (suse)
+ os_version=$(cat "${__global:?}/explorer/os_version")
+ os_major=${os_version%%[!0-9]*}
+
+ # TODO: Consider using `yast2 timezone ...` instead
+ if test $((os_major)) -lt 15 || test $((os_major)) -eq 42
+ then
+ # It seems that starting with SuSE 15 /etc/sysconfig/clock is
+ # abandoned. The file still exists but only contains a
+ # DEFAULT_TIMEZONE entry.
+ __key_value /etc/sysconfig/clock:TIMEZONE \
+ --file /etc/sysconfig/clock \
+ --delimiter '=' --exact_delimiter \
+ --key TIMEZONE --value "\"${TZ}\""
+ fi
+ ;;
+ (voidlinux)
+ test -n "${zoneinfo_file}" || invalid_timezone
+
+ __key_value /etc/rc.conf:TIMEZONE \
+ --file /etc/rc.conf \
+ --key TIMEZONE \
+ --delimiter '=' --exact_delimiter \
+ --value "\"${TZ}\""
+ ;;
+ (*)
+ : "${__type:?}" # make shellcheck happy
+ echo "Your operating system (${os}) is currently not supported by this type (${__type##*/})." >&2
+ echo 'Please contribute an implementation for it if you can.' >&2
+ exit 1
+ ;;
esac
-__link /etc/localtime \
- --source "/usr/share/zoneinfo/${timezone}" \
- --type symbolic
+rel_zoneinfo_file="$(backdirs "$(count_path_components "${localtime_file%/*}")")${zoneinfo_file#/}"
+__link "${localtime_file}" \
+ --type symbolic \
+ --source "${rel_zoneinfo_file}"
diff --git a/type/__timezone/parameter/optional b/type/__timezone/parameter/optional
new file mode 100644
index 000000000..d0719869f
--- /dev/null
+++ b/type/__timezone/parameter/optional
@@ -0,0 +1 @@
+tzdir