From b3b6938fda9244607fb00bfd36a74bccab0c38a9 Mon Sep 17 00:00:00 2001 From: Stepan Blyshchak <38952541+stepanblyschak@users.noreply.github.com> Date: Thu, 15 Jul 2021 20:35:56 +0300 Subject: [PATCH] [dhcp-relay] make DHCP relay an extension (#6531) - Why I did it Make DHCP relay docker an extension. DHCP relay now carries dhcp relay commands CLI plugin and has a complete manifest. It is installed as extension if INCLUDE_DHCP_REALY is set to y. DEPENDS on #5939 - How I did it Modify DHCP relay docker makefile and dockerfile. Make changes to sonic_debian_extension.j2 to install sonic packages. I moved DHCP related CLI tests from sonic-utilities to DHCP relay docker. This PR introduces a way to write a plugin as part of docker image and run the tests from cli-plugin-tests directory under docker directory. The test result is available in target/docker-dhcp-relay.gz.log: [ REASON ] : target/docker-dhcp-relay.gz does not exist NON-EXISTENT PREREQUISITES: docker-start target/docker-config-engine-buster.gz-load target/python-wheels/sonic_utilities-1.2-py3-none-any.whl-in stall target/debs/buster/python3-swsscommon_1.0.0_amd64.deb-install [ FLAGS FILE ] : [] [ FLAGS DEPENDS ] : [] [ FLAGS DIFF ] : [] ============================= test session starts ============================== platform linux -- Python 3.7.3, pytest-3.10.1, py-1.7.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /sonic/dockers/docker-dhcp-relay/cli-plugin-tests, inifile: plugins: cov-2.6.0 collecting ... collected 10 items test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_plugin_registration PASSED [ 10%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_add_dhcp_relay_with_nonexist_vlanid PASSED [ 20%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_add_dhcp_relay_with_invalid_vlanid PASSED [ 30%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_add_dhcp_relay_with_invalid_ip PASSED [ 40%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_add_dhcp_relay_with_exist_ip PASSED [ 50%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_add_del_dhcp_relay_dest PASSED [ 60%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_remove_nonexist_dhcp_relay_dest PASSED [ 70%] test_config_dhcp_relay.py::TestConfigVlanDhcpRelay::test_config_vlan_remove_dhcp_relay_dest_with_nonexist_vlanid PASSED [ 80%] test_show_dhcp_relay.py::TestVlanDhcpRelay::test_plugin_registration PASSED [ 90%] test_show_dhcp_relay.py::TestVlanDhcpRelay::test_dhcp_relay_column_output PASSED [100%] =============================== warnings summary =============================== /usr/local/lib/python3.7/dist-packages/tabulate.py:7 /usr/local/lib/python3.7/dist-packages/tabulate.py:7: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working from collections import namedtuple, Iterable -- Docs: https://docs.pytest.org/en/latest/warnings.html ==================== 10 passed, 1 warnings in 0.35 seconds ===================== --- Makefile.work | 1 + dockers/docker-dhcp-relay/Dockerfile.j2 | 1 + .../cli-plugin-tests/conftest.py | 27 +++ .../cli-plugin-tests/mock_tables.py | 154 ++++++++++++++++++ .../cli-plugin-tests/pytest.ini | 3 + .../test_config_dhcp_relay.py | 141 ++++++++++++++++ .../cli-plugin-tests/test_show_dhcp_relay.py | 28 ++++ .../cli/config/plugins/dhcp_relay.py | 84 ++++++++++ .../cli/show/plugins/show_dhcp_relay.py | 20 +++ files/build_templates/init_cfg.json.j2 | 2 +- files/scripts/swss.sh | 2 +- rules/config | 3 + rules/docker-dhcp-relay.mk | 38 ++++- slave.mk | 1 + 14 files changed, 497 insertions(+), 8 deletions(-) create mode 100644 dockers/docker-dhcp-relay/cli-plugin-tests/conftest.py create mode 100644 dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py create mode 100644 dockers/docker-dhcp-relay/cli-plugin-tests/pytest.ini create mode 100644 dockers/docker-dhcp-relay/cli-plugin-tests/test_config_dhcp_relay.py create mode 100644 dockers/docker-dhcp-relay/cli-plugin-tests/test_show_dhcp_relay.py create mode 100644 dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py create mode 100644 dockers/docker-dhcp-relay/cli/show/plugins/show_dhcp_relay.py diff --git a/Makefile.work b/Makefile.work index 3ef51898f..2b025e38d 100644 --- a/Makefile.work +++ b/Makefile.work @@ -270,6 +270,7 @@ SONIC_BUILD_INSTRUCTION := make \ HTTPS_PROXY=$(https_proxy) \ NO_PROXY=$(no_proxy) \ SONIC_INCLUDE_SYSTEM_TELEMETRY=$(INCLUDE_SYSTEM_TELEMETRY) \ + INCLUDE_DHCP_RELAY=$(INCLUDE_DHCP_RELAY) \ SONIC_INCLUDE_RESTAPI=$(INCLUDE_RESTAPI) \ TELEMETRY_WRITABLE=$(TELEMETRY_WRITABLE) \ EXTRA_DOCKER_TARGETS=$(EXTRA_DOCKER_TARGETS) \ diff --git a/dockers/docker-dhcp-relay/Dockerfile.j2 b/dockers/docker-dhcp-relay/Dockerfile.j2 index 1100aa510..578f27c24 100644 --- a/dockers/docker-dhcp-relay/Dockerfile.j2 +++ b/dockers/docker-dhcp-relay/Dockerfile.j2 @@ -32,5 +32,6 @@ COPY ["docker_init.sh", "start.sh", "/usr/bin/"] COPY ["docker-dhcp-relay.supervisord.conf.j2", "port-name-alias-map.txt.j2", "wait_for_intf.sh.j2", "/usr/share/sonic/templates/"] COPY ["files/supervisor-proc-exit-listener", "/usr/bin"] COPY ["critical_processes", "/etc/supervisor"] +COPY ["cli", "/cli/"] ENTRYPOINT ["/usr/bin/docker_init.sh"] diff --git a/dockers/docker-dhcp-relay/cli-plugin-tests/conftest.py b/dockers/docker-dhcp-relay/cli-plugin-tests/conftest.py new file mode 100644 index 000000000..4eb79edd0 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli-plugin-tests/conftest.py @@ -0,0 +1,27 @@ +import pytest +import mock_tables # lgtm [py/unused-import] +from unittest import mock + +@pytest.fixture() +def mock_cfgdb(): + cfgdb = mock.Mock() + CONFIG = { + 'VLAN': { + 'Vlan1000': { + 'dhcp_servers': ['192.0.0.1'] + } + } + } + + def get_entry(table, key): + return CONFIG[table][key] + + def set_entry(table, key, data): + CONFIG[table].setdefault(key, {}) + CONFIG[table][key] = data + + cfgdb.get_entry = mock.Mock(side_effect=get_entry) + cfgdb.set_entry = mock.Mock(side_effect=set_entry) + + yield cfgdb + diff --git a/dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py b/dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py new file mode 100644 index 000000000..650d2cf48 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py @@ -0,0 +1,154 @@ +# MONKEY PATCH!!! +import json +import os +from unittest import mock + +import mockredis +import redis +import swsssdk +from sonic_py_common import multi_asic +from swsssdk import SonicDBConfig, SonicV2Connector, ConfigDBConnector, ConfigDBPipeConnector +from swsscommon import swsscommon + + +topo = None +dedicated_dbs = {} + +def clean_up_config(): + # Set SonicDBConfig variables to initial state + # so that it can be loaded with single or multiple + # namespaces before the test begins. + SonicDBConfig._sonic_db_config = {} + SonicDBConfig._sonic_db_global_config_init = False + SonicDBConfig._sonic_db_config_init = False + +def load_namespace_config(): + # To support multi asic testing + # SonicDBConfig load_sonic_global_db_config + # is invoked to load multiple namespaces + clean_up_config() + SonicDBConfig.load_sonic_global_db_config( + global_db_file_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'database_global.json')) + +def load_database_config(): + # Load local database_config.json for single namespace test scenario + clean_up_config() + SonicDBConfig.load_sonic_db_config( + sonic_db_file_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'database_config.json')) + + +_old_connect_SonicV2Connector = SonicV2Connector.connect + +def connect_SonicV2Connector(self, db_name, retry_on=True): + # add topo to kwargs for testing different topology + self.dbintf.redis_kwargs['topo'] = topo + # add the namespace to kwargs for testing multi asic + self.dbintf.redis_kwargs['namespace'] = self.namespace + # Mock DB filename for unit-test + global dedicated_dbs + if dedicated_dbs and dedicated_dbs.get(db_name): + self.dbintf.redis_kwargs['db_name'] = dedicated_dbs[db_name] + else: + self.dbintf.redis_kwargs['db_name'] = db_name + self.dbintf.redis_kwargs['decode_responses'] = True + _old_connect_SonicV2Connector(self, db_name, retry_on) + +def _subscribe_keyspace_notification(self, db_name, client): + pass + + +def config_set(self, *args): + pass + + +class MockPubSub: + def get_message(self): + return None + + def psubscribe(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return self + + def listen(self): + return [] + + def punsubscribe(self, *args, **kwargs): + pass + + def clear(self): + pass + +INPUT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class SwssSyncClient(mockredis.MockRedis): + def __init__(self, *args, **kwargs): + super(SwssSyncClient, self).__init__(strict=True, *args, **kwargs) + # Namespace is added in kwargs specifically for unit-test + # to identify the file path to load the db json files. + topo = kwargs.pop('topo') + namespace = kwargs.pop('namespace') + db_name = kwargs.pop('db_name') + self.decode_responses = kwargs.pop('decode_responses', False) == True + fname = db_name.lower() + ".json" + self.pubsub = MockPubSub() + + if namespace is not None and namespace is not multi_asic.DEFAULT_NAMESPACE: + fname = os.path.join(INPUT_DIR, namespace, fname) + elif topo is not None: + fname = os.path.join(INPUT_DIR, topo, fname) + else: + fname = os.path.join(INPUT_DIR, fname) + + if os.path.exists(fname): + with open(fname) as f: + js = json.load(f) + for k, v in js.items(): + if 'expireat' in v and 'ttl' in v and 'type' in v and 'value' in v: + # database is in redis-dump format + if v['type'] == 'hash': + # ignore other types for now since sonic has hset keys only in the db + for attr, value in v['value'].items(): + self.hset(k, attr, value) + else: + for attr, value in v.items(): + self.hset(k, attr, value) + + # Patch mockredis/mockredis/client.py + # The offical implementation assume decode_responses=False + # Here we detect the option and decode after doing encode + def _encode(self, value): + "Return a bytestring representation of the value. Taken from redis-py connection.py" + + value = super(SwssSyncClient, self)._encode(value) + + if self.decode_responses: + return value.decode('utf-8') + + # Patch mockredis/mockredis/client.py + # The official implementation will filter out keys with a slash '/' + # ref: https://github.com/locationlabs/mockredis/blob/master/mockredis/client.py + def keys(self, pattern='*'): + """Emulate keys.""" + import fnmatch + import re + + # Make regex out of glob styled pattern. + regex = fnmatch.translate(pattern) + regex = re.compile(regex) + + # Find every key that matches the pattern + return [key for key in self.redis if regex.match(key)] + + +swsssdk.interface.DBInterface._subscribe_keyspace_notification = _subscribe_keyspace_notification +mockredis.MockRedis.config_set = config_set +redis.StrictRedis = SwssSyncClient +SonicV2Connector.connect = connect_SonicV2Connector +swsscommon.SonicV2Connector = SonicV2Connector +swsscommon.ConfigDBConnector = ConfigDBConnector +swsscommon.ConfigDBPipeConnector = ConfigDBPipeConnector diff --git a/dockers/docker-dhcp-relay/cli-plugin-tests/pytest.ini b/dockers/docker-dhcp-relay/cli-plugin-tests/pytest.ini new file mode 100644 index 000000000..b960c46f4 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli-plugin-tests/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --cov-config=.coveragerc --cov --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv + diff --git a/dockers/docker-dhcp-relay/cli-plugin-tests/test_config_dhcp_relay.py b/dockers/docker-dhcp-relay/cli-plugin-tests/test_config_dhcp_relay.py new file mode 100644 index 000000000..8a47f6994 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli-plugin-tests/test_config_dhcp_relay.py @@ -0,0 +1,141 @@ +import os +import sys +import traceback +from unittest import mock + +from click.testing import CliRunner + +from utilities_common.db import Db + +import pytest + +sys.path.append('../cli/config/plugins/') +import dhcp_relay + +config_vlan_add_dhcp_relay_output="""\ +Added DHCP relay destination address 192.0.0.100 to Vlan1000 +Restarting DHCP relay service... +""" + +config_vlan_del_dhcp_relay_output="""\ +Removed DHCP relay destination address 192.0.0.100 from Vlan1000 +Restarting DHCP relay service... +""" + +class TestConfigVlanDhcpRelay(object): + def test_plugin_registration(self): + cli = mock.MagicMock() + dhcp_relay.register(cli) + cli.commands['vlan'].add_command.assert_called_once_with(dhcp_relay.vlan_dhcp_relay) + + def test_config_vlan_add_dhcp_relay_with_nonexist_vlanid(self): + runner = CliRunner() + + with mock.patch('utilities_common.cli.run_command') as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], + ["1001", "192.0.0.100"]) + print(result.exit_code) + print(result.output) + # traceback.print_tb(result.exc_info[2]) + assert result.exit_code != 0 + assert "Error: Vlan1001 doesn't exist" in result.output + assert mock_run_command.call_count == 0 + + def test_config_vlan_add_dhcp_relay_with_invalid_vlanid(self): + runner = CliRunner() + + with mock.patch('utilities_common.cli.run_command') as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], + ["4096", "192.0.0.100"]) + print(result.exit_code) + print(result.output) + # traceback.print_tb(result.exc_info[2]) + assert result.exit_code != 0 + assert "Error: Vlan4096 doesn't exist" in result.output + assert mock_run_command.call_count == 0 + + def test_config_vlan_add_dhcp_relay_with_invalid_ip(self): + runner = CliRunner() + + with mock.patch('utilities_common.cli.run_command') as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], + ["1000", "192.0.0.1000"]) + print(result.exit_code) + print(result.output) + # traceback.print_tb(result.exc_info[2]) + assert result.exit_code != 0 + assert "Error: 192.0.0.1000 is invalid IP address" in result.output + assert mock_run_command.call_count == 0 + + def test_config_vlan_add_dhcp_relay_with_exist_ip(self, mock_cfgdb): + runner = CliRunner() + db = Db() + db.cfgdb = mock_cfgdb + + with mock.patch('utilities_common.cli.run_command') as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], + ["1000", "192.0.0.1"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert "192.0.0.1 is already a DHCP relay destination for Vlan1000" in result.output + assert mock_run_command.call_count == 0 + + def test_config_vlan_add_del_dhcp_relay_dest(self, mock_cfgdb): + runner = CliRunner() + db = Db() + db.cfgdb = mock_cfgdb + + # add new relay dest + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], + ["1000", "192.0.0.100"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == config_vlan_add_dhcp_relay_output + assert mock_run_command.call_count == 3 + db.cfgdb.set_entry.assert_called_once_with('VLAN', 'Vlan1000', {'dhcp_servers': ['192.0.0.1', '192.0.0.100']}) + + db.cfgdb.set_entry.reset_mock() + + # del relay dest + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["del"], + ["1000", "192.0.0.100"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == config_vlan_del_dhcp_relay_output + assert mock_run_command.call_count == 3 + db.cfgdb.set_entry.assert_called_once_with('VLAN', 'Vlan1000', {'dhcp_servers': ['192.0.0.1']}) + + def test_config_vlan_remove_nonexist_dhcp_relay_dest(self, mock_cfgdb): + runner = CliRunner() + db = Db() + db.cfgdb = mock_cfgdb + + with mock.patch('utilities_common.cli.run_command') as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["del"], + ["1000", "192.0.0.100"], obj=db) + print(result.exit_code) + print(result.output) + # traceback.print_tb(result.exc_info[2]) + assert result.exit_code != 0 + assert "Error: 192.0.0.100 is not a DHCP relay destination for Vlan1000" in result.output + assert mock_run_command.call_count == 0 + + def test_config_vlan_remove_dhcp_relay_dest_with_nonexist_vlanid(self, mock_cfgdb): + runner = CliRunner() + db = Db() + db.cfgdb = mock_cfgdb + + with mock.patch('utilities_common.cli.run_command') as mock_run_command: + result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["del"], + ["1001", "192.0.0.1"], obj=Db) + print(result.exit_code) + print(result.output) + # traceback.print_tb(result.exc_info[2]) + assert result.exit_code != 0 + assert "Error: Vlan1001 doesn't exist" in result.output + assert mock_run_command.call_count == 0 diff --git a/dockers/docker-dhcp-relay/cli-plugin-tests/test_show_dhcp_relay.py b/dockers/docker-dhcp-relay/cli-plugin-tests/test_show_dhcp_relay.py new file mode 100644 index 000000000..b8219fcc6 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli-plugin-tests/test_show_dhcp_relay.py @@ -0,0 +1,28 @@ +import os +import sys +import traceback +from unittest import mock + +from click.testing import CliRunner + +import show.vlan as vlan +from utilities_common.db import Db + +sys.path.insert(0, '../cli/show/plugins/') +import show_dhcp_relay + + +class TestVlanDhcpRelay(object): + def test_plugin_registration(self): + cli = mock.MagicMock() + show_dhcp_relay.register(cli) + assert 'DHCP Helper Address' in dict(vlan.VlanBrief.COLUMNS) + + def test_dhcp_relay_column_output(self): + ctx = ( + ({'Vlan100': {'dhcp_servers': ['192.0.0.1', '192.168.0.2']}}, {}, {}), + (), + ) + assert show_dhcp_relay.get_dhcp_helper_address(ctx, 'Vlan100') == '192.0.0.1\n192.168.0.2' + + diff --git a/dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py b/dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py new file mode 100644 index 000000000..e3cdd8304 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py @@ -0,0 +1,84 @@ +import click +import utilities_common.cli as clicommon + +@click.group(cls=clicommon.AbbreviationGroup, name='dhcp_relay') +def vlan_dhcp_relay(): + pass + +@vlan_dhcp_relay.command('add') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('dhcp_relay_destination_ip', metavar='', required=True) +@clicommon.pass_db +def add_vlan_dhcp_relay_destination(db, vid, dhcp_relay_destination_ip): + """ Add a destination IP address to the VLAN's DHCP relay """ + + ctx = click.get_current_context() + + if not clicommon.is_ipaddress(dhcp_relay_destination_ip): + ctx.fail('{} is invalid IP address'.format(dhcp_relay_destination_ip)) + + vlan_name = 'Vlan{}'.format(vid) + vlan = db.cfgdb.get_entry('VLAN', vlan_name) + if len(vlan) == 0: + ctx.fail("{} doesn't exist".format(vlan_name)) + + dhcp_relay_dests = vlan.get('dhcp_servers', []) + if dhcp_relay_destination_ip in dhcp_relay_dests: + click.echo("{} is already a DHCP relay destination for {}".format(dhcp_relay_destination_ip, vlan_name)) + return + + dhcp_relay_dests.append(dhcp_relay_destination_ip) + vlan['dhcp_servers'] = dhcp_relay_dests + db.cfgdb.set_entry('VLAN', vlan_name, vlan) + click.echo("Added DHCP relay destination address {} to {}".format(dhcp_relay_destination_ip, vlan_name)) + try: + click.echo("Restarting DHCP relay service...") + clicommon.run_command("systemctl stop dhcp_relay", display_cmd=False) + clicommon.run_command("systemctl reset-failed dhcp_relay", display_cmd=False) + clicommon.run_command("systemctl start dhcp_relay", display_cmd=False) + except SystemExit as e: + ctx.fail("Restart service dhcp_relay failed with error {}".format(e)) + +@vlan_dhcp_relay.command('del') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('dhcp_relay_destination_ip', metavar='', required=True) +@clicommon.pass_db +def del_vlan_dhcp_relay_destination(db, vid, dhcp_relay_destination_ip): + """ Remove a destination IP address from the VLAN's DHCP relay """ + + ctx = click.get_current_context() + + if not clicommon.is_ipaddress(dhcp_relay_destination_ip): + ctx.fail('{} is invalid IP address'.format(dhcp_relay_destination_ip)) + + vlan_name = 'Vlan{}'.format(vid) + vlan = db.cfgdb.get_entry('VLAN', vlan_name) + if len(vlan) == 0: + ctx.fail("{} doesn't exist".format(vlan_name)) + + dhcp_relay_dests = vlan.get('dhcp_servers', []) + if not dhcp_relay_destination_ip in dhcp_relay_dests: + ctx.fail("{} is not a DHCP relay destination for {}".format(dhcp_relay_destination_ip, vlan_name)) + + dhcp_relay_dests.remove(dhcp_relay_destination_ip) + if len(dhcp_relay_dests) == 0: + del vlan['dhcp_servers'] + else: + vlan['dhcp_servers'] = dhcp_relay_dests + db.cfgdb.set_entry('VLAN', vlan_name, vlan) + click.echo("Removed DHCP relay destination address {} from {}".format(dhcp_relay_destination_ip, vlan_name)) + try: + click.echo("Restarting DHCP relay service...") + clicommon.run_command("systemctl stop dhcp_relay", display_cmd=False) + clicommon.run_command("systemctl reset-failed dhcp_relay", display_cmd=False) + clicommon.run_command("systemctl start dhcp_relay", display_cmd=False) + except SystemExit as e: + ctx.fail("Restart service dhcp_relay failed with error {}".format(e)) + + +def register(cli): + cli.commands['vlan'].add_command(vlan_dhcp_relay) + + +if __name__ == '__main__': + vlan_dhcp_relay() diff --git a/dockers/docker-dhcp-relay/cli/show/plugins/show_dhcp_relay.py b/dockers/docker-dhcp-relay/cli/show/plugins/show_dhcp_relay.py new file mode 100644 index 000000000..ae8c453e4 --- /dev/null +++ b/dockers/docker-dhcp-relay/cli/show/plugins/show_dhcp_relay.py @@ -0,0 +1,20 @@ +from natsort import natsorted +import show.vlan as vlan + +def get_dhcp_helper_address(ctx, vlan): + cfg, _ = ctx + vlan_dhcp_helper_data, _, _ = cfg + vlan_config = vlan_dhcp_helper_data.get(vlan) + if not vlan_config: + return "" + + dhcp_helpers = vlan_config.get('dhcp_servers', []) + + return '\n'.join(natsorted(dhcp_helpers)) + + +vlan.VlanBrief.register_column('DHCP Helper Address', get_dhcp_helper_address) + + +def register(cli): + pass diff --git a/files/build_templates/init_cfg.json.j2 b/files/build_templates/init_cfg.json.j2 index 885482d31..6e0d75518 100644 --- a/files/build_templates/init_cfg.json.j2 +++ b/files/build_templates/init_cfg.json.j2 @@ -47,7 +47,7 @@ "has_per_asic_scope": {% if feature + '@.service' in installer_services.split(' ') %}true{% else %}false{% endif %}, "auto_restart": "{{autorestart}}", {%- if include_kubernetes == "y" %} -{%- if feature in ["dhcp_relay", "lldp", "pmon", "radv", "snmp", "telemetry"] %} +{%- if feature in ["lldp", "pmon", "radv", "snmp", "telemetry"] %} "set_owner": "kube", {% else %} "set_owner": "local", {% endif %} {% endif %} "high_mem_alert": "disabled" diff --git a/files/scripts/swss.sh b/files/scripts/swss.sh index 730d75bea..752e52317 100755 --- a/files/scripts/swss.sh +++ b/files/scripts/swss.sh @@ -1,6 +1,6 @@ #!/bin/bash -DEPENDENT="radv dhcp_relay" +DEPENDENT="radv" MULTI_INST_DEPENDENT="teamd" # Update dependent list based on other packages requirements diff --git a/rules/config b/rules/config index d1fc669ab..32117dec7 100644 --- a/rules/config +++ b/rules/config @@ -139,6 +139,9 @@ INCLUDE_RESTAPI = n # INCLUDE_NAT - build docker-nat for nat support INCLUDE_NAT = y +# INCLUDE_DHCP_RELAY - build and install dhcp-relay package +INCLUDE_DHCP_RELAY = y + # TELEMETRY_WRITABLE - Enable write/config operations via the gNMI interface. # Uncomment to enable: # TELEMETRY_WRITABLE = y diff --git a/rules/docker-dhcp-relay.mk b/rules/docker-dhcp-relay.mk index a4cb3573e..a8b33c5bc 100644 --- a/rules/docker-dhcp-relay.mk +++ b/rules/docker-dhcp-relay.mk @@ -15,17 +15,43 @@ $(DOCKER_DHCP_RELAY)_DBG_IMAGE_PACKAGES = $($(DOCKER_CONFIG_ENGINE_BUSTER)_DBG_I $(DOCKER_DHCP_RELAY)_LOAD_DOCKERS = $(DOCKER_CONFIG_ENGINE_BUSTER) +$(DOCKER_DHCP_RELAY)_INSTALL_PYTHON_WHEELS = $(SONIC_UTILITIES_PY3) +$(DOCKER_DHCP_RELAY)_INSTALL_DEBS = $(PYTHON3_SWSSCOMMON) + $(DOCKER_DHCP_RELAY)_VERSION = 1.0.0 $(DOCKER_DHCP_RELAY)_PACKAGE_NAME = dhcp-relay +$(DOCKER_DHCP_RELAY)_PACKAGE_DEPENDS = database^1.0.0 + +$(DOCKER_DHCP_RELAY)_SERVICE_REQUIRES = updategraph +$(DOCKER_DHCP_RELAY)_SERVICE_AFTER = swss syncd teamd +$(DOCKER_DHCP_RELAY)_SERVICE_BEFORE = ntp-config +$(DOCKER_DHCP_RELAY)_SERVICE_DEPENDENT_OF = swss SONIC_DOCKER_IMAGES += $(DOCKER_DHCP_RELAY) -SONIC_INSTALL_DOCKER_IMAGES += $(DOCKER_DHCP_RELAY) - SONIC_DOCKER_DBG_IMAGES += $(DOCKER_DHCP_RELAY_DBG) -SONIC_INSTALL_DOCKER_DBG_IMAGES += $(DOCKER_DHCP_RELAY_DBG) + +ifeq ($(INCLUDE_KUBERNETES),y) +$(DOCKER_DHCP_RELAY)_DEFAULT_FEATURE_OWNER = kube +endif + +$(DOCKER_DHCP_RELAY)_DEFAULT_FEATURE_STATE_ENABLED = y + +ifeq ($(INCLUDE_DHCP_RELAY),y) +ifeq ($(INSTALL_DEBUG_TOOLS),y) +SONIC_PACKAGES_LOCAL += $(DOCKER_DHCP_RELAY_DBG) +else +SONIC_PACKAGES_LOCAL += $(DOCKER_DHCP_RELAY) +endif +endif $(DOCKER_DHCP_RELAY)_CONTAINER_NAME = dhcp_relay -$(DOCKER_DHCP_RELAY)_RUN_OPT += --privileged -t -$(DOCKER_DHCP_RELAY)_RUN_OPT += -v /etc/sonic:/etc/sonic:ro -$(DOCKER_DHCP_RELAY)_RUN_OPT += -v /usr/share/sonic/scripts:/usr/share/sonic/scripts:ro +$(DOCKER_DHCP_RELAY)_CONTAINER_PRIVILEGED = true +$(DOCKER_DHCP_RELAY)_CONTAINER_VOLUMES += /etc/sonic:/etc/sonic:ro +$(DOCKER_DHCP_RELAY)_CONTAINER_VOLUMES += /usr/share/sonic/scripts:/usr/share/sonic/scripts:ro +$(DOCKER_DHCP_RELAY)_CONTAINER_TMPFS += /tmp/ +$(DOCKER_DHCP_RELAY)_CONTAINER_TMPFS += /var/tmp/ + +$(DOCKER_DHCP_RELAY)_CLI_CONFIG_PLUGIN = /cli/config/plugins/dhcp_relay.py +$(DOCKER_DHCP_RELAY)_CLI_SHOW_PLUGIN = /cli/show/plugins/show_dhcp_relay.py + $(DOCKER_DHCP_RELAY)_FILES += $(SUPERVISOR_PROC_EXIT_LISTENER_SCRIPT) diff --git a/slave.mk b/slave.mk index 107177c65..101091905 100644 --- a/slave.mk +++ b/slave.mk @@ -264,6 +264,7 @@ $(info "ENABLE_HOST_SERVICE_ON_START" : "$(ENABLE_HOST_SERVICE_ON_START)") $(info "INCLUDE_RESTAPI" : "$(INCLUDE_RESTAPI)") $(info "INCLUDE_SFLOW" : "$(INCLUDE_SFLOW)") $(info "INCLUDE_NAT" : "$(INCLUDE_NAT)") +$(info "INCLUDE_DHCP_RELAY" : "$(INCLUDE_DHCP_RELAY)") $(info "INCLUDE_KUBERNETES" : "$(INCLUDE_KUBERNETES)") $(info "INCLUDE_MACSEC" : "$(INCLUDE_MACSEC)") $(info "TELEMETRY_WRITABLE" : "$(TELEMETRY_WRITABLE)")