Skip to content

Commit

Permalink
[config]Improve config save cli to save to one file for multiasic (so…
Browse files Browse the repository at this point in the history
…nic-net#3288)

HLD design : sonic-net/SONiC#1684

#### What I did
Add support for config save to one file for multi-aisc.
#### How I did it
Extend support for one file save for multiasic using the below format:
```
{
  "localhost": {/*host config*/},
  "asic0": {/*asic0 config*/},
  ...
  "asicN": {/*asicN config*/}
}
```
#### How to verify it
Unit test and manual test on multiasic platform.
Example running multi:
```
admin@str2-8800-sup-2:~$ sudo config save -y tmp.json
Integrate each ASIC's config into a single JSON file tmp.json.
admin@str2-8800-sup-2:~$ cat tmp.json |more
{
    "localhost": {
        "ACL_TABLE": {
            "NTP_ACL": {
                "policy_desc": "NTP_ACL",
                "services": [
                    "NTP"
...
    "asic0": {
        "AUTO_TECHSUPPORT": {
            "GLOBAL": {
                "available_mem_threshold": "10.0",
```
  • Loading branch information
wen587 authored and arfeigin committed Jun 16, 2024
1 parent c401b57 commit 12add72
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 3 deletions.
27 changes: 25 additions & 2 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,22 @@ def validate_gre_type(ctx, _, value):
except ValueError:
raise click.UsageError("{} is not a valid GRE type".format(value))


def multi_asic_save_config(db, filename):
"""A function to save all asic's config to single file
"""
all_current_config = {}
cfgdb_clients = db.cfgdb_clients

for ns, config_db in cfgdb_clients.items():
current_config = config_db.get_config()
sonic_cfggen.FormatConverter.to_serialized(current_config)
asic_name = "localhost" if ns == DEFAULT_NAMESPACE else ns
all_current_config[asic_name] = sort_dict(current_config)
click.echo("Integrate each ASIC's config into a single JSON file {}.".format(filename))
with open(filename, 'w') as file:
json.dump(all_current_config, file, indent=4)

# Function to apply patch for a single ASIC.
def apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path):
scope, changes = scope_changes
Expand Down Expand Up @@ -1276,7 +1292,8 @@ def config(ctx):
@click.option('-y', '--yes', is_flag=True, callback=_abort_if_false,
expose_value=False, prompt='Existing files will be overwritten, continue?')
@click.argument('filename', required=False)
def save(filename):
@clicommon.pass_db
def save(db, filename):
"""Export current config DB to a file on disk.\n
<filename> : Names of configuration file(s) to save, separated by comma with no spaces in between
"""
Expand All @@ -1291,7 +1308,13 @@ def save(filename):
if filename is not None:
cfg_files = filename.split(',')

if len(cfg_files) != num_cfg_file:
# If only one filename is provided in multi-ASIC mode,
# save all ASIC configurations to that single file.
if len(cfg_files) == 1 and multi_asic.is_multi_asic():
filename = cfg_files[0]
multi_asic_save_config(db, filename)
return
elif len(cfg_files) != num_cfg_file:
click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file))
return

