Skip to content

Commit bcb10f1

Browse files
authored
Support golden config in db migrator (#3076)
What I did Need to support golden config in db migrator. How I did it If there's golden config json, read from golden config instead of minigraph. And db migrator will use golden config data to generate new configuration. How to verify it Run unit test.
1 parent 20d1495 commit bcb10f1

File tree

6 files changed

+161
-34
lines changed

6 files changed

+161
-34
lines changed

config/main.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from utilities_common.intf_filter import parse_interface_in_filter
3535
from utilities_common import bgp_util
3636
import utilities_common.cli as clicommon
37-
from utilities_common.helper import get_port_pbh_binding, get_port_acl_binding
37+
from utilities_common.helper import get_port_pbh_binding, get_port_acl_binding, update_config
3838
from utilities_common.general import load_db_config, load_module_from_source
3939
from .validated_config_db_connector import ValidatedConfigDBConnector
4040
import utilities_common.multi_asic as multi_asic_util
@@ -1950,6 +1950,7 @@ def override_config_table(db, input_config_db, dry_run):
19501950
ns_config_input = config_input
19511951
# Generate sysinfo if missing in ns_config_input
19521952
generate_sysinfo(current_config, ns_config_input, ns)
1953+
# Use deepcopy by default to avoid modifying input config
19531954
updated_config = update_config(current_config, ns_config_input)
19541955

19551956
yang_enabled = device_info.is_yang_config_validation_enabled(config_db)
@@ -1985,14 +1986,6 @@ def validate_config_by_cm(cm, config_json, jname):
19851986
sys.exit(1)
19861987

19871988

1988-
def update_config(current_config, config_input):
1989-
updated_config = copy.deepcopy(current_config)
1990-
# Override current config with golden config
1991-
for table in config_input:
1992-
updated_config[table] = config_input[table]
1993-
return updated_config
1994-
1995-
19961989
def override_config_db(config_db, config_input):
19971990
# Deserialized golden config to DB recognized format
19981991
sonic_cfggen.FormatConverter.to_deserialized(config_input)

scripts/db_migrator.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
from sonic_py_common import device_info, logger
1111
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector, SonicDBConfig
1212
from minigraph import parse_xml
13+
from utilities_common.helper import update_config
1314

1415
INIT_CFG_FILE = '/etc/sonic/init_cfg.json'
1516
MINIGRAPH_FILE = '/etc/sonic/minigraph.xml'
17+
GOLDEN_CFG_FILE = '/etc/sonic/golden_config_db.json'
1618

1719
# mock the redis for unit test purposes #
1820
try:
@@ -24,10 +26,12 @@
2426
sys.path.insert(0, tests_path)
2527
INIT_CFG_FILE = os.path.join(mocked_db_path, "init_cfg.json")
2628
MINIGRAPH_FILE = os.path.join(mocked_db_path, "minigraph.xml")
29+
GOLDEN_CFG_FILE = os.path.join(mocked_db_path, "golden_config_db.json")
2730
except KeyError:
2831
pass
2932

3033
SYSLOG_IDENTIFIER = 'db_migrator'
34+
DEFAULT_NAMESPACE = ''
3135

3236

3337
# Global logger instance
@@ -60,15 +64,8 @@ def __init__(self, namespace, socket=None):
6064
self.TABLE_KEY = 'DATABASE'
6165
self.TABLE_FIELD = 'VERSION'
6266

63-
# load config data from minigraph to get the default/hardcoded values from minigraph.py
64-
# this is to avoid duplicating the hardcoded these values in db_migrator
65-
self.minigraph_data = None
66-
try:
67-
if os.path.isfile(MINIGRAPH_FILE):
68-
self.minigraph_data = parse_xml(MINIGRAPH_FILE)
69-
except Exception as e:
70-
log.log_error('Caught exception while trying to parse minigraph: ' + str(e))
71-
pass
67+
# Generate config_src_data from minigraph and golden config
68+
self.generate_config_src(namespace)
7269

7370
db_kwargs = {}
7471
if socket:
@@ -107,6 +104,55 @@ def __init__(self, namespace, socket=None):
107104
from mellanox_buffer_migrator import MellanoxBufferMigrator
108105
self.mellanox_buffer_migrator = MellanoxBufferMigrator(self.configDB, self.appDB, self.stateDB)
109106

107+
def generate_config_src(self, ns):
108+
'''
109+
Generate config_src_data from minigraph and golden config
110+
This method uses golden_config_data and minigraph_data as local variables,
111+
which means they are not accessible or modifiable from outside this method.
112+
This way, this method ensures that these variables are not changed unintentionally.
113+
Args:
114+
ns: namespace
115+
Returns:
116+
'''
117+
# load config data from golden_config_db.json
118+
golden_config_data = None
119+
try:
120+
if os.path.isfile(GOLDEN_CFG_FILE):
121+
with open(GOLDEN_CFG_FILE) as f:
122+
golden_data = json.load(f)
123+
if ns is None:
124+
golden_config_data = golden_data
125+
else:
126+
if ns == DEFAULT_NAMESPACE:
127+
config_namespace = "localhost"
128+
else:
129+
config_namespace = ns
130+
golden_config_data = golden_data[config_namespace]
131+
except Exception as e:
132+
log.log_error('Caught exception while trying to load golden config: ' + str(e))
133+
pass
134+
# load config data from minigraph to get the default/hardcoded values from minigraph.py
135+
minigraph_data = None
136+
try:
137+
if os.path.isfile(MINIGRAPH_FILE):
138+
minigraph_data = parse_xml(MINIGRAPH_FILE)
139+
except Exception as e:
140+
log.log_error('Caught exception while trying to parse minigraph: ' + str(e))
141+
pass
142+
# When both golden config and minigraph exists, override minigraph config with golden config
143+
# config_src_data is the source of truth for config data
144+
# this is to avoid duplicating the hardcoded these values in db_migrator
145+
self.config_src_data = None
146+
if minigraph_data:
147+
# Shallow copy for better performance
148+
self.config_src_data = minigraph_data
149+
if golden_config_data:
150+
# Shallow copy for better performance
151+
self.config_src_data = update_config(minigraph_data, golden_config_data, False)
152+
elif golden_config_data:
153+
# Shallow copy for better performance
154+
self.config_src_data = golden_config_data
155+
110156
def migrate_pfc_wd_table(self):
111157
'''
112158
Migrate all data entries from table PFC_WD_TABLE to PFC_WD
@@ -547,9 +593,9 @@ def migrate_vxlan_config(self):
547593

548594
def migrate_restapi(self):
549595
# RESTAPI - add missing key
550-
if not self.minigraph_data or 'RESTAPI' not in self.minigraph_data:
596+
if not self.config_src_data or 'RESTAPI' not in self.config_src_data:
551597
return
552-
restapi_data = self.minigraph_data['RESTAPI']
598+
restapi_data = self.config_src_data['RESTAPI']
553599
log.log_notice('Migrate RESTAPI configuration')
554600
config = self.configDB.get_entry('RESTAPI', 'config')
555601
if not config:
@@ -560,9 +606,9 @@ def migrate_restapi(self):
560606

561607
def migrate_telemetry(self):
562608
# TELEMETRY - add missing key
563-
if not self.minigraph_data or 'TELEMETRY' not in self.minigraph_data:
609+
if not self.config_src_data or 'TELEMETRY' not in self.config_src_data:
564610
return
565-
telemetry_data = self.minigraph_data['TELEMETRY']
611+
telemetry_data = self.config_src_data['TELEMETRY']
566612
log.log_notice('Migrate TELEMETRY configuration')
567613
gnmi = self.configDB.get_entry('TELEMETRY', 'gnmi')
568614
if not gnmi:
@@ -573,9 +619,9 @@ def migrate_telemetry(self):
573619

574620
def migrate_console_switch(self):
575621
# CONSOLE_SWITCH - add missing key
576-
if not self.minigraph_data or 'CONSOLE_SWITCH' not in self.minigraph_data:
622+
if not self.config_src_data or 'CONSOLE_SWITCH' not in self.config_src_data:
577623
return
578-
console_switch_data = self.minigraph_data['CONSOLE_SWITCH']
624+
console_switch_data = self.config_src_data['CONSOLE_SWITCH']
579625
log.log_notice('Migrate CONSOLE_SWITCH configuration')
580626
console_mgmt = self.configDB.get_entry('CONSOLE_SWITCH', 'console_mgmt')
581627
if not console_mgmt:
@@ -584,11 +630,11 @@ def migrate_console_switch(self):
584630

585631
def migrate_device_metadata(self):
586632
# DEVICE_METADATA - synchronous_mode entry
587-
if not self.minigraph_data or 'DEVICE_METADATA' not in self.minigraph_data:
633+
if not self.config_src_data or 'DEVICE_METADATA' not in self.config_src_data:
588634
return
589635
log.log_notice('Migrate DEVICE_METADATA missing configuration')
590636
metadata = self.configDB.get_entry('DEVICE_METADATA', 'localhost')
591-
device_metadata_data = self.minigraph_data["DEVICE_METADATA"]["localhost"]
637+
device_metadata_data = self.config_src_data["DEVICE_METADATA"]["localhost"]
592638
if 'synchronous_mode' not in metadata:
593639
metadata['synchronous_mode'] = device_metadata_data.get("synchronous_mode")
594640
self.configDB.set_entry('DEVICE_METADATA', 'localhost', metadata)
@@ -628,19 +674,19 @@ def migrate_dns_nameserver(self):
628674
Handle DNS_NAMESERVER table migration. Migrations handled:
629675
If there's no DNS_NAMESERVER in config_DB, load DNS_NAMESERVER from minigraph
630676
"""
631-
if not self.minigraph_data or 'DNS_NAMESERVER' not in self.minigraph_data:
677+
if not self.config_src_data or 'DNS_NAMESERVER' not in self.config_src_data:
632678
return
633679
dns_table = self.configDB.get_table('DNS_NAMESERVER')
634680
if not dns_table:
635-
for addr, config in self.minigraph_data['DNS_NAMESERVER'].items():
681+
for addr, config in self.config_src_data['DNS_NAMESERVER'].items():
636682
self.configDB.set_entry('DNS_NAMESERVER', addr, config)
637683

638684
def migrate_routing_config_mode(self):
639685
# DEVICE_METADATA - synchronous_mode entry
640-
if not self.minigraph_data or 'DEVICE_METADATA' not in self.minigraph_data:
686+
if not self.config_src_data or 'DEVICE_METADATA' not in self.config_src_data:
641687
return
642688
device_metadata_old = self.configDB.get_entry('DEVICE_METADATA', 'localhost')
643-
device_metadata_new = self.minigraph_data['DEVICE_METADATA']['localhost']
689+
device_metadata_new = self.config_src_data['DEVICE_METADATA']['localhost']
644690
# overwrite the routing-config-mode as per minigraph parser
645691
# Criteria for update:
646692
# if config mode is missing in base OS or if base and target modes are not same
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"DEVICE_METADATA": {
3+
"localhost": {
4+
"hostname": "SONiC-Golden-Config"
5+
}}
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"DEVICE_METADATA": {
3+
"localhost": {
4+
"hostname": "SONiC-Golden-Config"
5+
}
6+
}
7+
}

tests/db_migrator_test.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,8 @@ def test_dns_nameserver_migrator(self):
368368
dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns-nameserver-input')
369369
import db_migrator
370370
dbmgtr = db_migrator.DBMigrator(None)
371-
# Set minigraph_data to DNS_NAMESERVERS
372-
dbmgtr.minigraph_data = {
371+
# Set config_src_data to DNS_NAMESERVERS
372+
dbmgtr.config_src_data = {
373373
'DNS_NAMESERVER': {
374374
'1.1.1.1': {},
375375
'2001:1001:110:1001::1': {}
@@ -635,8 +635,8 @@ def test_warm_upgrade__without_mg_to_2_0_2(self):
635635
dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'cross_branch_upgrade_to_version_2_0_2_input')
636636
import db_migrator
637637
dbmgtr = db_migrator.DBMigrator(None)
638-
# set minigraph_data to None to mimic the missing minigraph.xml scenario
639-
dbmgtr.minigraph_data = None
638+
# set config_src_data to None to mimic the missing minigraph.xml scenario
639+
dbmgtr.config_src_data = None
640640
dbmgtr.migrate()
641641
dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'cross_branch_upgrade_without_mg_2_0_2_expected.json')
642642
expected_db = Db()
@@ -823,3 +823,49 @@ def test_sflow_migrator(self):
823823
expected_keys = expected_appl_db.get_all(expected_appl_db.APPL_DB, key)
824824
diff = DeepDiff(resulting_keys, expected_keys, ignore_order=True)
825825
assert not diff
826+
827+
class TestGoldenConfig(object):
828+
@classmethod
829+
def setup_class(cls):
830+
os.system("cp %s %s" % (mock_db_path + '/golden_config_db.json.test', mock_db_path + '/golden_config_db.json'))
831+
os.environ['UTILITIES_UNIT_TESTING'] = "2"
832+
833+
@classmethod
834+
def teardown_class(cls):
835+
os.environ['UTILITIES_UNIT_TESTING'] = "0"
836+
os.system("rm %s" % (mock_db_path + '/golden_config_db.json'))
837+
838+
def test_golden_config_hostname(self):
839+
import db_migrator
840+
dbmgtr = db_migrator.DBMigrator(None)
841+
config = dbmgtr.config_src_data
842+
device_metadata = config.get('DEVICE_METADATA', {})
843+
assert device_metadata != {}
844+
host = device_metadata.get('localhost', {})
845+
assert host != {}
846+
hostname = host.get('hostname', '')
847+
# hostname is from golden_config_db.json
848+
assert hostname == 'SONiC-Golden-Config'
849+
850+
class TestGoldenConfigInvalid(object):
851+
@classmethod
852+
def setup_class(cls):
853+
os.system("cp %s %s" % (mock_db_path + '/golden_config_db.json.invalid', mock_db_path + '/golden_config_db.json'))
854+
os.environ['UTILITIES_UNIT_TESTING'] = "2"
855+
856+
@classmethod
857+
def teardown_class(cls):
858+
os.environ['UTILITIES_UNIT_TESTING'] = "0"
859+
os.system("rm %s" % (mock_db_path + '/golden_config_db.json'))
860+
861+
def test_golden_config_hostname(self):
862+
import db_migrator
863+
dbmgtr = db_migrator.DBMigrator(None)
864+
config = dbmgtr.config_src_data
865+
device_metadata = config.get('DEVICE_METADATA', {})
866+
assert device_metadata != {}
867+
host = device_metadata.get('localhost', {})
868+
assert host != {}
869+
hostname = host.get('hostname', '')
870+
# hostname is from minigraph.xml
871+
assert hostname == 'SONiC-Dummy'

utilities_common/helper.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dump.match_infra import MatchEngine, MatchRequest, ConnectionPool
22
from dump.match_helper import get_matched_keys
33
from .db import Db
4+
import copy
45

56
def get_port_acl_binding(db_wrap, port, ns):
67
"""
@@ -64,3 +65,30 @@ def get_port_pbh_binding(db_wrap, port, ns):
6465
ret = m_engine.fetch(req)
6566
pbh_tables, _ = get_matched_keys(ret)
6667
return pbh_tables
68+
69+
70+
def update_config(current_config, config_input, deepcopy=True):
71+
"""
72+
Override current config with golden config
73+
Shallow copy only copies the references to the original object,
74+
so any changes to one object will also change the other.
75+
Therefore, we should be careful when using shallow copy to avoid unwanted modifications.
76+
77+
Args:
78+
current_config: current config
79+
config_input: input golden config
80+
deepcopy: True for deep copy, False for shallow copy
81+
82+
Returns:
83+
Final config after overriding
84+
"""
85+
if deepcopy:
86+
# Deep copy for safety
87+
updated_config = copy.deepcopy(current_config)
88+
else:
89+
# Shallow copy for better performance
90+
updated_config = current_config
91+
# Override current config with golden config
92+
for table in config_input:
93+
updated_config[table] = config_input[table]
94+
return updated_config

0 commit comments

Comments
 (0)