Skip to content

Commit 0af4386

Browse files
Consolidate the get running config way. (sonic-net#3585)
#### What I did Addressing the [issue 20508](sonic-net#20508). ADO: 29979987 #### How I did it Remove temp file as intermediate steps. #### How to verify it ``` admin@str2-7250-lc1-2:~$ cat test.json [ { "op": "add", "path": "/asic0/BGP_DEVICE_GLOBAL/STATE/idf_isolation_state", "value": "unisolated" } ] admin@str2-7250-lc1-2:~$ sudo config apply-patch test.json sonic_yang(6):Note: Below table(s) have no YANG models: DHCP_SERVER sonic_yang(6):Note: Below table(s) have no YANG models: LOGGER sonic_yang(6):Note: Below table(s) have no YANG models: LOGGER Patch Applier: asic0: Patch application starting. Patch Applier: asic0: Patch: [{"op": "add", "path": "/BGP_DEVICE_GLOBAL/STATE/idf_isolation_state", "value": "unisolated"}] Patch Applier: asic0 getting current config db. Patch Applier: asic0: simulating the target full config after applying the patch. Patch Applier: asic0: validating all JsonPatch operations are permitted on the specified fields Patch Applier: asic0: validating target config does not have empty tables, since they do not show up in ConfigDb. Patch Applier: asic0: sorting patch updates. Patch Applier: The asic0 patch was converted into 1 change: Patch Applier: asic0: applying 1 change in order: Patch Applier: * [{"op": "replace", "path": "/BGP_DEVICE_GLOBAL/STATE/idf_isolation_state", "value": "unisolated"}] Patch Applier: asic0: verifying patch updates are reflected on ConfigDB. Patch Applier: asic0 patch application completed. Patch applied successfully. admin@str2-7250-lc1-2:~$ show ver SONiC Software Version: SONiC.20220532.72 SONiC OS Version: 11 Distribution: Debian 11.9 Kernel: 5.10.0-23-2-amd64 Build commit: 7766169087 Build date: Fri Oct 4 00:15:40 UTC 2024 Built by: azureuser@98b2318ac000000 Platform: x86_64-nokia_ixr7250e_36x400g-r0 HwSKU: Nokia-IXR7250E-36x100G ASIC: broadcom ASIC Count: 2 Serial Number: NS220304200 Model Number: 3HE12578AARA01 Hardware Revision: 56 Uptime: 05:08:45 up 2 days, 10:16, 1 user, load average: 1.64, 1.82, 1.74 Date: Fri 25 Oct 2024 05:08:45 Docker images: REPOSITORY TAG IMAGE ID SIZE docker-mux 20220532.72 a27de04f0900 375MB docker-mux latest a27de04f0900 375MB docker-macsec latest 9cad4ac054db 372MB docker-sonic-restapi 20220532.72 2dc9b6c42cdb 345MB docker-sonic-restapi latest 2dc9b6c42cdb 345MB docker-orchagent 20220532.72 560867c70e69 360MB docker-orchagent latest 560867c70e69 360MB docker-fpm-frr 20220532.72 525aad3b1670 367MB docker-fpm-frr latest 525aad3b1670 367MB docker-teamd 20220532.72 9bc2875ba21c 343MB docker-teamd latest 9bc2875ba21c 343MB docker-syncd-brcm-dnx 20220532.72 58ee35f9df5b 674MB docker-syncd-brcm-dnx latest 58ee35f9df5b 674MB docker-gbsyncd-credo 20220532.72 5084ec39b3fc 346MB docker-gbsyncd-credo latest 5084ec39b3fc 346MB docker-gbsyncd-broncos 20220532.72 f1011e5ed75c 374MB docker-gbsyncd-broncos latest f1011e5ed75c 374MB docker-dhcp-relay latest 137faf5f4038 337MB docker-platform-monitor 20220532.72 41d6954ab85a 452MB docker-platform-monitor latest 41d6954ab85a 452MB docker-snmp 20220532.72 916f66a40a77 376MB docker-snmp latest 916f66a40a77 376MB docker-sonic-telemetry 20220532.72 e8037e0fd00c 407MB docker-sonic-telemetry latest e8037e0fd00c 407MB docker-router-advertiser 20220532.72 a5afbccec5da 327MB docker-router-advertiser latest a5afbccec5da 327MB docker-lldp 20220532.72 01386dd544cf 369MB docker-lldp latest 01386dd544cf 369MB docker-database 20220532.72 2da62f2abd04 327MB docker-database latest 2da62f2abd04 327MB docker-acms 20220532.72 264a51a7a259 374MB docker-acms latest 264a51a7a259 374MB k8s.gcr.io/pause 3.5 ed210e3e4a5b 683kB ```
1 parent 964b489 commit 0af4386

File tree

5 files changed

+88
-118
lines changed

5 files changed

+88
-118
lines changed

generic_config_updater/change_applier.py

+3-30
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from swsscommon.swsscommon import ConfigDBConnector
1010
from sonic_py_common import multi_asic
1111
from .gu_common import GenericConfigUpdaterError, genericUpdaterLogging
12+
from .gu_common import get_config_db_as_json
1213

1314
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
1415
UPDATER_CONF_FILE = f"{SCRIPT_DIR}/gcu_services_validator.conf.json"
@@ -137,7 +138,7 @@ def _report_mismatch(self, run_data, upd_data):
137138
str(jsondiff.diff(run_data, upd_data))[0:40]))
138139

