Skip to content

Commit e59bbfc

Browse files
Fixing state_db not having delete_field attribute causing a crash when DPUs in bad state (#4064)
* fixing state_db not having delete_field attribute causing a crash when DPUs in bad state * Addressed copilot review comment * Addressed copilot review comment * Addressed a test case test_delete_field to cover the newly added code * Fixed SA errors * Addressing review comments * Addressing review comments * Addressing review comments
1 parent 9386963 commit e59bbfc

File tree

2 files changed

+42
-8
lines changed

2 files changed

+42
-8
lines changed

config/chassis_modules.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import subprocess
77
import utilities_common.cli as clicommon
88
from utilities_common.chassis import is_smartswitch, get_all_dpus
9-
from datetime import datetime, timedelta
9+
from datetime import datetime, timedelta, timezone
1010

1111
TIMEOUT_SECS = 10
1212
TRANSITION_TIMEOUT = timedelta(seconds=240) # 4 minutes
@@ -27,6 +27,12 @@ def set_entry(self, table, key, entry):
2727
for field, value in entry.items():
2828
self.db.set("STATE_DB", redis_key, field, value)
2929

30+
def delete_field(self, table, key, field):
31+
"""Delete a specific field from table|key."""
32+
redis_key = f"{table}|{key}"
33+
client = self.db.get_redis_client("STATE_DB")
34+
return client.hdel(redis_key, field)
35+
3036
#
3137
# 'chassis_modules' group ('config chassis_modules ...')
3238
#
@@ -72,8 +78,9 @@ def set_state_transition_in_progress(db, chassis_module_name, value):
7278
entry = state_db.get_entry('CHASSIS_MODULE_TABLE', chassis_module_name) or {}
7379
entry['state_transition_in_progress'] = value
7480
if value == 'True':
75-
entry['transition_start_time'] = datetime.utcnow().isoformat()
81+
entry['transition_start_time'] = datetime.now(timezone.utc).isoformat()
7682
else:
83+
# Remove transition_start_time from both local entry and database
7784
entry.pop('transition_start_time', None)
7885
state_db.delete_field('CHASSIS_MODULE_TABLE', chassis_module_name, 'transition_start_time')
7986
state_db.set_entry('CHASSIS_MODULE_TABLE', chassis_module_name, entry)
@@ -92,7 +99,14 @@ def is_transition_timed_out(db, chassis_module_name):
9299
start_time = datetime.fromisoformat(start_time_str)
93100
except ValueError:
94101
return False
95-
return datetime.utcnow() - start_time > TRANSITION_TIMEOUT
102+
103+
# Use UTC everywhere for consistent comparison
104+
current_time = datetime.now(timezone.utc)
105+
if start_time.tzinfo is None:
106+
# If stored time is naive, assume it's UTC
107+
start_time = start_time.replace(tzinfo=timezone.utc)
108+
109+
return current_time - start_time > TRANSITION_TIMEOUT
96110

97111
#
98112
# Name: check_config_module_state_with_timeout

tests/chassis_modules_test.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22
import os
33
from click.testing import CliRunner
4-
from datetime import datetime, timedelta
4+
from datetime import datetime, timedelta, timezone
55
from config.chassis_modules import (
66
set_state_transition_in_progress,
77
is_transition_timed_out,
@@ -490,7 +490,7 @@ def test_shutdown_triggers_transition_in_progress(self):
490490
fvs = {
491491
'admin_status': 'up',
492492
'state_transition_in_progress': 'True',
493-
'transition_start_time': datetime.utcnow().isoformat()
493+
'transition_start_time': datetime.now(timezone.utc).isoformat()
494494
}
495495
db.cfgdb.set_entry('CHASSIS_MODULE', "DPU0", fvs)
496496

@@ -517,7 +517,7 @@ def test_shutdown_triggers_transition_timeout(self):
517517
fvs = {
518518
'admin_status': 'up',
519519
'state_transition_in_progress': 'True',
520-
'transition_start_time': (datetime.utcnow() - timedelta(minutes=30)).isoformat()
520+
'transition_start_time': (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
521521
}
522522
db.cfgdb.set_entry('CHASSIS_MODULE', "DPU0", fvs)
523523

@@ -593,15 +593,35 @@ def test_is_transition_timed_out_all_paths(self):
593593
assert is_transition_timed_out(db, "DPU0") is False
594594

595595
# Case 4: Timed out
596-
old_time = (datetime.utcnow() - TRANSITION_TIMEOUT - timedelta(seconds=1)).isoformat()
596+
old_time = (datetime.now(timezone.utc) - TRANSITION_TIMEOUT - timedelta(seconds=1)).isoformat()
597597
db.statedb.get_entry.return_value = {"transition_start_time": old_time}
598598
assert is_transition_timed_out(db, "DPU0") is True
599599

600600
# Case 5: Not timed out yet
601-
now = datetime.utcnow().isoformat()
601+
now = datetime.now(timezone.utc).isoformat()
602602
db.statedb.get_entry.return_value = {"transition_start_time": now}
603603
assert is_transition_timed_out(db, "DPU0") is False
604604

605+
def test_delete_field(self):
606+
"""Single test to cover missing delete_field and timezone handling lines"""
607+
from config.chassis_modules import StateDBHelper
608+
from datetime import timezone
609+
610+
# Test delete_field method (covers lines 32-34)
611+
mock_sonic_db = mock.MagicMock()
612+
mock_redis_client = mock.MagicMock()
613+
mock_sonic_db.get_redis_client.return_value = mock_redis_client
614+
helper = StateDBHelper(mock_sonic_db)
615+
helper.delete_field('TEST_TABLE', 'test_key', 'test_field')
616+
mock_redis_client.hdel.assert_called_once_with("TEST_TABLE|test_key", "test_field")
617+
618+
# Test timezone-aware datetime handling (covers line 109)
619+
db = mock.MagicMock()
620+
db.statedb = mock.MagicMock()
621+
tz_time = (datetime.now(timezone.utc) - TRANSITION_TIMEOUT - timedelta(seconds=1)).isoformat()
622+
db.statedb.get_entry.return_value = {"transition_start_time": tz_time}
623+
assert is_transition_timed_out(db, "DPU0") is True
624+
605625
@classmethod
606626
def teardown_class(cls):
607627
print("TEARDOWN")

0 commit comments

Comments
 (0)