Expand Down
5 changes: 5 additions & 0 deletions tests/config_save_output/all_config_db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"localhost": {},
"asic0": {},
"asic1": {}
}
211 changes: 210 additions & 1 deletion tests/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,30 @@
Reloading Monit configuration ...
"""

save_config_output = """\
Running command: /usr/local/bin/sonic-cfggen -d --print-data > /etc/sonic/config_db.json
"""

save_config_filename_output = """\
Running command: /usr/local/bin/sonic-cfggen -d --print-data > /tmp/config_db.json
"""

save_config_masic_output = """\
Running command: /usr/local/bin/sonic-cfggen -d --print-data > /etc/sonic/config_db.json
Running command: /usr/local/bin/sonic-cfggen -n asic0 -d --print-data > /etc/sonic/config_db0.json
Running command: /usr/local/bin/sonic-cfggen -n asic1 -d --print-data > /etc/sonic/config_db1.json
"""

save_config_filename_masic_output = """\
Running command: /usr/local/bin/sonic-cfggen -d --print-data > config_db.json
Running command: /usr/local/bin/sonic-cfggen -n asic0 -d --print-data > config_db0.json
Running command: /usr/local/bin/sonic-cfggen -n asic1 -d --print-data > config_db1.json
"""

save_config_onefile_masic_output = """\
Integrate each ASIC's config into a single JSON file /tmp/all_config_db.json.
"""

config_temp = {
"scope": {
"ACL_TABLE": {
Expand Down Expand Up @@ -333,6 +357,191 @@ def test_plattform_fw_update(self, mock_check_call):
assert result.exit_code == 0
mock_check_call.assert_called_with(["fwutil", "update", 'update', 'module', 'Module1', 'component', 'BIOS', 'fw'])


class TestConfigSave(object):
@classmethod
def setup_class(cls):
os.environ['UTILITIES_UNIT_TESTING'] = "1"
print("SETUP")
import config.main
importlib.reload(config.main)

def test_config_save(self, get_cmd_module, setup_single_broadcom_asic):
def read_json_file_side_effect(filename):
return {}

with mock.patch("utilities_common.cli.run_command",
mock.MagicMock(side_effect=mock_run_command_side_effect)),\
mock.patch('config.main.read_json_file',
mock.MagicMock(side_effect=read_json_file_side_effect)),\
mock.patch('config.main.open',
mock.MagicMock()):
(config, show) = get_cmd_module

runner = CliRunner()

result = runner.invoke(config.config.commands["save"], ["-y"])

print(result.exit_code)
print(result.output)
traceback.print_tb(result.exc_info[2])

assert result.exit_code == 0
assert "\n".join([li.rstrip() for li in result.output.split('\n')]) == save_config_output

def test_config_save_filename(self, get_cmd_module, setup_single_broadcom_asic):
def read_json_file_side_effect(filename):
return {}

with mock.patch("utilities_common.cli.run_command",
mock.MagicMock(side_effect=mock_run_command_side_effect)),\
mock.patch('config.main.read_json_file',
mock.MagicMock(side_effect=read_json_file_side_effect)),\
mock.patch('config.main.open',
mock.MagicMock()):

(config, show) = get_cmd_module

runner = CliRunner()

output_file = os.path.join(os.sep, "tmp", "config_db.json")
result = runner.invoke(config.config.commands["save"], ["-y", output_file])

print(result.exit_code)
print(result.output)
traceback.print_tb(result.exc_info[2])

assert result.exit_code == 0
assert "\n".join([li.rstrip() for li in result.output.split('\n')]) == save_config_filename_output

@classmethod
def teardown_class(cls):
print("TEARDOWN")
os.environ['UTILITIES_UNIT_TESTING'] = "0"


class TestConfigSaveMasic(object):
@classmethod
def setup_class(cls):
print("SETUP")
os.environ['UTILITIES_UNIT_TESTING'] = "2"
os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "multi_asic"
import config.main
importlib.reload(config.main)
# change to multi asic config
from .mock_tables import dbconnector
from .mock_tables import mock_multi_asic
importlib.reload(mock_multi_asic)
dbconnector.load_namespace_config()

def test_config_save_masic(self):
def read_json_file_side_effect(filename):
return {}

with mock.patch("utilities_common.cli.run_command",
mock.MagicMock(side_effect=mock_run_command_side_effect)),\
mock.patch('config.main.read_json_file',
mock.MagicMock(side_effect=read_json_file_side_effect)),\
mock.patch('config.main.open',
mock.MagicMock()):

runner = CliRunner()

result = runner.invoke(config.config.commands["save"], ["-y"])

print(result.exit_code)
print(result.output)
traceback.print_tb(result.exc_info[2])

assert result.exit_code == 0
assert "\n".join([li.rstrip() for li in result.output.split('\n')]) == save_config_masic_output

def test_config_save_filename_masic(self):
def read_json_file_side_effect(filename):
return {}

with mock.patch("utilities_common.cli.run_command",
mock.MagicMock(side_effect=mock_run_command_side_effect)),\
mock.patch('config.main.read_json_file',
mock.MagicMock(side_effect=read_json_file_side_effect)),\
mock.patch('config.main.open',
mock.MagicMock()):

runner = CliRunner()

result = runner.invoke(
config.config.commands["save"],
["-y", "config_db.json,config_db0.json,config_db1.json"]
)

print(result.exit_code)
print(result.output)
traceback.print_tb(result.exc_info[2])

assert result.exit_code == 0
assert "\n".join([li.rstrip() for li in result.output.split('\n')]) == save_config_filename_masic_output

def test_config_save_filename_wrong_cnt_masic(self):
def read_json_file_side_effect(filename):
return {}

with mock.patch('config.main.read_json_file', mock.MagicMock(side_effect=read_json_file_side_effect)):

runner = CliRunner()

result = runner.invoke(
config.config.commands["save"],
["-y", "config_db.json,config_db0.json"]
)

print(result.exit_code)
print(result.output)
traceback.print_tb(result.exc_info[2])

assert "Input 3 config file(s) separated by comma for multiple files" in result.output

def test_config_save_onefile_masic(self):
def get_config_side_effect():
return {}

with mock.patch('swsscommon.swsscommon.ConfigDBConnector.get_config',
mock.MagicMock(side_effect=get_config_side_effect)):
runner = CliRunner()

output_file = os.path.join(os.sep, "tmp", "all_config_db.json")
print("Saving output in {}".format(output_file))
try:
os.remove(output_file)
except OSError:
pass
result = runner.invoke(
config.config.commands["save"],
["-y", output_file]
)

print(result.exit_code)
print(result.output)
assert result.exit_code == 0
assert "\n".join([li.rstrip() for li in result.output.split('\n')]) == save_config_onefile_masic_output

cwd = os.path.dirname(os.path.realpath(__file__))
expected_result = os.path.join(
cwd, "config_save_output", "all_config_db.json"
)
assert filecmp.cmp(output_file, expected_result, shallow=False)

@classmethod
def teardown_class(cls):
print("TEARDOWN")
os.environ['UTILITIES_UNIT_TESTING'] = "0"
os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = ""
# change back to single asic config
from .mock_tables import dbconnector
from .mock_tables import mock_single_asic
importlib.reload(mock_single_asic)
dbconnector.load_namespace_config()


class TestConfigReload(object):
dummy_cfg_file = os.path.join(os.sep, "tmp", "config.json")

Expand Down Expand Up @@ -2889,4 +3098,4 @@ def teardown_class(cls):
from .mock_tables import dbconnector
from .mock_tables import mock_single_asic
importlib.reload(mock_single_asic)
dbconnector.load_database_config()
dbconnector.load_database_config()

0 comments on commit 12add72

Please sign in to comment.