139140
def apply(self, change):
140-
run_data = self._get_running_config()
141+
run_data = get_config_db_as_json(self.scope)
141142
upd_data = prune_empty_table(change.apply(copy.deepcopy(run_data)))
142143
upd_keys = defaultdict(dict)
143144

@@ -146,7 +147,7 @@ def apply(self, change):
146147

147148
ret = self._services_validate(run_data, upd_data, upd_keys)
148149
if not ret:
149-
run_data = self._get_running_config()
150+
run_data = get_config_db_as_json(self.scope)
150151
self.remove_backend_tables_from_config(upd_data)
151152
self.remove_backend_tables_from_config(run_data)
152153
if upd_data != run_data:
@@ -159,31 +160,3 @@ def apply(self, change):
159160
def remove_backend_tables_from_config(self, data):
160161
for key in self.backend_tables:
161162
data.pop(key, None)
162-
163-
def _get_running_config(self):
164-
_, fname = tempfile.mkstemp(suffix="_changeApplier")
165-
166-
if self.scope:
167-
cmd = ['sonic-cfggen', '-d', '--print-data', '-n', self.scope]
168-
else:
169-
cmd = ['sonic-cfggen', '-d', '--print-data']
170-
171-
with open(fname, "w") as file:
172-
result = subprocess.Popen(cmd, stdout=file, stderr=subprocess.PIPE, text=True)
173-
_, err = result.communicate()
174-
175-
return_code = result.returncode
176-
if return_code:
177-
os.remove(fname)
178-
raise GenericConfigUpdaterError(
179-
f"Failed to get running config for scope: {self.scope}," +
180-
f"Return code: {return_code}, Error: {err}")
181-
182-
run_data = {}
183-
try:
184-
with open(fname, "r") as file:
185-
run_data = json.load(file)
186-
finally:
187-
if os.path.isfile(fname):
188-
os.remove(fname)
189-
return run_data

generic_config_updater/gu_common.py

+24-16
Original file line numberDiff line numberDiff line change
@@ -53,31 +53,39 @@ def __eq__(self, other):
5353
return self.patch == other.patch
5454
return False
5555

