Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate the get running config way. #3585

Merged
33 changes: 3 additions & 30 deletions generic_config_updater/change_applier.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from swsscommon.swsscommon import ConfigDBConnector
from sonic_py_common import multi_asic
from .gu_common import GenericConfigUpdaterError, genericUpdaterLogging
from .gu_common import get_config_db_as_json

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

def apply(self, change):
run_data = self._get_running_config()
run_data = get_config_db_as_json(self.scope)
upd_data = prune_empty_table(change.apply(copy.deepcopy(run_data)))
upd_keys = defaultdict(dict)

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

ret = self._services_validate(run_data, upd_data, upd_keys)
if not ret:
run_data = self._get_running_config()
run_data = get_config_db_as_json(self.scope)
self.remove_backend_tables_from_config(upd_data)
self.remove_backend_tables_from_config(run_data)
if upd_data != run_data:
Expand All @@ -159,31 +160,3 @@ def apply(self, change):
def remove_backend_tables_from_config(self, data):
for key in self.backend_tables:
data.pop(key, None)

def _get_running_config(self):
_, fname = tempfile.mkstemp(suffix="_changeApplier")

if self.scope:
cmd = ['sonic-cfggen', '-d', '--print-data', '-n', self.scope]
else:
cmd = ['sonic-cfggen', '-d', '--print-data']

with open(fname, "w") as file:
result = subprocess.Popen(cmd, stdout=file, stderr=subprocess.PIPE, text=True)
_, err = result.communicate()

return_code = result.returncode
if return_code:
os.remove(fname)
raise GenericConfigUpdaterError(
f"Failed to get running config for scope: {self.scope}," +
f"Return code: {return_code}, Error: {err}")

run_data = {}
try:
with open(fname, "r") as file:
run_data = json.load(file)
finally:
if os.path.isfile(fname):
os.remove(fname)
return run_data
40 changes: 24 additions & 16 deletions generic_config_updater/gu_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,39 @@ def __eq__(self, other):
return self.patch == other.patch
return False


def get_config_db_as_json(scope=None):
text = get_config_db_as_text(scope=scope)
config_db_json = json.loads(text)
config_db_json.pop("bgpraw", None)
return config_db_json


