Some configuration files that need updates are directly under in /etc. To update them atomically, we need write access to /etc. For Ubuntu Core this is an issue as /etc is not writable. Only a selection of subdirectories can be writable. The general solution is symlinks or bind mounts to writable places. But for atomic writes in /etc, that does not work. So Ubuntu has had a patch for that that did not age well. Instead we would like to introduce some environment variables for alternate paths. * SYSTEMD_ETC_HOSTNAME: /etc/hostname * SYSTEMD_ETC_MACHINE_INFO: /etc/machine-info * SYSTEMD_ETC_LOCALTIME: /etc/localtime * SYSTEMD_ETC_LOCALE_CONF: /etc/locale.conf * SYSTEMD_ETC_VCONSOLE_CONF: /etc/vconsole.conf * SYSTEMD_ETC_ADJTIME: /etc/adjtime While it is for now expected that there is a symlink from the standard, we still try to read them from that alternate path. This is important for `/etc/localtime`, which is a symlink, so we cannot have an indirect symlink or bind mount for it. Since machine-id is typically written only once and not updated. This commit does not cover it. An initrd can properly create it and bind mount it.
453 lines
13 KiB
Bash
Executable File
453 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
set -eux
|
|
set -o pipefail
|
|
|
|
# shellcheck source=test/units/test-control.sh
|
|
. "$(dirname "$0")"/test-control.sh
|
|
# shellcheck source=test/units/util.sh
|
|
. "$(dirname "$0")"/util.sh
|
|
|
|
testcase_timedatectl() {
|
|
timedatectl --no-pager --help
|
|
timedatectl --version
|
|
|
|
timedatectl
|
|
timedatectl --no-ask-password
|
|
timedatectl status --machine=testuser@.host
|
|
timedatectl status
|
|
timedatectl show
|
|
timedatectl show --all
|
|
timedatectl show -p NTP
|
|
timedatectl show -p NTP --value
|
|
timedatectl list-timezones
|
|
|
|
if ! systemd-detect-virt -qc; then
|
|
systemctl enable --runtime --now systemd-timesyncd
|
|
timedatectl timesync-status
|
|
timedatectl show-timesync
|
|
fi
|
|
}
|
|
|
|
restore_timezone() {
|
|
if [[ -f /tmp/timezone.bak ]]; then
|
|
mv /tmp/timezone.bak /etc/timezone
|
|
else
|
|
rm -f /etc/timezone
|
|
fi
|
|
}
|
|
|
|
testcase_timezone() {
|
|
local ORIG_TZ=
|
|
|
|
# Debian/Ubuntu specific file
|
|
if [[ -f /etc/timezone ]]; then
|
|
mv /etc/timezone /tmp/timezone.bak
|
|
fi
|
|
|
|
trap restore_timezone RETURN
|
|
|
|
if [[ -L /etc/localtime ]]; then
|
|
ORIG_TZ=$(readlink /etc/localtime | sed 's#^.*zoneinfo/##')
|
|
echo "original tz: $ORIG_TZ"
|
|
fi
|
|
|
|
echo 'timedatectl works'
|
|
assert_in "Local time:" "$(timedatectl --no-pager)"
|
|
|
|
echo 'change timezone'
|
|
assert_eq "$(timedatectl --no-pager set-timezone Europe/Kyiv 2>&1)" ""
|
|
assert_eq "$(readlink /etc/localtime | sed 's#^.*zoneinfo/##')" "Europe/Kyiv"
|
|
if [[ -f /etc/timezone ]]; then
|
|
assert_eq "$(cat /etc/timezone)" "Europe/Kyiv"
|
|
fi
|
|
assert_in "Time zone: Europe/Kyiv \(EES*T, \+0[0-9]00\)" "$(timedatectl)"
|
|
|
|
if [[ -n "$ORIG_TZ" ]]; then
|
|
echo 'reset timezone to original'
|
|
assert_eq "$(timedatectl set-timezone "$ORIG_TZ" 2>&1)" ""
|
|
assert_eq "$(readlink /etc/localtime | sed 's#^.*zoneinfo/##')" "$ORIG_TZ"
|
|
if [[ -f /etc/timezone ]]; then
|
|
assert_eq "$(cat /etc/timezone)" "$ORIG_TZ"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
restore_adjtime() {
|
|
if [[ -e /etc/adjtime.bak ]]; then
|
|
mv /etc/adjtime.bak /etc/adjtime
|
|
else
|
|
rm /etc/adjtime
|
|
fi
|
|
}
|
|
|
|
check_adjtime_not_exist() {
|
|
if [[ -e /etc/adjtime ]]; then
|
|
echo "/etc/adjtime unexpectedly exists." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
testcase_adjtime() {
|
|
# test setting UTC vs. LOCAL in /etc/adjtime
|
|
if [[ -e /etc/adjtime ]]; then
|
|
mv /etc/adjtime /etc/adjtime.bak
|
|
fi
|
|
|
|
trap restore_adjtime RETURN
|
|
|
|
echo 'no adjtime file'
|
|
rm -f /etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
|
|
echo 'UTC set in adjtime file'
|
|
printf '0.0 0 0\n0\nUTC\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
UTC"
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'non-zero values in adjtime file'
|
|
printf '0.1 123 0\n0\nLOCAL\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
assert_eq "$(cat /etc/adjtime)" "0.1 123 0
|
|
0
|
|
UTC"
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.1 123 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'fourth line adjtime file'
|
|
printf '0.0 0 0\n0\nLOCAL\nsomethingelse\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
UTC
|
|
somethingelse"
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL
|
|
somethingelse"
|
|
|
|
echo 'no final newline in adjtime file'
|
|
printf '0.0 0 0\n0\nUTC' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
printf '0.0 0 0\n0\nUTC' >/etc/adjtime
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'only one line in adjtime file'
|
|
printf '0.0 0 0\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
printf '0.0 0 0\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'only one line in adjtime file, no final newline'
|
|
printf '0.0 0 0' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
printf '0.0 0 0' >/etc/adjtime
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'only two lines in adjtime file'
|
|
printf '0.0 0 0\n0\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
printf '0.0 0 0\n0\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'only two lines in adjtime file, no final newline'
|
|
printf '0.0 0 0\n0' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
printf '0.0 0 0\n0' >/etc/adjtime
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
|
|
echo 'unknown value in 3rd line of adjtime file'
|
|
printf '0.0 0 0\n0\nFOO\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 0
|
|
check_adjtime_not_exist
|
|
printf '0.0 0 0\n0\nFOO\n' >/etc/adjtime
|
|
timedatectl set-local-rtc 1
|
|
assert_eq "$(cat /etc/adjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
}
|
|
|
|
assert_ntp() {
|
|
local value="${1:?}"
|
|
|
|
for _ in {0..9}; do
|
|
[[ "$(busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 org.freedesktop.timedate1 NTP)" == "b $value" ]] && return 0
|
|
sleep .5
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
assert_timedated_signal() {
|
|
local timestamp="${1:?}"
|
|
local value="${2:?}"
|
|
local args=(-q -n 1 --since="$timestamp" -p info -t busctl)
|
|
|
|
journalctl --sync
|
|
|
|
for _ in {0..9}; do
|
|
if journalctl "${args[@]}" --grep .; then
|
|
[[ "$(journalctl "${args[@]}" -o cat | jq -r '.payload.data[1].NTP.data')" == "$value" ]];
|
|
return 0
|
|
fi
|
|
|
|
sleep .5
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
assert_timesyncd_state() {
|
|
local state="${1:?}"
|
|
|
|
for _ in {0..9}; do
|
|
[[ "$(systemctl show systemd-timesyncd.service -P ActiveState)" == "$state" ]] && return 0
|
|
sleep .5
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
testcase_ntp() {
|
|
# timesyncd has ConditionVirtualization=!container by default; drop/mock that for testing
|
|
if systemd-detect-virt --container --quiet; then
|
|
systemctl disable --quiet --now systemd-timesyncd
|
|
mkdir -p /run/systemd/system/systemd-timesyncd.service.d
|
|
cat >/run/systemd/system/systemd-timesyncd.service.d/container.conf <<EOF
|
|
[Unit]
|
|
ConditionVirtualization=
|
|
|
|
[Service]
|
|
Type=simple
|
|
AmbientCapabilities=
|
|
ExecStart=
|
|
ExecStart=sleep infinity
|
|
EOF
|
|
systemctl daemon-reload
|
|
fi
|
|
|
|
systemd-run --unit busctl-monitor.service --service-type=notify \
|
|
busctl monitor --json=short --match="type=signal,sender=org.freedesktop.timedate1,member=PropertiesChanged,path=/org/freedesktop/timedate1"
|
|
|
|
: 'Disable NTP'
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl set-ntp false
|
|
assert_timedated_signal "$ts" "false"
|
|
assert_timesyncd_state "inactive"
|
|
assert_ntp "false"
|
|
assert_rc 3 systemctl is-active --quiet systemd-timesyncd
|
|
|
|
: 'Enable NTP'
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl set-ntp true
|
|
assert_timedated_signal "$ts" "true"
|
|
assert_ntp "true"
|
|
assert_timesyncd_state "active"
|
|
assert_rc 0 systemctl is-active --quiet systemd-timesyncd
|
|
|
|
: 'Re-disable NTP'
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl set-ntp false
|
|
assert_timedated_signal "$ts" "false"
|
|
assert_ntp "false"
|
|
assert_rc 3 systemctl is-active --quiet systemd-timesyncd
|
|
|
|
systemctl stop busctl-monitor.service
|
|
rm -rf /run/systemd/system/systemd-timesyncd.service.d/
|
|
systemctl daemon-reload
|
|
}
|
|
|
|
assert_timesyncd_signal() {
|
|
local timestamp="${1:?}"
|
|
local property="${2:?}"
|
|
local value="${3:?}"
|
|
local args=(-q --since="$timestamp" -p info -t busctl)
|
|
|
|
journalctl --sync
|
|
|
|
for _ in {0..9}; do
|
|
if journalctl "${args[@]}" --grep .; then
|
|
[[ "$(journalctl "${args[@]}" -o cat | jq -r ".payload.data[1].$property.data | join(\" \")")" == "$value" ]];
|
|
return 0
|
|
fi
|
|
|
|
sleep .5
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
assert_networkd_ntp() {
|
|
local interface="${1:?}"
|
|
local value="${2:?}"
|
|
# Go through the array of NTP servers and for each entry do:
|
|
# - if the entry is an IPv4 address, join the Address array into a dot separated string
|
|
# - if the entry is a server address, select it unchanged
|
|
# These steps produce an array of strings, that is then joined into a space-separated string
|
|
# Note: this doesn't support IPv6 addresses, since converting them to a string is a bit more
|
|
# involved than a simple join(), but let's leave that to another time
|
|
local expr='[.NTP[] | (select(.Family == 2).Address | join(".")), select(has("Server")).Server] | join(" ")'
|
|
|
|
[[ "$(networkctl status "$interface" --json=short | jq -r "$expr")" == "$value" ]]
|
|
}
|
|
|
|
testcase_timesyncd() {
|
|
if systemd-detect-virt -cq; then
|
|
echo "This test case requires a VM, skipping..."
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v networkctl >/dev/null; then
|
|
echo "This test requires systemd-networkd, skipping..."
|
|
return 0
|
|
fi
|
|
|
|
# Create a dummy interface managed by networkd, so we can configure link NTP servers
|
|
mkdir -p /run/systemd/network/
|
|
cat >/etc/systemd/network/10-ntp99.netdev <<EOF
|
|
[NetDev]
|
|
Name=ntp99
|
|
Kind=dummy
|
|
EOF
|
|
cat >/etc/systemd/network/10-ntp99.network <<EOF
|
|
[Match]
|
|
Name=ntp99
|
|
|
|
[Network]
|
|
Address=10.0.0.1/24
|
|
EOF
|
|
|
|
systemctl unmask systemd-timesyncd systemd-networkd
|
|
systemctl restart systemd-timesyncd
|
|
systemctl restart systemd-networkd
|
|
networkctl status ntp99
|
|
|
|
systemd-run --unit busctl-monitor.service --service-type=notify \
|
|
busctl monitor --json=short --match="type=signal,sender=org.freedesktop.timesync1,member=PropertiesChanged,path=/org/freedesktop/timesync1"
|
|
|
|
# LinkNTPServers
|
|
#
|
|
# Single IP
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl ntp-servers ntp99 10.0.0.1
|
|
assert_networkd_ntp ntp99 10.0.0.1
|
|
assert_timesyncd_signal "$ts" LinkNTPServers 10.0.0.1
|
|
# Setting NTP servers to the same value shouldn't emit a PropertiesChanged signal
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl ntp-servers ntp99 10.0.0.1
|
|
assert_networkd_ntp ntp99 10.0.0.1
|
|
(! assert_timesyncd_signal "$ts" LinkNTPServers 10.0.0.1)
|
|
# Multiple IPs
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl ntp-servers ntp99 10.0.0.1 192.168.0.99
|
|
assert_networkd_ntp ntp99 "10.0.0.1 192.168.0.99"
|
|
assert_timesyncd_signal "$ts" LinkNTPServers "10.0.0.1 192.168.0.99"
|
|
# Multiple IPs + servers
|
|
ts="$(date +"%F %T.%6N")"
|
|
timedatectl ntp-servers ntp99 10.0.0.1 192.168.0.99 foo.localhost foo 10.11.12.13
|
|
assert_networkd_ntp ntp99 "10.0.0.1 192.168.0.99 foo.localhost foo 10.11.12.13"
|
|
assert_timesyncd_signal "$ts" LinkNTPServers "10.0.0.1 192.168.0.99 foo.localhost foo 10.11.12.13"
|
|
|
|
# RuntimeNTPServers
|
|
#
|
|
# There's no user-facing API that allows changing this property (afaik), so let's
|
|
# call SetRuntimeNTPServers() directly to test things out. The inner workings should
|
|
# be exactly the same as in the previous case, so do just one test to make sure
|
|
# things work
|
|
ts="$(date +"%F %T.%6N")"
|
|
busctl call org.freedesktop.timesync1 /org/freedesktop/timesync1 org.freedesktop.timesync1.Manager \
|
|
SetRuntimeNTPServers as 4 "10.0.0.1" foo "192.168.99.1" bar
|
|
servers="$(busctl get-property org.freedesktop.timesync1 /org/freedesktop/timesync1 org.freedesktop.timesync1.Manager RuntimeNTPServers)"
|
|
[[ "$servers" == 'as 4 "10.0.0.1" "foo" "192.168.99.1" "bar"' ]]
|
|
assert_timesyncd_signal "$ts" RuntimeNTPServers "10.0.0.1 foo 192.168.99.1 bar"
|
|
|
|
# Cleanup
|
|
systemctl stop systemd-networkd systemd-timesyncd
|
|
rm -f /run/systemd/network/ntp99.*
|
|
}
|
|
|
|
teardown_timedated_alternate_paths() {
|
|
set +eu
|
|
|
|
rm -rf /run/systemd/system/systemd-timedated.service.d
|
|
systemctl daemon-reload
|
|
systemctl restart systemd-timedated
|
|
}
|
|
|
|
testcase_timedated_alternate_paths() {
|
|
trap teardown_timedated_alternate_paths RETURN
|
|
|
|
mkdir -p /run/alternate-path
|
|
mkdir -p /run/systemd/system/systemd-timedated.service.d
|
|
cat >/run/systemd/system/systemd-timedated.service.d/override.conf <<EOF
|
|
[Service]
|
|
Environment=SYSTEMD_ETC_LOCALTIME=/run/alternate-path/mylocaltime
|
|
Environment=SYSTEMD_ETC_ADJTIME=/run/alternate-path/myadjtime
|
|
EOF
|
|
systemctl daemon-reload
|
|
systemctl restart systemd-timedated
|
|
|
|
assert_in "Local time:" "$(timedatectl --no-pager)"
|
|
|
|
assert_eq "$(timedatectl --no-pager set-timezone Europe/Kyiv 2>&1)" ""
|
|
assert_eq "$(readlink /run/alternate-path/mylocaltime | sed 's#^.*zoneinfo/##')" "Europe/Kyiv"
|
|
assert_in "Time zone: Europe/Kyiv \(EES*T, \+0[0-9]00\)" "$(timedatectl)"
|
|
|
|
# Restart to force using get_timezine
|
|
systemctl restart systemd-timedated
|
|
assert_in "Time zone: Europe/Kyiv \(EES*T, \+0[0-9]00\)" "$(timedatectl)"
|
|
|
|
assert_in "RTC in local TZ: no" "$(timedatectl --no-pager)"
|
|
assert_rc 0 timedatectl set-local-rtc 1
|
|
assert_in "RTC in local TZ: yes" "$(timedatectl --no-pager)"
|
|
assert_eq "$(cat /run/alternate-path/myadjtime)" "0.0 0 0
|
|
0
|
|
LOCAL"
|
|
assert_rc 0 timedatectl set-local-rtc 0
|
|
if [[ -e /run/alternate-path/myadjtime ]]; then
|
|
echo "/run/alternate-path/myadjtime still exists" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
run_testcases
|
|
|
|
touch /testok
|