56+
57+
def get_config_db_as_json(scope=None):
58+
text = get_config_db_as_text(scope=scope)
59+
config_db_json = json.loads(text)
60+
config_db_json.pop("bgpraw", None)
61+
return config_db_json
62+
63+
64+
def get_config_db_as_text(scope=None):
65+
if scope is not None and scope != multi_asic.DEFAULT_NAMESPACE:
66+
cmd = ['sonic-cfggen', '-d', '--print-data', '-n', scope]
67+
else:
68+
cmd = ['sonic-cfggen', '-d', '--print-data']
69+
result = subprocess.Popen(cmd, shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
70+
text, err = result.communicate()
71+
return_code = result.returncode
72+
if return_code:
73+
raise GenericConfigUpdaterError(f"Failed to get running config for namespace: {scope},"
74+
f" Return code: {return_code}, Error: {err}")
75+
return text
76+
77+
5678
class ConfigWrapper:
5779
def __init__(self, yang_dir=YANG_DIR, scope=multi_asic.DEFAULT_NAMESPACE):
5880
self.scope = scope
5981
self.yang_dir = YANG_DIR
6082
self.sonic_yang_with_loaded_models = None
6183

6284
def get_config_db_as_json(self):
63-
text = self._get_config_db_as_text()
64-
config_db_json = json.loads(text)
65-
config_db_json.pop("bgpraw", None)
66-
return config_db_json
85+
return get_config_db_as_json(self.scope)
6786

6887
def _get_config_db_as_text(self):
69-
if self.scope is not None and self.scope != multi_asic.DEFAULT_NAMESPACE:
70-
cmd = ['sonic-cfggen', '-d', '--print-data', '-n', self.scope]
71-
else:
72-
cmd = ['sonic-cfggen', '-d', '--print-data']
73-
74-
result = subprocess.Popen(cmd, shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
75-
text, err = result.communicate()
76-
return_code = result.returncode
77-
if return_code: # non-zero means failure
78-
raise GenericConfigUpdaterError(f"Failed to get running config for namespace: {self.scope},"
79-
f" Return code: {return_code}, Error: {err}")
80-
return text
88+
return get_config_db_as_text(self.scope)
8189

8290
def get_sonic_yang_as_json(self):
8391
config_db_json = self.get_config_db_as_json()

tests/generic_config_updater/change_applier_test.py

+11-14
Original file line numberDiff line numberDiff line change
@@ -72,28 +72,25 @@
7272
def debug_print(msg):
7373
print(msg)
7474

75-
76-
# Mimics os.system call for sonic-cfggen -d --print-data > filename
75+
# Mimics os.system call for `sonic-cfggen -d --print-data` output
7776
def subprocess_Popen_cfggen(cmd, *args, **kwargs):
7877
global running_config
7978

80-
# Extract file name from kwargs if 'stdout' is a file object
81-
stdout = kwargs.get('stdout')
82-
if hasattr(stdout, 'name'):
83-
fname = stdout.name
79+
stdout = kwargs.get('stdout', None)
80+
81+
if stdout is None:
82+
output = json.dumps(running_config, indent=4)
83+
elif isinstance(stdout, int) and stdout == -1:
84+
output = json.dumps(running_config, indent=4)
8485
else:
85-
raise ValueError("stdout is not a file")
86+
raise ValueError("stdout must be set to subprocess.PIPE or omitted for capturing output")
8687

87-
# Write the running configuration to the file specified in stdout
88-
with open(fname, "w") as s:
89-
json.dump(running_config, s, indent=4)
90-
9188
class MockPopen:
9289
def __init__(self):
93-
self.returncode = 0 # Simulate successful command execution
90+
self.returncode = 0
9491

9592
def communicate(self):
96-
return "", "" # Simulate empty stdout and stderr
93+
return output.encode(), "".encode()
9794

9895
return MockPopen()
9996

@@ -225,7 +222,7 @@ def vlan_validate(old_cfg, new_cfg, keys):
225222

226223
class TestChangeApplier(unittest.TestCase):
227224

228-
@patch("generic_config_updater.change_applier.subprocess.Popen")
225+
@patch("generic_config_updater.gu_common.subprocess.Popen")
229226
@patch("generic_config_updater.change_applier.get_config_db")
230227
@patch("generic_config_updater.change_applier.set_config")
231228
def test_change_apply(self, mock_set, mock_db, mock_subprocess_Popen):

tests/generic_config_updater/gcu_feature_patch_application_test.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
from mock import patch
77

88
import generic_config_updater.change_applier
9+
import generic_config_updater.gu_common
910
import generic_config_updater.patch_sorter as ps
1011
import generic_config_updater.generic_updater as gu
1112
from .gutest_helpers import Files
1213
from generic_config_updater.gu_common import ConfigWrapper, PatchWrapper
1314

1415
running_config = {}
15-
16+
17+
1618
def set_entry(config_db, tbl, key, data):
1719
global running_config
1820
if data != None:
@@ -26,9 +28,11 @@ def set_entry(config_db, tbl, key, data):
2628
if not running_config[tbl]:
2729
running_config.pop(tbl)
2830

29-
def get_running_config():
31+
32+
def get_running_config(scope="localhost"):
3033
return running_config
3134

35+
3236
class TestFeaturePatchApplication(unittest.TestCase):
3337
def setUp(self):
3438
self.config_wrapper = ConfigWrapper()
@@ -87,13 +91,13 @@ def create_patch_applier(self, config):
8791
config_wrapper = self.config_wrapper
8892
config_wrapper.get_config_db_as_json = MagicMock(side_effect=get_running_config)
8993
change_applier = generic_config_updater.change_applier.ChangeApplier()
90-
change_applier._get_running_config = MagicMock(side_effect=get_running_config)
9194
patch_wrapper = PatchWrapper(config_wrapper)
9295
return gu.PatchApplier(config_wrapper=config_wrapper, patch_wrapper=patch_wrapper, changeapplier=change_applier)
9396

97+
@patch('generic_config_updater.change_applier.get_config_db_as_json', side_effect=get_running_config)
9498
@patch("generic_config_updater.change_applier.get_config_db")
9599
@patch("generic_config_updater.change_applier.set_config")
96-
def run_single_success_case_applier(self, data, mock_set, mock_db):
100+
def run_single_success_case_applier(self, data, mock_set, mock_db, mock_get_config_db_as_json):
97101
current_config = data["current_config"]
98102
expected_config = data["expected_config"]
99103
patch = jsonpatch.JsonPatch(data["patch"])
@@ -121,7 +125,8 @@ def run_single_success_case_applier(self, data, mock_set, mock_db):
121125
self.assertEqual(simulated_config, expected_config)
122126

123127
@patch("generic_config_updater.change_applier.get_config_db")
124-
def run_single_failure_case_applier(self, data, mock_db):
128+
@patch('generic_config_updater.change_applier.get_config_db_as_json', side_effect=get_running_config)
129+
def run_single_failure_case_applier(self, data, mock_db, mock_get_config_db_as_json):
125130
current_config = data["current_config"]
126131
patch = jsonpatch.JsonPatch(data["patch"])
127132
expected_error_substrings = data["expected_error_substrings"]

tests/generic_config_updater/multiasic_change_applier_test.py

+40-53
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@
77
import generic_config_updater.gu_common
88

99

10+
def mock_get_running_config_side_effect(scope):
11+
print(f"mocked_value_for_{scope}")
12+
return {
13+
"tables": {
14+
"ACL_TABLE": {
15+
"services_to_validate": ["aclservice"],
16+
"validate_commands": ["acl_loader show table"]
17+
},
18+
"PORT": {
19+
"services_to_validate": ["portservice"],
20+
"validate_commands": ["show interfaces status"]
21+
}
22+
},
23+
"services": {
24+
"aclservice": {
25+
"validate_commands": ["acl_loader show table"]
26+
},
27+
"portservice": {
28+
"validate_commands": ["show interfaces status"]
29+
}
30+
}
31+
}
32+
33+
1034
class TestMultiAsicChangeApplier(unittest.TestCase):
1135

1236
@patch('sonic_py_common.multi_asic.is_multi_asic')
@@ -137,34 +161,15 @@ def test_extract_scope_singleasic(self, mock_is_multi_asic):
137161
except Exception:
138162
assert(not result)
139163

140-
@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
164+
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
141165
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
142166
def test_apply_change_default_scope(self, mock_ConfigDBConnector, mock_get_running_config):
143167
# Setup mock for ConfigDBConnector
144168
mock_db = MagicMock()
145169
mock_ConfigDBConnector.return_value = mock_db
146170

147171
# Setup mock for json.load to return some running configuration
148-
mock_get_running_config.return_value = {
149-
"tables": {
150-
"ACL_TABLE": {
151-
"services_to_validate": ["aclservice"],
152-
"validate_commands": ["acl_loader show table"]
153-
},
154-
"PORT": {
155-
"services_to_validate": ["portservice"],
156-
"validate_commands": ["show interfaces status"]
157-
}
158-
},
159-
"services": {
160-
"aclservice": {
161-
"validate_commands": ["acl_loader show table"]
162-
},
163-
"portservice": {
164-
"validate_commands": ["show interfaces status"]
165-
}
166-
}
167-
}
172+
mock_get_running_config.side_effect = mock_get_running_config_side_effect
168173

169174
# Instantiate ChangeApplier with the default scope
170175
applier = generic_config_updater.change_applier.ChangeApplier()
@@ -178,34 +183,13 @@ def test_apply_change_default_scope(self, mock_ConfigDBConnector, mock_get_runni
178183
# Assert ConfigDBConnector called with the correct namespace
179184
mock_ConfigDBConnector.assert_called_once_with(use_unix_socket_path=True, namespace="")
180185

181-
@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
186+
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
182187
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
183188
def test_apply_change_given_scope(self, mock_ConfigDBConnector, mock_get_running_config):
184189
# Setup mock for ConfigDBConnector
185190
mock_db = MagicMock()
186191
mock_ConfigDBConnector.return_value = mock_db
187-
188-
# Setup mock for json.load to return some running configuration
189-
mock_get_running_config.return_value = {
190-
"tables": {
191-
"ACL_TABLE": {
192-
"services_to_validate": ["aclservice"],
193-
"validate_commands": ["acl_loader show table"]
194-
},
195-
"PORT": {
196-
"services_to_validate": ["portservice"],
197-
"validate_commands": ["show interfaces status"]
198-
}
199-
},
200-
"services": {
201-
"aclservice": {
202-
"validate_commands": ["acl_loader show table"]
203-
},
204-
"portservice": {
205-
"validate_commands": ["show interfaces status"]
206-
}
207-
}
208-
}
192+
mock_get_running_config.side_effect = mock_get_running_config_side_effect
209193

210194
# Instantiate ChangeApplier with the default scope
211195
applier = generic_config_updater.change_applier.ChangeApplier(scope="asic0")
@@ -219,7 +203,7 @@ def test_apply_change_given_scope(self, mock_ConfigDBConnector, mock_get_running
219203
# Assert ConfigDBConnector called with the correct scope
220204
mock_ConfigDBConnector.assert_called_once_with(use_unix_socket_path=True, namespace="asic0")
221205

222-
@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
206+
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
223207
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
224208
def test_apply_change_failure(self, mock_ConfigDBConnector, mock_get_running_config):
225209
# Setup mock for ConfigDBConnector
@@ -241,22 +225,25 @@ def test_apply_change_failure(self, mock_ConfigDBConnector, mock_get_running_con
241225

242226
self.assertTrue('Failed to get running config' in str(context.exception))
243227

244-
@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
228+
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
245229
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
246230
def test_apply_patch_with_empty_tables_failure(self, mock_ConfigDBConnector, mock_get_running_config):
247231
# Setup mock for ConfigDBConnector
248232
mock_db = MagicMock()
249233
mock_ConfigDBConnector.return_value = mock_db
250234

251235
# Setup mock for json.load to simulate configuration where crucial tables are unexpectedly empty
252-
mock_get_running_config.return_value = {
253-
"tables": {
254-
# Simulate empty tables or missing crucial configuration
255-
},
256-
"services": {
257-
# Normally, services would be listed here
236+
def mock_get_empty_running_config_side_effect():
237+
return {
238+
"tables": {
239+
# Simulate empty tables or missing crucial configuration
240+
},
241+
"services": {
242+
# Normally, services would be listed here
243+
}
258244
}
259-
}
245+
246+
mock_get_running_config.side_effect = mock_get_empty_running_config_side_effect
260247

261248
# Instantiate ChangeApplier with a specific scope to simulate applying changes in a multi-asic environment
262249
applier = generic_config_updater.change_applier.ChangeApplier(scope="asic0")

0 commit comments

Comments
 (0)