diff --git a/config/main.py b/config/main.py index 7cfcd79eaa..02a225e2a6 100644 --- a/config/main.py +++ b/config/main.py @@ -1283,16 +1283,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 d353b3cedc..12d6af86e8 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -4635,7 +4635,7 @@ running on the device. 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 52ee7165cd..22b117002c 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -3,6 +3,7 @@ import os import traceback import json +import ipaddress from unittest import mock import click @@ -21,6 +22,42 @@ 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`. +""" + def mock_run_command_side_effect(*args, **kwargs): command = args[0] @@ -238,3 +275,86 @@ def teardown_class(cls): from .mock_tables import mock_single_asic imp.reload(mock_single_asic) dbconnector.load_namespace_config() + +class TestConfigLoadMgmtConfig(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + print("SETUP") + import config.main + imp.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): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + print("TEARDOWN")