def get_config_db_as_text(scope=None):
if scope is not None and scope != multi_asic.DEFAULT_NAMESPACE:
cmd = ['sonic-cfggen', '-d', '--print-data', '-n', scope]
else:
cmd = ['sonic-cfggen', '-d', '--print-data']
result = subprocess.Popen(cmd, shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
text, err = result.communicate()
return_code = result.returncode
if return_code:
raise GenericConfigUpdaterError(f"Failed to get running config for namespace: {scope},"
f" Return code: {return_code}, Error: {err}")
return text


class ConfigWrapper:
def __init__(self, yang_dir=YANG_DIR, scope=multi_asic.DEFAULT_NAMESPACE):
self.scope = scope
self.yang_dir = YANG_DIR
self.sonic_yang_with_loaded_models = None

def get_config_db_as_json(self):
text = self._get_config_db_as_text()
config_db_json = json.loads(text)
config_db_json.pop("bgpraw", None)
return config_db_json
return get_config_db_as_json(self.scope)

def _get_config_db_as_text(self):
Copy link
Contributor

@qiluo-msft qiluo-msft Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_config_db_as_text

This function can be removed. #Closed

if self.scope is not None and self.scope != multi_asic.DEFAULT_NAMESPACE:
cmd = ['sonic-cfggen', '-d', '--print-data', '-n', self.scope]
else:
cmd = ['sonic-cfggen', '-d', '--print-data']

result = subprocess.Popen(cmd, shell=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
text, err = result.communicate()
return_code = result.returncode
if return_code: # non-zero means failure
raise GenericConfigUpdaterError(f"Failed to get running config for namespace: {self.scope},"
f" Return code: {return_code}, Error: {err}")
return text
return get_config_db_as_text(self.scope)

def get_sonic_yang_as_json(self):
config_db_json = self.get_config_db_as_json()
Expand Down
25 changes: 11 additions & 14 deletions tests/generic_config_updater/change_applier_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,25 @@
def debug_print(msg):
print(msg)


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

# Extract file name from kwargs if 'stdout' is a file object
stdout = kwargs.get('stdout')
if hasattr(stdout, 'name'):
fname = stdout.name
stdout = kwargs.get('stdout', None)

if stdout is None:
output = json.dumps(running_config, indent=4)
elif isinstance(stdout, int) and stdout == -1:
output = json.dumps(running_config, indent=4)
else:
raise ValueError("stdout is not a file")
raise ValueError("stdout must be set to subprocess.PIPE or omitted for capturing output")

# Write the running configuration to the file specified in stdout
with open(fname, "w") as s:
json.dump(running_config, s, indent=4)

class MockPopen:
def __init__(self):
self.returncode = 0 # Simulate successful command execution
self.returncode = 0

def communicate(self):
return "", "" # Simulate empty stdout and stderr
return output.encode(), "".encode()

return MockPopen()

Expand Down Expand Up @@ -225,7 +222,7 @@ def vlan_validate(old_cfg, new_cfg, keys):

class TestChangeApplier(unittest.TestCase):

@patch("generic_config_updater.change_applier.subprocess.Popen")
@patch("generic_config_updater.gu_common.subprocess.Popen")
@patch("generic_config_updater.change_applier.get_config_db")
@patch("generic_config_updater.change_applier.set_config")
def test_change_apply(self, mock_set, mock_db, mock_subprocess_Popen):
Expand Down
15 changes: 10 additions & 5 deletions tests/generic_config_updater/gcu_feature_patch_application_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
from mock import patch

import generic_config_updater.change_applier
import generic_config_updater.gu_common
import generic_config_updater.patch_sorter as ps
import generic_config_updater.generic_updater as gu
from .gutest_helpers import Files
from generic_config_updater.gu_common import ConfigWrapper, PatchWrapper

running_config = {}



def set_entry(config_db, tbl, key, data):
global running_config
if data != None:
Expand All @@ -26,9 +28,11 @@ def set_entry(config_db, tbl, key, data):
if not running_config[tbl]:
running_config.pop(tbl)

def get_running_config():

def get_running_config(scope="localhost"):
return running_config


class TestFeaturePatchApplication(unittest.TestCase):
def setUp(self):
self.config_wrapper = ConfigWrapper()
Expand Down Expand Up @@ -87,13 +91,13 @@ def create_patch_applier(self, config):
config_wrapper = self.config_wrapper
config_wrapper.get_config_db_as_json = MagicMock(side_effect=get_running_config)
change_applier = generic_config_updater.change_applier.ChangeApplier()
change_applier._get_running_config = MagicMock(side_effect=get_running_config)
patch_wrapper = PatchWrapper(config_wrapper)
return gu.PatchApplier(config_wrapper=config_wrapper, patch_wrapper=patch_wrapper, changeapplier=change_applier)

@patch('generic_config_updater.change_applier.get_config_db_as_json', side_effect=get_running_config)
@patch("generic_config_updater.change_applier.get_config_db")
@patch("generic_config_updater.change_applier.set_config")
def run_single_success_case_applier(self, data, mock_set, mock_db):
def run_single_success_case_applier(self, data, mock_set, mock_db, mock_get_config_db_as_json):
current_config = data["current_config"]
expected_config = data["expected_config"]
patch = jsonpatch.JsonPatch(data["patch"])
Expand Down Expand Up @@ -121,7 +125,8 @@ def run_single_success_case_applier(self, data, mock_set, mock_db):
self.assertEqual(simulated_config, expected_config)

@patch("generic_config_updater.change_applier.get_config_db")
def run_single_failure_case_applier(self, data, mock_db):
@patch('generic_config_updater.change_applier.get_config_db_as_json', side_effect=get_running_config)
def run_single_failure_case_applier(self, data, mock_db, mock_get_config_db_as_json):
current_config = data["current_config"]
patch = jsonpatch.JsonPatch(data["patch"])
expected_error_substrings = data["expected_error_substrings"]
Expand Down
93 changes: 40 additions & 53 deletions tests/generic_config_updater/multiasic_change_applier_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@
import generic_config_updater.gu_common


def mock_get_running_config_side_effect(scope):
print(f"mocked_value_for_{scope}")
return {
"tables": {
"ACL_TABLE": {
"services_to_validate": ["aclservice"],
"validate_commands": ["acl_loader show table"]
},
"PORT": {
"services_to_validate": ["portservice"],
"validate_commands": ["show interfaces status"]
}
},
"services": {
"aclservice": {
"validate_commands": ["acl_loader show table"]
},
"portservice": {
"validate_commands": ["show interfaces status"]
}
}
}


class TestMultiAsicChangeApplier(unittest.TestCase):

@patch('sonic_py_common.multi_asic.is_multi_asic')
Expand Down Expand Up @@ -137,34 +161,15 @@ def test_extract_scope_singleasic(self, mock_is_multi_asic):
except Exception:
assert(not result)

@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
def test_apply_change_default_scope(self, mock_ConfigDBConnector, mock_get_running_config):
# Setup mock for ConfigDBConnector
mock_db = MagicMock()
mock_ConfigDBConnector.return_value = mock_db

# Setup mock for json.load to return some running configuration
mock_get_running_config.return_value = {
"tables": {
"ACL_TABLE": {
"services_to_validate": ["aclservice"],
"validate_commands": ["acl_loader show table"]
},
"PORT": {
"services_to_validate": ["portservice"],
"validate_commands": ["show interfaces status"]
}
},
"services": {
"aclservice": {
"validate_commands": ["acl_loader show table"]
},
"portservice": {
"validate_commands": ["show interfaces status"]
}
}
}
mock_get_running_config.side_effect = mock_get_running_config_side_effect

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

@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
def test_apply_change_given_scope(self, mock_ConfigDBConnector, mock_get_running_config):
# Setup mock for ConfigDBConnector
mock_db = MagicMock()
mock_ConfigDBConnector.return_value = mock_db

# Setup mock for json.load to return some running configuration
mock_get_running_config.return_value = {
"tables": {
"ACL_TABLE": {
"services_to_validate": ["aclservice"],
"validate_commands": ["acl_loader show table"]
},
"PORT": {
"services_to_validate": ["portservice"],
"validate_commands": ["show interfaces status"]
}
},
"services": {
"aclservice": {
"validate_commands": ["acl_loader show table"]
},
"portservice": {
"validate_commands": ["show interfaces status"]
}
}
}
mock_get_running_config.side_effect = mock_get_running_config_side_effect

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

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

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

@patch('generic_config_updater.change_applier.ChangeApplier._get_running_config', autospec=True)
@patch('generic_config_updater.change_applier.get_config_db_as_json', autospec=True)
@patch('generic_config_updater.change_applier.ConfigDBConnector', autospec=True)
def test_apply_patch_with_empty_tables_failure(self, mock_ConfigDBConnector, mock_get_running_config):
# Setup mock for ConfigDBConnector
mock_db = MagicMock()
mock_ConfigDBConnector.return_value = mock_db

# Setup mock for json.load to simulate configuration where crucial tables are unexpectedly empty
mock_get_running_config.return_value = {
"tables": {
# Simulate empty tables or missing crucial configuration
},
"services": {
# Normally, services would be listed here
def mock_get_empty_running_config_side_effect():
return {
"tables": {
# Simulate empty tables or missing crucial configuration
},
"services": {
# Normally, services would be listed here
}
}
}

mock_get_running_config.side_effect = mock_get_empty_running_config_side_effect

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