Skip to content

Commit 6507fa3

Browse files
authored
[hostcfg] Fix timezone mismatch after image upgrade (#312)
Why I did it To fix an issue where newly installed SONiC images default /etc/localtime to UTC even when the previous image was configured with and used a different timezone, and config save was executed before the image update. This causes a mismatch between the timezone defined in CONFIG_DB and the one defined in the Linux file /etc/localtime, because when the new image comes up with a new filesystem, the Linux file /etc/localtime is created with the default UTC, while the timezone value in CONFIG_DB (DEVICE_METADATA|localhost|timezone) is preserved from the previous image. How I did it In src/sonic-host-services/scripts/hostcfgd::DeviceMetaCfg:load, we already read the timezone from CONFIG_DB to save it for future events (DeviceMetaCfg:timezone_update observes changes in DEVICE_METADATA). So at this point, when hostcfgd is up, we can simply update the Linux file /etc/localtime with the initial correct timezone value from CONFIG_DB by executing (only if it differs from the current /etc/localtime): timedatectl set-timezone (as is already done in timezone_update). The same pattern exists in hostcfgd::DnsCfg:load. How to verify it Set a non-UTC timezone on the current image: config clock timezone Asia/Jerusalem Run config save Install a new image Reboot and check that hostcfgd is up: systemctl status hostcfgd (~1m) Verify that the timezone is identical in CONFIG_DB and /etc/localtime: timedatectl date cat /etc/localtime cat /etc/sonic/config_db.json | grep timezone hget "DEVICE_METADATA|localhost" "timezone"
1 parent 8b7ae3e commit 6507fa3

File tree

2 files changed

+163
-20
lines changed

2 files changed

+163
-20
lines changed

scripts/hostcfgd

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ MKHOME_DIR_LIB_REG = r'.*pam_mkhomedir'
5050
ETC_PAMD_SSHD = "/etc/pam.d/sshd"
5151
ETC_PAMD_LOGIN = "/etc/pam.d/login"
5252
ETC_LOGIN_DEF = "/etc/login.defs"
53+
ETC_LOCALTIME = "/etc/localtime"
54+
ZONEINFO_DIR = "/usr/share/zoneinfo"
5355

5456
# Linux login.def default values (password hardening disable)
5557
LINUX_DEFAULT_PASS_MAX_DAYS = 99999
@@ -1495,6 +1497,7 @@ class DeviceMetaCfg(object):
14951497

14961498
# Load appropriate config
14971499
self.timezone = dev_meta.get('localhost', {}).get('timezone')
1500+
self.apply_timezone_if_needed(self.timezone)
14981501
self.syslog_with_osversion = dev_meta.get('localhost', {}).get('syslog_with_osversion')
14991502

15001503
def hostname_update(self, data):
@@ -1527,34 +1530,50 @@ class DeviceMetaCfg(object):
15271530
return
15281531
run_cmd(['sudo', 'monit', 'reload'])
15291532

1530-
def timezone_update(self, data):
1533+
def apply_timezone_if_needed(self, new_tz):
15311534
"""
1532-
Apply timezone handler.
1535+
Apply timezone if it differs from either:
1536+
- the cached DB value (self.timezone)
1537+
- the actual system timezone (/etc/localtime)
15331538
Run the following command in Linux: timedatectl set-timezone <timezone>
1539+
15341540
Args:
1535-
data: Read table's key's data.
1541+
new_tz: New timezone value from DB
15361542
"""
1537-
new_timezone = data.get('timezone')
1538-
syslog.syslog(syslog.LOG_DEBUG,
1539-
f'DeviceMetaCfg: timezone update to {new_timezone}')
1543+
try:
1544+
syslog.syslog(syslog.LOG_DEBUG, f'DeviceMetaCfg: timezone update to {new_tz}')
1545+
if new_tz is None:
1546+
syslog.syslog(syslog.LOG_DEBUG, 'DeviceMetaCfg: Recieved empty timezone')
1547+
return
1548+
1549+
system_timezone_realpath = os.path.realpath(ETC_LOCALTIME)
1550+
new_timezone_realpath = os.path.realpath(f'{ZONEINFO_DIR}/{new_tz}')
1551+
if new_tz == self.timezone and new_timezone_realpath == system_timezone_realpath:
1552+
syslog.syslog(syslog.LOG_DEBUG, 'DeviceMetaCfg: No change in timezone')
1553+
return
15401554

1541-
if new_timezone is None:
1542-
syslog.syslog(syslog.LOG_DEBUG,
1543-
f'DeviceMetaCfg: Recieved empty timezone')
1544-
return
1555+
run_cmd(['timedatectl', 'set-timezone', new_tz])
1556+
self.timezone = new_tz
1557+
syslog.syslog(syslog.LOG_INFO, f'DeviceMetaCfg: Applied timezone {self.timezone}')
15451558

1546-
if new_timezone == self.timezone:
1547-
syslog.syslog(syslog.LOG_DEBUG,
1548-
f'DeviceMetaCfg: No change in timezone')
1549-
return
1559+
run_cmd(['systemctl', 'restart', 'rsyslog'], True, False)
1560+
syslog.syslog(syslog.LOG_INFO, 'DeviceMetaCfg: Restarted rsyslog after timezone change')
15501561

1551-
# run command will print out log error in case of error
1552-
run_cmd(['timedatectl', 'set-timezone', new_timezone])
1553-
self.timezone = new_timezone
1562+
except OSError as e:
1563+
syslog.syslog(syslog.LOG_ERR, f'DeviceMetaCfg: Invalid timezone files for {ETC_LOCALTIME} {new_tz}: {e}')
1564+
except subprocess.CalledProcessError as e:
1565+
syslog.syslog(syslog.LOG_ERR, f'DeviceMetaCfg: Failed to set-timezone {new_tz} and restart rsyslog: {e}')
1566+
except Exception as e:
1567+
syslog.syslog(syslog.LOG_ERR, f'DeviceMetaCfg: Failed to apply timezone {new_tz}: {e}')
15541568

1555-
run_cmd(['systemctl', 'restart', 'rsyslog'], True, False)
1556-
syslog.syslog(syslog.LOG_INFO, 'DeviceMetaCfg: Restart rsyslog after '
1557-
'changing timezone')
1569+
def timezone_update(self, data):
1570+
"""
1571+
Call apply timezone handler.
1572+
1573+
Args:
1574+
data: Read table's key's data.
1575+
"""
1576+
self.apply_timezone_if_needed(data.get('timezone'))
15581577

15591578
def rsyslog_config(self, data):
15601579
"""

tests/hostcfgd/hostcfgd_test.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,3 +853,127 @@ def test_process_name_mismatch(self):
853853
pid = self.mem_stat_cfg.get_memory_statistics_pid()
854854
self.assertIsNone(pid)
855855
mock_syslog.assert_any_call(mock.ANY, "MemoryStatisticsCfg: PID 123 does not correspond to memory_statistics_service.py.")
856+
857+
858+
class TestDeviceMetaCfgLoad(TestCase):
859+
"""Test suite for DeviceMetaCfg load method with timezone functionality."""
860+
861+
def setUp(self):
862+
"""Set up test environment before each test case."""
863+
self.devmeta_cfg = hostcfgd.DeviceMetaCfg()
864+
865+
@mock.patch('hostcfgd.os.path.realpath')
866+
@mock.patch('hostcfgd.run_cmd')
867+
@mock.patch('hostcfgd.syslog.syslog')
868+
def test_load_initial_timezone_different_from_current(self, mock_syslog, mock_run_cmd, mock_realpath):
869+
""" Test initial timezone setting when desired timezone differs from current. """
870+
mock_realpath.side_effect = [
871+
'/usr/share/zoneinfo/UTC',
872+
'/usr/share/zoneinfo/America/New_York'
873+
]
874+
dev_meta = {
875+
'localhost': {
876+
'hostname': 'test-host',
877+
'timezone': 'America/New_York'
878+
}
879+
}
880+
881+
self.devmeta_cfg.load(dev_meta)
882+
883+
expected_calls = [
884+
call(['timedatectl', 'set-timezone', 'America/New_York']),
885+
call(['systemctl', 'restart', 'rsyslog'], True, False)
886+
]
887+
mock_run_cmd.assert_has_calls(expected_calls, any_order=False)
888+
889+
expected_syslog_calls = [
890+
call(mock.ANY, 'DeviceMetaCfg: Applied timezone America/New_York'),
891+
call(mock.ANY, 'DeviceMetaCfg: Restarted rsyslog after timezone change')
892+
]
893+
mock_syslog.assert_has_calls(expected_syslog_calls, any_order=False)
894+
895+
@mock.patch('hostcfgd.os.path.realpath')
896+
@mock.patch('hostcfgd.run_cmd')
897+
@mock.patch('hostcfgd.syslog.syslog')
898+
def test_apply_timezone_oserror_exception(self, mock_syslog, mock_run_cmd, mock_realpath):
899+
""" Test OSError exception handling in apply_timezone_if_needed. """
900+
mock_realpath.side_effect = [
901+
'/usr/share/zoneinfo/UTC',
902+
OSError("Permission denied accessing timezone file")
903+
]
904+
self.devmeta_cfg.apply_timezone_if_needed('America/New_York')
905+
expected_syslog_calls = [
906+
call(mock.ANY, 'DeviceMetaCfg: timezone update to America/New_York'),
907+
call(mock.ANY, 'DeviceMetaCfg: Invalid timezone files for /etc/localtime America/New_York: Permission denied accessing timezone file')
908+
]
909+
mock_syslog.assert_has_calls(expected_syslog_calls, any_order=False)
910+
mock_run_cmd.assert_not_called()
911+
912+
@mock.patch('hostcfgd.os.path.realpath')
913+
@mock.patch('hostcfgd.run_cmd')
914+
@mock.patch('hostcfgd.syslog.syslog')
915+
def test_apply_timezone_subprocess_exception(self, mock_syslog, mock_run_cmd, mock_realpath):
916+
""" Test subprocess.CalledProcessError exception handling in apply_timezone_if_needed. """
917+
mock_realpath.side_effect = [
918+
'/usr/share/zoneinfo/UTC',
919+
'/usr/share/zoneinfo/America/New_York'
920+
]
921+
mock_run_cmd.side_effect = [
922+
CalledProcessError(returncode=1, cmd=['timedatectl', 'set-timezone', 'America/New_York'], output="Invalid timezone"),
923+
None
924+
]
925+
self.devmeta_cfg.apply_timezone_if_needed('America/New_York')
926+
expected_syslog_calls = [
927+
call(mock.ANY, 'DeviceMetaCfg: timezone update to America/New_York'),
928+
call(mock.ANY, 'DeviceMetaCfg: Failed to set-timezone America/New_York and restart rsyslog: Command \'[\'timedatectl\', \'set-timezone\', \'America/New_York\']\' returned non-zero exit status 1.')
929+
]
930+
mock_syslog.assert_has_calls(expected_syslog_calls, any_order=False)
931+
932+
@mock.patch('hostcfgd.os.path.realpath')
933+
@mock.patch('hostcfgd.run_cmd')
934+
@mock.patch('hostcfgd.syslog.syslog')
935+
def test_apply_timezone_general_exception(self, mock_syslog, mock_run_cmd, mock_realpath):
936+
""" Test general Exception handling in apply_timezone_if_needed. """
937+
mock_realpath.side_effect = [
938+
'/usr/share/zoneinfo/UTC',
939+
'/usr/share/zoneinfo/America/New_York'
940+
]
941+
mock_run_cmd.side_effect = RuntimeError("Unexpected system error")
942+
self.devmeta_cfg.apply_timezone_if_needed('America/New_York')
943+
expected_syslog_calls = [
944+
call(mock.ANY, 'DeviceMetaCfg: timezone update to America/New_York'),
945+
call(mock.ANY, 'DeviceMetaCfg: Failed to apply timezone America/New_York: Unexpected system error')
946+
]
947+
mock_syslog.assert_has_calls(expected_syslog_calls, any_order=False)
948+
949+
@mock.patch('hostcfgd.os.path.realpath')
950+
@mock.patch('hostcfgd.run_cmd')
951+
@mock.patch('hostcfgd.syslog.syslog')
952+
def test_apply_timezone_no_change_needed(self, mock_syslog, mock_run_cmd, mock_realpath):
953+
""" Test apply_timezone_if_needed when no change is needed. """
954+
mock_realpath.side_effect = [
955+
'/usr/share/zoneinfo/America/New_York',
956+
'/usr/share/zoneinfo/America/New_York'
957+
]
958+
self.devmeta_cfg.timezone = 'America/New_York'
959+
self.devmeta_cfg.apply_timezone_if_needed('America/New_York')
960+
expected_syslog_calls = [
961+
call(mock.ANY, 'DeviceMetaCfg: timezone update to America/New_York'),
962+
call(mock.ANY, 'DeviceMetaCfg: No change in timezone')
963+
]
964+
mock_syslog.assert_has_calls(expected_syslog_calls, any_order=False)
965+
mock_run_cmd.assert_not_called()
966+
967+
@mock.patch('hostcfgd.os.path.realpath')
968+
@mock.patch('hostcfgd.run_cmd')
969+
@mock.patch('hostcfgd.syslog.syslog')
970+
def test_apply_timezone_empty_timezone(self, mock_syslog, mock_run_cmd, mock_realpath):
971+
""" Test apply_timezone_if_needed with None/empty timezone. """
972+
self.devmeta_cfg.apply_timezone_if_needed(None)
973+
expected_syslog_calls = [
974+
call(mock.ANY, 'DeviceMetaCfg: timezone update to None'),
975+
call(mock.ANY, 'DeviceMetaCfg: Recieved empty timezone')
976+
]
977+
mock_syslog.assert_has_calls(expected_syslog_calls, any_order=False)
978+
mock_realpath.assert_not_called()
979+
mock_run_cmd.assert_not_called()

0 commit comments

Comments
 (0)