From f01749de996fb9114d2d3e08fcd2b1f30488d2b0 Mon Sep 17 00:00:00 2001 From: Junhua Zhai Date: Fri, 22 Jul 2022 01:52:46 +0000 Subject: [PATCH] [macsec] cli multi-namespace support (#11285) Enable multi-asic platform support for macsec cli --- .../cli-plugin-tests/conftest.py | 1 + .../cli-plugin-tests/mock_single_asic.py | 81 +++++++++++++++++++ .../cli-plugin-tests/mock_tables.py | 5 +- .../cli-plugin-tests/test_config_macsec.py | 64 +++++++-------- .../cli/config/plugins/macsec.py | 69 +++++++++------- .../cli/show/plugins/show_macsec.py | 56 ++++++++----- 6 files changed, 193 insertions(+), 83 deletions(-) create mode 100644 dockers/docker-macsec/cli-plugin-tests/mock_single_asic.py diff --git a/dockers/docker-macsec/cli-plugin-tests/conftest.py b/dockers/docker-macsec/cli-plugin-tests/conftest.py index e6608ce71265..c7fd38b84179 100644 --- a/dockers/docker-macsec/cli-plugin-tests/conftest.py +++ b/dockers/docker-macsec/cli-plugin-tests/conftest.py @@ -1,5 +1,6 @@ import pytest import mock_tables # lgtm [py/unused-import] +import mock_single_asic # lgtm[py/unused-import] from unittest import mock diff --git a/dockers/docker-macsec/cli-plugin-tests/mock_single_asic.py b/dockers/docker-macsec/cli-plugin-tests/mock_single_asic.py new file mode 100644 index 000000000000..db5642b4652c --- /dev/null +++ b/dockers/docker-macsec/cli-plugin-tests/mock_single_asic.py @@ -0,0 +1,81 @@ +# MONKEY PATCH!!! +from unittest import mock + +from sonic_py_common import multi_asic +from utilities_common import multi_asic as multi_asic_util + +mock_intf_table = { + '': { + 'eth0': { + 2: [{'addr': '10.1.1.1', 'netmask': '255.255.255.0', 'broadcast': '10.1.1.1'}], + 10: [{'addr': '3100::1', 'netmask': 'ffff:ffff:ffff:ffff::/64'}] + }, + 'Ethernet0': { + 17: [{'addr': '82:fd:d1:5b:45:2f', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], + 2: [ + {'addr': '20.1.1.1', 'netmask': '255.255.255.0', 'broadcast': '20.1.1.1'}, + {'addr': '21.1.1.1', 'netmask': '255.255.255.0', 'broadcast': '21.1.1.1'} + ], + 10: [ + {'addr': 'aa00::1', 'netmask': 'ffff:ffff:ffff:ffff::/64'}, + {'addr': '2100::1', 'netmask': 'ffff:ffff:ffff:ffff::/64'}, + {'addr': 'fe80::64be:a1ff:fe85:c6c4%Ethernet0', 'netmask': 'ffff:ffff:ffff:ffff::/64'} + ] + }, + 'PortChannel0001': { + 17: [{'addr': '82:fd:d1:5b:45:2f', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], + 2: [{'addr': '30.1.1.1', 'netmask': '255.255.255.0', 'broadcast': '30.1.1.1'}], + 10: [ + {'addr': 'ab00::1', 'netmask': 'ffff:ffff:ffff:ffff::/64'}, + {'addr': 'fe80::cc8d:60ff:fe08:139f%PortChannel0001', 'netmask': 'ffff:ffff:ffff:ffff::/64'} + ] + }, + 'Vlan100': { + 17: [{'addr': '82:fd:d1:5b:45:2f', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], + 2: [{'addr': '40.1.1.1', 'netmask': '255.255.255.0', 'broadcast': '30.1.1.1'}], + 10: [ + {'addr': 'cc00::1', 'netmask': 'ffff:ffff:ffff:ffff::/64'}, + {'addr': 'fe80::c029:3fff:fe41:cf56%Vlan100', 'netmask': 'ffff:ffff:ffff:ffff::/64'} + ] + }, + 'lo': { + 2: [{'addr': '127.0.0.1', 'netmask': '255.0.0.0', 'broadcast': '127.255.255.255'}], + 10: [{'addr': '::1', 'netmask':'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'}] + } + } +} + + +def mock_get_num_asics(): + return 1 + +def mock_is_multi_asic(): + return False + +def mock_get_namespace_list(namespace=None): + return [''] + + +def mock_single_asic_get_ip_intf_from_ns(namespace): + interfaces = [] + try: + interfaces = list(mock_intf_table[namespace].keys()) + except KeyError: + pass + return interfaces + + +def mock_single_asic_get_ip_intf_addr_from_ns(namespace, iface): + ipaddresses = [] + try: + ipaddresses = mock_intf_table[namespace][iface] + except KeyError: + pass + return ipaddresses + + +multi_asic.is_multi_asic = mock_is_multi_asic +multi_asic.get_num_asics = mock_get_num_asics +multi_asic.get_namespace_list = mock_get_namespace_list +multi_asic_util.multi_asic_get_ip_intf_from_ns = mock_single_asic_get_ip_intf_from_ns +multi_asic_util.multi_asic_get_ip_intf_addr_from_ns = mock_single_asic_get_ip_intf_addr_from_ns diff --git a/dockers/docker-macsec/cli-plugin-tests/mock_tables.py b/dockers/docker-macsec/cli-plugin-tests/mock_tables.py index 3708644bfba2..7580f9929ed5 100644 --- a/dockers/docker-macsec/cli-plugin-tests/mock_tables.py +++ b/dockers/docker-macsec/cli-plugin-tests/mock_tables.py @@ -125,7 +125,10 @@ def __init__(self, db): def get(self, macsec, name): key = self.db.hget("COUNTERS_MACSEC_NAME_MAP", name) - return self.db.get("COUNTERS:" + key) + if key: + fvs = self.db.get("COUNTERS:" + key) + if fvs: return True, fvs + return False, () swsssdk.interface.DBInterface._subscribe_keyspace_notification = _subscribe_keyspace_notification diff --git a/dockers/docker-macsec/cli-plugin-tests/test_config_macsec.py b/dockers/docker-macsec/cli-plugin-tests/test_config_macsec.py index 425ca2afa433..45ab80c7ed12 100644 --- a/dockers/docker-macsec/cli-plugin-tests/test_config_macsec.py +++ b/dockers/docker-macsec/cli-plugin-tests/test_config_macsec.py @@ -2,7 +2,6 @@ from unittest import mock from click.testing import CliRunner -from utilities_common.db import Db sys.path.append('../cli/config/plugins/') import macsec @@ -20,14 +19,13 @@ def test_plugin_registration(self): cli.add_command.assert_called_once_with(macsec.macsec) def test_default_profile(self, mock_cfgdb): + cfgdb = mock_cfgdb runner = CliRunner() - db = Db() - db.cfgdb = mock_cfgdb - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], - [profile_name, "--primary_cak=" + primary_cak,"--primary_ckn=" + primary_ckn], - obj=db) + result = runner.invoke(macsec.macsec, + ["profile", "add", profile_name, "--primary_cak=" + primary_cak,"--primary_ckn=" + primary_ckn], + obj=cfgdb) assert result.exit_code == 0 - profile_table = db.cfgdb.get_entry("MACSEC_PROFILE", profile_name) + profile_table = cfgdb.get_entry("MACSEC_PROFILE", profile_name) assert profile_table assert profile_table["priority"] == "255" assert profile_table["cipher_suite"] == "GCM-AES-128" @@ -39,15 +37,14 @@ def test_default_profile(self, mock_cfgdb): assert profile_table["send_sci"] == "true" assert "rekey_period" not in profile_table - result = runner.invoke(macsec.macsec.commands["profile"].commands["del"], [profile_name], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "del", profile_name], obj=cfgdb) assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info) - profile_table = db.cfgdb.get_entry("MACSEC_PROFILE", profile_name) + profile_table = cfgdb.get_entry("MACSEC_PROFILE", profile_name) assert not profile_table def test_macsec_valid_profile(self, mock_cfgdb): + cfgdb = mock_cfgdb runner = CliRunner() - db = Db() - db.cfgdb = mock_cfgdb profile_name = "test" profile_map = { @@ -67,9 +64,9 @@ def test_macsec_valid_profile(self, mock_cfgdb): if v is not None: options[-1] += "=" + str(v) - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], options, obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add"] + options, obj=cfgdb) assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info) - profile_table = db.cfgdb.get_entry("MACSEC_PROFILE", profile_name) + profile_table = cfgdb.get_entry("MACSEC_PROFILE", profile_name) assert profile_table assert profile_table["priority"] == str(profile_map["priority"]) assert profile_table["cipher_suite"] == profile_map["cipher_suite"] @@ -87,62 +84,65 @@ def test_macsec_valid_profile(self, mock_cfgdb): assert profile_table["rekey_period"] == str(profile_map["rekey_period"]) def test_macsec_invalid_profile(self, mock_cfgdb): + cfgdb = mock_cfgdb runner = CliRunner() - db = Db() - db.cfgdb = mock_cfgdb # Loss primary cak and primary ckn - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], ["test"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add", "test"], obj=cfgdb) assert result.exit_code != 0 # Invalid primary cak - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], ["test", "--primary_cak=abcdfghjk90123456789012345678912","--primary_ckn=01234567890123456789012345678912", "--cipher_suite=GCM-AES-128"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add", "test", + "--primary_cak=abcdfghjk90123456789012345678912","--primary_ckn=01234567890123456789012345678912", + "--cipher_suite=GCM-AES-128"], obj=cfgdb) assert result.exit_code != 0 # Invalid primary cak length - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], ["test", "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912", "--cipher_suite=GCM-AES-256"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add", "test", + "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912", + "--cipher_suite=GCM-AES-256"], obj=cfgdb) assert result.exit_code != 0 def test_macsec_port(self, mock_cfgdb): + cfgdb = mock_cfgdb runner = CliRunner() - db = Db() - db.cfgdb = mock_cfgdb - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], ["test", "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add", "test", + "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912"], + obj=cfgdb) assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info) - result = runner.invoke(macsec.macsec.commands["port"].commands["add"], ["Ethernet0", "test"], obj=db) + result = runner.invoke(macsec.macsec, ["port", "add", "Ethernet0", "test"], obj=cfgdb) assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info) - port_table = db.cfgdb.get_entry("PORT", "Ethernet0") + port_table = cfgdb.get_entry("PORT", "Ethernet0") assert port_table assert port_table["macsec"] == "test" assert port_table["admin_status"] == "up" - result = runner.invoke(macsec.macsec.commands["profile"].commands["del"], ["test"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "del", "test"], obj=cfgdb) assert result.exit_code != 0 - result = runner.invoke(macsec.macsec.commands["port"].commands["del"], ["Ethernet0"], obj=db) + result = runner.invoke(macsec.macsec, ["port", "del", "Ethernet0"], obj=cfgdb) assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info) - port_table = db.cfgdb.get_entry("PORT", "Ethernet0") + port_table = cfgdb.get_entry("PORT", "Ethernet0") assert "macsec" not in port_table or not port_table["macsec"] assert port_table["admin_status"] == "up" def test_macsec_invalid_operation(self, mock_cfgdb): + cfgdb = mock_cfgdb runner = CliRunner() - db = Db() - db.cfgdb = mock_cfgdb # Enable nonexisted profile - result = runner.invoke(macsec.macsec.commands["port"].commands["add"], ["Ethernet0", "test"], obj=db) + result = runner.invoke(macsec.macsec, ["port", "add", "Ethernet0", "test"], obj=cfgdb) assert result.exit_code != 0 # Delete nonexisted profile - result = runner.invoke(macsec.macsec.commands["profile"].commands["del"], ["test"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "del", "test"], obj=cfgdb) assert result.exit_code != 0 - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], ["test", "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add", "test", "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912"], obj=cfgdb) assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info) # Repeat add profile - result = runner.invoke(macsec.macsec.commands["profile"].commands["add"], ["test", "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912"], obj=db) + result = runner.invoke(macsec.macsec, ["profile", "add", "test", "--primary_cak=01234567890123456789012345678912","--primary_ckn=01234567890123456789012345678912"], obj=cfgdb) assert result.exit_code != 0 diff --git a/dockers/docker-macsec/cli/config/plugins/macsec.py b/dockers/docker-macsec/cli/config/plugins/macsec.py index b76de8c98c7f..1b820dfd0e99 100644 --- a/dockers/docker-macsec/cli/config/plugins/macsec.py +++ b/dockers/docker-macsec/cli/config/plugins/macsec.py @@ -1,13 +1,27 @@ import click import utilities_common.cli as clicommon +from sonic_py_common import multi_asic +from swsscommon.swsscommon import ConfigDBConnector +from utilities_common.constants import DEFAULT_NAMESPACE +from utilities_common.db import Db # # 'macsec' group ('config macsec ...') # @click.group(cls=clicommon.AbbreviationGroup, name='macsec') -def macsec(): +# TODO add "hidden=True if this is a single ASIC platform, once we have click 7.0 in all branches. +@click.option('-n', '--namespace', help='Namespace name', + required=True if multi_asic.is_multi_asic() else False, type=click.Choice(multi_asic.get_namespace_list())) +@click.pass_context +def macsec(ctx, namespace): """MACsec-related configuration tasks""" - pass + if not ctx.obj or isinstance(ctx.obj, Db): + # Set namespace to default_namespace if it is None. + if namespace is None: + namespace = DEFAULT_NAMESPACE + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=str(namespace)) + config_db.connect() + ctx.obj = config_db # @@ -24,31 +38,29 @@ def macsec_port(): @macsec_port.command('add') @click.argument('port', metavar='', required=True) @click.argument('profile', metavar='', required=True) -@clicommon.pass_db -def add_port(db, port, profile): +def add_port(port, profile): """ Add MACsec port """ ctx = click.get_current_context() + config_db = ctx.obj if clicommon.get_interface_naming_mode() == "alias": - alias = port - iface_alias_converter = clicommon.InterfaceAliasConverter(db) - port = iface_alias_converter.alias_to_name(alias) + port = interface_alias_to_name(config_db, port) if port is None: - ctx.fail("cannot find port name for alias {}".format(alias)) + ctx.fail("cannot find port name for alias {}".format(port)) - profile_entry = db.cfgdb.get_entry('MACSEC_PROFILE', profile) + profile_entry = config_db.get_entry('MACSEC_PROFILE', profile) if len(profile_entry) == 0: ctx.fail("profile {} doesn't exist".format(profile)) - port_entry = db.cfgdb.get_entry('PORT', port) + port_entry = config_db.get_entry('PORT', port) if len(port_entry) == 0: ctx.fail("port {} doesn't exist".format(port)) port_entry['macsec'] = profile - db.cfgdb.set_entry("PORT", port, port_entry) + config_db.set_entry("PORT", port, port_entry) # @@ -56,27 +68,25 @@ def add_port(db, port, profile): # @macsec_port.command('del') @click.argument('port', metavar='', required=True) -@clicommon.pass_db -def del_port(db, port): +def del_port(port): """ Delete MACsec port """ ctx = click.get_current_context() + config_db = ctx.obj if clicommon.get_interface_naming_mode() == "alias": - alias = port - iface_alias_converter = clicommon.InterfaceAliasConverter(db) - port = iface_alias_converter.alias_to_name(alias) + port = interface_alias_to_name(config_db, port) if port is None: - ctx.fail("cannot find port name for alias {}".format(alias)) + ctx.fail("cannot find port name for alias {}".format(port)) - port_entry = db.cfgdb.get_entry('PORT', port) + port_entry = config_db.get_entry('PORT', port) if len(port_entry) == 0: ctx.fail("port {} doesn't exist".format(port)) del port_entry['macsec'] - db.cfgdb.set_entry("PORT", port, port_entry) + config_db.set_entry("PORT", port, port_entry) # @@ -109,13 +119,14 @@ def is_hexstring(hexstring: str): @click.option('--replay_window', metavar='', required=False, default=0, show_default=True, type=click.IntRange(0, 2**32), help="Replay window size that is the number of packets that could be out of order. This field works only if ENABLE_REPLAY_PROTECT is true.") @click.option('--send_sci/--no_send_sci', metavar='', required=False, default=True, show_default=True, is_flag=True, help="Send SCI in SecTAG field of MACsec header.") @click.option('--rekey_period', metavar='', required=False, default=0, show_default=True, type=click.IntRange(min=0), help="The period of proactively refresh (Unit second).") -@clicommon.pass_db -def add_profile(db, profile, priority, cipher_suite, primary_cak, primary_ckn, policy, enable_replay_protect, replay_window, send_sci, rekey_period): +def add_profile(profile, priority, cipher_suite, primary_cak, primary_ckn, policy, enable_replay_protect, replay_window, send_sci, rekey_period): """ Add MACsec profile """ ctx = click.get_current_context() - profile_entry = db.cfgdb.get_entry('MACSEC_PROFILE', profile) + config_db = ctx.obj + + profile_entry = config_db.get_entry('MACSEC_PROFILE', profile) if not len(profile_entry) == 0: ctx.fail("{} already exists".format(profile)) @@ -157,7 +168,7 @@ def add_profile(db, profile, priority, cipher_suite, primary_cak, primary_ckn, p profile_table[k] = "false" else: profile_table[k] = str(v) - db.cfgdb.set_entry("MACSEC_PROFILE", profile, profile_table) + config_db.set_entry("MACSEC_PROFILE", profile, profile_table) # @@ -165,24 +176,24 @@ def add_profile(db, profile, priority, cipher_suite, primary_cak, primary_ckn, p # @macsec_profile.command('del') @click.argument('profile', metavar='', required=True) -@clicommon.pass_db -def del_profile(db, profile): +def del_profile( profile): """ Delete MACsec profile """ ctx = click.get_current_context() + config_db = ctx.obj - profile_entry = db.cfgdb.get_entry('MACSEC_PROFILE', profile) + profile_entry = config_db.get_entry('MACSEC_PROFILE', profile) if len(profile_entry) == 0: ctx.fail("{} doesn't exist".format(profile)) # Check if the profile is being used by any port - for port in db.cfgdb.get_keys('PORT'): - attr = db.cfgdb.get_entry('PORT', port) + for port in config_db.get_keys('PORT'): + attr = config_db.get_entry('PORT', port) if 'macsec' in attr and attr['macsec'] == profile: ctx.fail("{} is being used by port {}, Please remove the MACsec from the port firstly".format(profile, port)) - db.cfgdb.set_entry("MACSEC_PROFILE", profile, None) + config_db.set_entry("MACSEC_PROFILE", profile, None) def register(cli): diff --git a/dockers/docker-macsec/cli/show/plugins/show_macsec.py b/dockers/docker-macsec/cli/show/plugins/show_macsec.py index 0d32f7e96249..3f1058df3572 100644 --- a/dockers/docker-macsec/cli/show/plugins/show_macsec.py +++ b/dockers/docker-macsec/cli/show/plugins/show_macsec.py @@ -4,22 +4,19 @@ import click from tabulate import tabulate -from swsscommon.swsscommon import SonicV2Connector +import utilities_common.multi_asic as multi_asic_util from swsscommon.swsscommon import CounterTable, MacsecCounter -DB_CONNECTOR = SonicV2Connector(use_unix_socket_path=False) -DB_CONNECTOR.connect(DB_CONNECTOR.APPL_DB) -DB_CONNECTOR.connect(DB_CONNECTOR.COUNTERS_DB) -COUNTER_TABLE = CounterTable(DB_CONNECTOR.get_redis_client(DB_CONNECTOR.COUNTERS_DB)) +DB_CONNECTOR = None +COUNTER_TABLE = None class MACsecAppMeta(object): - SEPARATOR = DB_CONNECTOR.get_db_separator(DB_CONNECTOR.APPL_DB) - def __init__(self, *args) -> None: - key = self.__class__.get_appl_table_name() + MACsecAppMeta.SEPARATOR + \ - MACsecAppMeta.SEPARATOR.join(args) + SEPARATOR = DB_CONNECTOR.get_db_separator(DB_CONNECTOR.APPL_DB) + key = self.__class__.get_appl_table_name() + SEPARATOR + \ + SEPARATOR.join(args) self.meta = DB_CONNECTOR.get_all( DB_CONNECTOR.APPL_DB, key) if len(self.meta) == 0: @@ -184,19 +181,36 @@ def create_macsec_objs(interface_name: str) -> typing.List[MACsecAppMeta]: @click.command() @click.argument('interface_name', required=False) -def macsec(interface_name): - ctx = click.get_current_context() - objs = [] - interface_names = [name.split(":")[1] for name in DB_CONNECTOR.keys(DB_CONNECTOR.APPL_DB, "MACSEC_PORT*")] - if interface_name is not None: - if interface_name not in interface_names: - ctx.fail("Cannot find the port {} in MACsec port lists {}".format(interface_name, interface_names)) - else: +@multi_asic_util.multi_asic_click_options +def macsec(interface_name, namespace, display): + MacsecContext(namespace, display).show(interface_name) + + +class MacsecContext(object): + + def __init__(self, namespace_option, display_option): + self.db = None + self.multi_asic = multi_asic_util.MultiAsic( + display_option, namespace_option) + + @multi_asic_util.run_on_multi_asic + def show(self, interface_name): + global DB_CONNECTOR + global COUNTER_TABLE + DB_CONNECTOR = self.db + COUNTER_TABLE = CounterTable(self.db.get_redis_client(self.db.COUNTERS_DB)) + + interface_names = [name.split(":")[1] for name in self.db.keys(self.db.APPL_DB, "MACSEC_PORT*")] + if interface_name is not None: + if interface_name not in interface_names: + return interface_names = [interface_name] - for interface_name in natsorted(interface_names): - objs += create_macsec_objs(interface_name) - for obj in objs: - print(obj.dump_str()) + + objs = [] + for interface_name in natsorted(interface_names): + objs += create_macsec_objs(interface_name) + for obj in objs: + print(obj.dump_str()) def register(cli):