[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 =====================
This commit is contained in:
parent
323483cf9f
commit
790bdded96
@ -268,6 +268,7 @@ SONIC_BUILD_INSTRUCTION := make \
|
|||||||
HTTPS_PROXY=$(https_proxy) \
|
HTTPS_PROXY=$(https_proxy) \
|
||||||
NO_PROXY=$(no_proxy) \
|
NO_PROXY=$(no_proxy) \
|
||||||
SONIC_INCLUDE_SYSTEM_TELEMETRY=$(INCLUDE_SYSTEM_TELEMETRY) \
|
SONIC_INCLUDE_SYSTEM_TELEMETRY=$(INCLUDE_SYSTEM_TELEMETRY) \
|
||||||
|
INCLUDE_DHCP_RELAY=$(INCLUDE_DHCP_RELAY) \
|
||||||
SONIC_INCLUDE_RESTAPI=$(INCLUDE_RESTAPI) \
|
SONIC_INCLUDE_RESTAPI=$(INCLUDE_RESTAPI) \
|
||||||
TELEMETRY_WRITABLE=$(TELEMETRY_WRITABLE) \
|
TELEMETRY_WRITABLE=$(TELEMETRY_WRITABLE) \
|
||||||
EXTRA_DOCKER_TARGETS=$(EXTRA_DOCKER_TARGETS) \
|
EXTRA_DOCKER_TARGETS=$(EXTRA_DOCKER_TARGETS) \
|
||||||
|
@ -33,5 +33,6 @@ COPY ["docker-dhcp-relay.supervisord.conf.j2", "port-name-alias-map.txt.j2", "wa
|
|||||||
COPY ["dhcp-relay.programs.j2", "dhcpv4-relay.agents.j2", "dhcpv6-relay.agents.j2", "dhcpv6-relay.monitors.j2", "/usr/share/sonic/templates/"]
|
COPY ["dhcp-relay.programs.j2", "dhcpv4-relay.agents.j2", "dhcpv6-relay.agents.j2", "dhcpv6-relay.monitors.j2", "/usr/share/sonic/templates/"]
|
||||||
COPY ["files/supervisor-proc-exit-listener", "/usr/bin"]
|
COPY ["files/supervisor-proc-exit-listener", "/usr/bin"]
|
||||||
COPY ["critical_processes", "/etc/supervisor"]
|
COPY ["critical_processes", "/etc/supervisor"]
|
||||||
|
COPY ["cli", "/cli/"]
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker_init.sh"]
|
ENTRYPOINT ["/usr/bin/docker_init.sh"]
|
||||||
|
27
dockers/docker-dhcp-relay/cli-plugin-tests/conftest.py
Normal file
27
dockers/docker-dhcp-relay/cli-plugin-tests/conftest.py
Normal file
@ -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
|
||||||
|
|
154
dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py
Normal file
154
dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py
Normal file
@ -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
|
3
dockers/docker-dhcp-relay/cli-plugin-tests/pytest.ini
Normal file
3
dockers/docker-dhcp-relay/cli-plugin-tests/pytest.ini
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = --cov-config=.coveragerc --cov --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv
|
||||||
|
|
@ -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
|
@ -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'
|
||||||
|
|
||||||
|
|
84
dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py
Normal file
84
dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py
Normal file
@ -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='<vid>', required=True, type=int)
|
||||||
|
@click.argument('dhcp_relay_destination_ip', metavar='<dhcp_relay_destination_ip>', 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='<vid>', required=True, type=int)
|
||||||
|
@click.argument('dhcp_relay_destination_ip', metavar='<dhcp_relay_destination_ip>', 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()
|
@ -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
|
@ -47,7 +47,7 @@
|
|||||||
"has_per_asic_scope": {% if feature + '@.service' in installer_services.split(' ') %}true{% else %}false{% endif %},
|
"has_per_asic_scope": {% if feature + '@.service' in installer_services.split(' ') %}true{% else %}false{% endif %},
|
||||||
"auto_restart": "{{autorestart}}",
|
"auto_restart": "{{autorestart}}",
|
||||||
{%- if include_kubernetes == "y" %}
|
{%- 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": "kube", {% else %}
|
||||||
"set_owner": "local", {% endif %} {% endif %}
|
"set_owner": "local", {% endif %} {% endif %}
|
||||||
"high_mem_alert": "disabled"
|
"high_mem_alert": "disabled"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
DEPENDENT="radv dhcp_relay"
|
DEPENDENT="radv"
|
||||||
MULTI_INST_DEPENDENT="teamd"
|
MULTI_INST_DEPENDENT="teamd"
|
||||||
|
|
||||||
# Update dependent list based on other packages requirements
|
# Update dependent list based on other packages requirements
|
||||||
|
@ -139,6 +139,9 @@ INCLUDE_RESTAPI = n
|
|||||||
# INCLUDE_NAT - build docker-nat for nat support
|
# INCLUDE_NAT - build docker-nat for nat support
|
||||||
INCLUDE_NAT = y
|
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.
|
# TELEMETRY_WRITABLE - Enable write/config operations via the gNMI interface.
|
||||||
# Uncomment to enable:
|
# Uncomment to enable:
|
||||||
# TELEMETRY_WRITABLE = y
|
# TELEMETRY_WRITABLE = y
|
||||||
|
@ -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)_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)_VERSION = 1.0.0
|
||||||
$(DOCKER_DHCP_RELAY)_PACKAGE_NAME = dhcp-relay
|
$(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_DOCKER_IMAGES += $(DOCKER_DHCP_RELAY)
|
||||||
SONIC_INSTALL_DOCKER_IMAGES += $(DOCKER_DHCP_RELAY)
|
|
||||||
|
|
||||||
SONIC_DOCKER_DBG_IMAGES += $(DOCKER_DHCP_RELAY_DBG)
|
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)_CONTAINER_NAME = dhcp_relay
|
||||||
$(DOCKER_DHCP_RELAY)_RUN_OPT += --privileged -t
|
$(DOCKER_DHCP_RELAY)_CONTAINER_PRIVILEGED = true
|
||||||
$(DOCKER_DHCP_RELAY)_RUN_OPT += -v /etc/sonic:/etc/sonic:ro
|
$(DOCKER_DHCP_RELAY)_CONTAINER_VOLUMES += /etc/sonic:/etc/sonic:ro
|
||||||
$(DOCKER_DHCP_RELAY)_RUN_OPT += -v /usr/share/sonic/scripts:/usr/share/sonic/scripts: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)
|
$(DOCKER_DHCP_RELAY)_FILES += $(SUPERVISOR_PROC_EXIT_LISTENER_SCRIPT)
|
||||||
|
1
slave.mk
1
slave.mk
@ -256,6 +256,7 @@ $(info "ENABLE_HOST_SERVICE_ON_START" : "$(ENABLE_HOST_SERVICE_ON_START)")
|
|||||||
$(info "INCLUDE_RESTAPI" : "$(INCLUDE_RESTAPI)")
|
$(info "INCLUDE_RESTAPI" : "$(INCLUDE_RESTAPI)")
|
||||||
$(info "INCLUDE_SFLOW" : "$(INCLUDE_SFLOW)")
|
$(info "INCLUDE_SFLOW" : "$(INCLUDE_SFLOW)")
|
||||||
$(info "INCLUDE_NAT" : "$(INCLUDE_NAT)")
|
$(info "INCLUDE_NAT" : "$(INCLUDE_NAT)")
|
||||||
|
$(info "INCLUDE_DHCP_RELAY" : "$(INCLUDE_DHCP_RELAY)")
|
||||||
$(info "INCLUDE_KUBERNETES" : "$(INCLUDE_KUBERNETES)")
|
$(info "INCLUDE_KUBERNETES" : "$(INCLUDE_KUBERNETES)")
|
||||||
$(info "INCLUDE_MACSEC" : "$(INCLUDE_MACSEC)")
|
$(info "INCLUDE_MACSEC" : "$(INCLUDE_MACSEC)")
|
||||||
$(info "TELEMETRY_WRITABLE" : "$(TELEMETRY_WRITABLE)")
|
$(info "TELEMETRY_WRITABLE" : "$(TELEMETRY_WRITABLE)")
|
||||||
|
Loading…
Reference in New Issue
Block a user