diff --git a/config/main.py b/config/main.py index 16aea6b610..47791cf69d 100644 --- a/config/main.py +++ b/config/main.py @@ -1625,16 +1625,25 @@ def load_mgmt_config(filename): config_data = parse_device_desc_xml(filename) hostname = config_data['DEVICE_METADATA']['localhost']['hostname'] _change_hostname(hostname) - mgmt_conf = netaddr.IPNetwork(list(config_data['MGMT_INTERFACE'].keys())[0][1]) - gw_addr = list(config_data['MGMT_INTERFACE'].values())[0]['gwaddr'] - command = "ifconfig eth0 {} netmask {}".format(str(mgmt_conf.ip), str(mgmt_conf.netmask)) - clicommon.run_command(command, display_cmd=True) - command = "ip route add default via {} dev eth0 table default".format(gw_addr) - clicommon.run_command(command, display_cmd=True, ignore_error=True) - command = "ip rule add from {} table default".format(str(mgmt_conf.ip)) - clicommon.run_command(command, display_cmd=True, ignore_error=True) - command = "[ -f /var/run/dhclient.eth0.pid ] && kill `cat /var/run/dhclient.eth0.pid` && rm -f /var/run/dhclient.eth0.pid" - clicommon.run_command(command, display_cmd=True, ignore_error=True) + for key in list(config_data['MGMT_INTERFACE'].keys()): + # key: (eth0, ipprefix) + # value: { gwaddr: ip } + mgmt_conf = netaddr.IPNetwork(key[1]) + gw_addr = config_data['MGMT_INTERFACE'][key]['gwaddr'] + if mgmt_conf.version == 4: + command = "ifconfig eth0 {} netmask {}".format(str(mgmt_conf.ip), str(mgmt_conf.netmask)) + clicommon.run_command(command, display_cmd=True) + else: + command = "ifconfig eth0 add {}".format(str(mgmt_conf)) + # Ignore error for IPv6 configuration command due to it not allows config the same IP twice + clicommon.run_command(command, display_cmd=True, ignore_error=True) + command = "ip{} route add default via {} dev eth0 table default".format(" -6" if mgmt_conf.version == 6 else "", gw_addr) + clicommon.run_command(command, display_cmd=True, ignore_error=True) + command = "ip{} rule add from {} table default".format(" -6" if mgmt_conf.version == 6 else "", str(mgmt_conf.ip)) + clicommon.run_command(command, display_cmd=True, ignore_error=True) + if len(config_data['MGMT_INTERFACE'].keys()) > 0: + command = "[ -f /var/run/dhclient.eth0.pid ] && kill `cat /var/run/dhclient.eth0.pid` && rm -f /var/run/dhclient.eth0.pid" + clicommon.run_command(command, display_cmd=True, ignore_error=True) click.echo("Please note loaded setting will be lost after system reboot. To preserve setting, run `config save`.") @config.command("load_minigraph") diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index ebba414a53..4b78da135b 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -5192,7 +5192,7 @@ When user specifies the optional argument "-f" or "--force", this command ignore This command is used to reconfigure hostname and mgmt interface based on device description file. This command either uses the optional file specified as arguement or looks for the file "/etc/sonic/device_desc.xml". -If the file does not exist or if the file does not have valid fields for "hostname" and "ManagementAddress", it fails. +If the file does not exist or if the file does not have valid fields for "hostname" and "ManagementAddress" (or "ManagementAddressV6"), it fails. When user specifies the optional argument "-y" or "--yes", this command forces the loading without prompting the user for confirmation. If the argument is not specified, it prompts the user to confirm whether user really wants to load this configuration file. diff --git a/tests/config_test.py b/tests/config_test.py index 87b66f7e61..c7d7512234 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -6,6 +6,7 @@ import jsonpatch import sys import unittest +import ipaddress from unittest import mock import click @@ -42,6 +43,41 @@ Please note setting loaded from minigraph will be lost after system reboot. To preserve setting, run `config save`. """ +load_mgmt_config_command_ipv4_only_output="""\ +Running command: /usr/local/bin/sonic-cfggen -M device_desc.xml --write-to-db +parse dummy device_desc.xml +change hostname to dummy +Running command: ifconfig eth0 10.0.0.100 netmask 255.255.255.0 +Running command: ip route add default via 10.0.0.1 dev eth0 table default +Running command: ip rule add from 10.0.0.100 table default +Running command: [ -f /var/run/dhclient.eth0.pid ] && kill `cat /var/run/dhclient.eth0.pid` && rm -f /var/run/dhclient.eth0.pid +Please note loaded setting will be lost after system reboot. To preserve setting, run `config save`. +""" + +load_mgmt_config_command_ipv6_only_output="""\ +Running command: /usr/local/bin/sonic-cfggen -M device_desc.xml --write-to-db +parse dummy device_desc.xml +change hostname to dummy +Running command: ifconfig eth0 add fc00:1::32/64 +Running command: ip -6 route add default via fc00:1::1 dev eth0 table default +Running command: ip -6 rule add from fc00:1::32 table default +Running command: [ -f /var/run/dhclient.eth0.pid ] && kill `cat /var/run/dhclient.eth0.pid` && rm -f /var/run/dhclient.eth0.pid +Please note loaded setting will be lost after system reboot. To preserve setting, run `config save`. +""" + +load_mgmt_config_command_ipv4_ipv6_output="""\ +Running command: /usr/local/bin/sonic-cfggen -M device_desc.xml --write-to-db +parse dummy device_desc.xml +change hostname to dummy +Running command: ifconfig eth0 10.0.0.100 netmask 255.255.255.0 +Running command: ip route add default via 10.0.0.1 dev eth0 table default +Running command: ip rule add from 10.0.0.100 table default +Running command: ifconfig eth0 add fc00:1::32/64 +Running command: ip -6 route add default via fc00:1::1 dev eth0 table default +Running command: ip -6 rule add from fc00:1::32 table default +Running command: [ -f /var/run/dhclient.eth0.pid ] && kill `cat /var/run/dhclient.eth0.pid` && rm -f /var/run/dhclient.eth0.pid +Please note loaded setting will be lost after system reboot. To preserve setting, run `config save`. +""" RELOAD_CONFIG_DB_OUTPUT = """\ Running command: rm -rf /tmp/dropstat-* @@ -1356,3 +1392,97 @@ def validate_list_checkpoints_optional_parameter(self, param_args, expected_call self.assertTrue(expected_output in result.output) mock_generic_updater.list_checkpoints.assert_called_once() mock_generic_updater.list_checkpoints.assert_has_calls([expected_call]) + + +class TestConfigLoadMgmtConfig(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + print("SETUP") + + from .mock_tables import mock_single_asic + importlib.reload(mock_single_asic) + + import config.main + importlib.reload(config.main) + + def test_config_load_mgmt_config_ipv4_only(self, get_cmd_module, setup_single_broadcom_asic): + device_desc_result = { + 'DEVICE_METADATA': { + 'localhost': { + 'hostname': 'dummy' + } + }, + 'MGMT_INTERFACE': { + ('eth0', '10.0.0.100/24') : { + 'gwaddr': ipaddress.ip_address(u'10.0.0.1') + } + } + } + self.check_output(get_cmd_module, device_desc_result, load_mgmt_config_command_ipv4_only_output, 5) + + def test_config_load_mgmt_config_ipv6_only(self, get_cmd_module, setup_single_broadcom_asic): + device_desc_result = { + 'DEVICE_METADATA': { + 'localhost': { + 'hostname': 'dummy' + } + }, + 'MGMT_INTERFACE': { + ('eth0', 'FC00:1::32/64') : { + 'gwaddr': ipaddress.ip_address(u'fc00:1::1') + } + } + } + self.check_output(get_cmd_module, device_desc_result, load_mgmt_config_command_ipv6_only_output, 5) + + def test_config_load_mgmt_config_ipv4_ipv6(self, get_cmd_module, setup_single_broadcom_asic): + device_desc_result = { + 'DEVICE_METADATA': { + 'localhost': { + 'hostname': 'dummy' + } + }, + 'MGMT_INTERFACE': { + ('eth0', '10.0.0.100/24') : { + 'gwaddr': ipaddress.ip_address(u'10.0.0.1') + }, + ('eth0', 'FC00:1::32/64') : { + 'gwaddr': ipaddress.ip_address(u'fc00:1::1') + } + } + } + self.check_output(get_cmd_module, device_desc_result, load_mgmt_config_command_ipv4_ipv6_output, 8) + + def check_output(self, get_cmd_module, parse_device_desc_xml_result, expected_output, expected_command_call_count): + def parse_device_desc_xml_side_effect(filename): + print("parse dummy device_desc.xml") + return parse_device_desc_xml_result + def change_hostname_side_effect(hostname): + print("change hostname to {}".format(hostname)) + with mock.patch("utilities_common.cli.run_command", mock.MagicMock(side_effect=mock_run_command_side_effect)) as mock_run_command: + with mock.patch('config.main.parse_device_desc_xml', mock.MagicMock(side_effect=parse_device_desc_xml_side_effect)): + with mock.patch('config.main._change_hostname', mock.MagicMock(side_effect=change_hostname_side_effect)): + (config, show) = get_cmd_module + runner = CliRunner() + with runner.isolated_filesystem(): + with open('device_desc.xml', 'w') as f: + f.write('dummy') + result = runner.invoke(config.config.commands["load_mgmt_config"], ["-y", "device_desc.xml"]) + print(result.exit_code) + print(result.output) + traceback.print_tb(result.exc_info[2]) + assert result.exit_code == 0 + assert "\n".join([l.rstrip() for l in result.output.split('\n')]) == expected_output + assert mock_run_command.call_count == expected_command_call_count + + @classmethod + def teardown_class(cls): + print("TEARDOWN") + os.environ['UTILITIES_UNIT_TESTING'] = "0" + + # 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()