From 0a33344a5f08c268d1b834872abfdb4c5004966e Mon Sep 17 00:00:00 2001 From: Nazarii Hnydyn Date: Wed, 4 Jan 2023 14:33:38 +0200 Subject: [PATCH 1/2] [hash]: Implement GH frontend. Signed-off-by: Nazarii Hnydyn --- config/plugins/sonic-hash.py | 273 ++++++++++++++++++ show/plugins/sonic-hash.py | 155 ++++++++++ tests/hash_input/assert_show_output.py | 158 ++++++++++ tests/hash_input/mock_config/ecmp.json | 5 + .../hash_input/mock_config/ecmp_and_lag.json | 6 + tests/hash_input/mock_config/empty.json | 5 + tests/hash_input/mock_config/lag.json | 5 + tests/hash_input/mock_state/ecmp.json | 7 + tests/hash_input/mock_state/ecmp_and_lag.json | 7 + tests/hash_input/mock_state/empty.json | 7 + tests/hash_input/mock_state/lag.json | 7 + .../mock_state/no_capabilities.json | 7 + .../hash_input/mock_state/not_applicable.json | 7 + tests/hash_test.py | 244 ++++++++++++++++ utilities_common/switch_hash.py | 64 ++++ 15 files changed, 957 insertions(+) create mode 100644 config/plugins/sonic-hash.py create mode 100644 show/plugins/sonic-hash.py create mode 100644 tests/hash_input/assert_show_output.py create mode 100644 tests/hash_input/mock_config/ecmp.json create mode 100644 tests/hash_input/mock_config/ecmp_and_lag.json create mode 100644 tests/hash_input/mock_config/empty.json create mode 100644 tests/hash_input/mock_config/lag.json create mode 100644 tests/hash_input/mock_state/ecmp.json create mode 100644 tests/hash_input/mock_state/ecmp_and_lag.json create mode 100644 tests/hash_input/mock_state/empty.json create mode 100644 tests/hash_input/mock_state/lag.json create mode 100644 tests/hash_input/mock_state/no_capabilities.json create mode 100644 tests/hash_input/mock_state/not_applicable.json create mode 100644 tests/hash_test.py create mode 100644 utilities_common/switch_hash.py diff --git a/config/plugins/sonic-hash.py b/config/plugins/sonic-hash.py new file mode 100644 index 0000000000..1d3c65c537 --- /dev/null +++ b/config/plugins/sonic-hash.py @@ -0,0 +1,273 @@ +""" +This CLI plugin was auto-generated by using 'sonic-cli-gen' utility +""" + +import click +import utilities_common.cli as clicommon + +from sonic_py_common import logger +from utilities_common.switch_hash import ( + CFG_SWITCH_HASH, + STATE_SWITCH_CAPABILITY, + SW_CAP_HASH_FIELD_LIST_KEY, + SW_CAP_ECMP_HASH_KEY, + SW_CAP_LAG_HASH_KEY, + SW_HASH_KEY, + SW_CAP_KEY, + HASH_FIELD_LIST, + SYSLOG_IDENTIFIER, + get_param, + get_param_hint, + get_dupes, + to_str, +) + + +log = logger.Logger(SYSLOG_IDENTIFIER) +log.set_min_log_priority_info() + +# +# Hash validators ----------------------------------------------------------------------------------------------------- +# + +def hash_field_validator(ctx, param, value): + """ + Check if hash field list argument is valid + Args: + ctx: click context + param: click parameter context + value: value of parameter + Returns: + str: validated parameter + """ + + for hash_field in value: + click.Choice(HASH_FIELD_LIST).convert(hash_field, param, ctx) + + return list(value) + + +def ecmp_hash_validator(ctx, db, ecmp_hash): + """ + Check if ECMP hash argument is valid + + Args: + ctx: click context + db: State DB connector object + ecmp_hash: ECMP hash field list + """ + + dup_list = get_dupes(ecmp_hash) + if dup_list: + raise click.UsageError("Invalid value for {}: {} has a duplicate hash field(s) {}".format( + get_param_hint(ctx, "ecmp_hash"), to_str(ecmp_hash), to_str(dup_list)), ctx + ) + + entry = db.get_all(db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, SW_CAP_KEY)) + + entry.setdefault(SW_CAP_HASH_FIELD_LIST_KEY, 'N/A') + entry.setdefault(SW_CAP_ECMP_HASH_KEY, 'false') + + if entry[SW_CAP_ECMP_HASH_KEY] == 'false': + raise click.UsageError("Failed to configure {}: operation is not supported".format( + get_param_hint(ctx, "ecmp_hash")), ctx + ) + + if not entry[SW_CAP_HASH_FIELD_LIST_KEY]: + raise click.UsageError("Failed to configure {}: no hash field capabilities".format( + get_param_hint(ctx, "ecmp_hash")), ctx + ) + + if entry[SW_CAP_HASH_FIELD_LIST_KEY] == 'N/A': + return + + cap_list = entry[SW_CAP_HASH_FIELD_LIST_KEY].split(',') + + for hash_field in ecmp_hash: + click.Choice(cap_list).convert(hash_field, get_param(ctx, "ecmp_hash"), ctx) + + +def lag_hash_validator(ctx, db, lag_hash): + """ + Check if LAG hash argument is valid + + Args: + ctx: click context + db: State DB connector object + lag_hash: LAG hash field list + """ + + dup_list = get_dupes(lag_hash) + if dup_list: + raise click.UsageError("Invalid value for {}: {} has a duplicate hash field(s) {}".format( + get_param_hint(ctx, "lag_hash"), to_str(lag_hash), to_str(dup_list)), ctx + ) + + entry = db.get_all(db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, SW_CAP_KEY)) + + entry.setdefault(SW_CAP_HASH_FIELD_LIST_KEY, 'N/A') + entry.setdefault(SW_CAP_LAG_HASH_KEY, 'false') + + if entry[SW_CAP_LAG_HASH_KEY] == 'false': + raise click.UsageError("Failed to configure {}: operation is not supported".format( + get_param_hint(ctx, "lag_hash")), ctx + ) + + if not entry[SW_CAP_HASH_FIELD_LIST_KEY]: + raise click.UsageError("Failed to configure {}: no hash field capabilities".format( + get_param_hint(ctx, "lag_hash")), ctx + ) + + if entry[SW_CAP_HASH_FIELD_LIST_KEY] == 'N/A': + return + + cap_list = entry[SW_CAP_HASH_FIELD_LIST_KEY].split(',') + + for hash_field in lag_hash: + click.Choice(cap_list).convert(hash_field, get_param(ctx, "lag_hash"), ctx) + +# +# Hash DB interface --------------------------------------------------------------------------------------------------- +# + +def update_entry_validated(db, table, key, data, create_if_not_exists=False): + """ Update entry in table and validate configuration. + If attribute value in data is None, the attribute is deleted. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector object. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + data (Dict): Entry data. + create_if_not_exists (bool): + In case entry does not exists already a new entry + is not created if this flag is set to False and + creates a new entry if flag is set to True. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + + if not data: + raise click.ClickException(f"No field/values to update {key}") + + if create_if_not_exists: + cfg[table].setdefault(key, {}) + + if key not in cfg[table]: + raise click.ClickException(f"{key} does not exist") + + entry_changed = False + for attr, value in data.items(): + if value == cfg[table][key].get(attr): + continue + entry_changed = True + if value is None: + cfg[table][key].pop(attr, None) + else: + cfg[table][key][attr] = value + + if not entry_changed: + return + + db.set_entry(table, key, cfg[table][key]) + +# +# Hash CLI ------------------------------------------------------------------------------------------------------------ +# + +@click.group( + name="switch-hash", + cls=clicommon.AliasedGroup +) +def SWITCH_HASH(): + """ Configure switch hash feature """ + + pass + + +@SWITCH_HASH.group( + name="global", + cls=clicommon.AliasedGroup +) +def SWITCH_HASH_GLOBAL(): + """ Configure switch hash global """ + + pass + + +@SWITCH_HASH_GLOBAL.command( + name="ecmp-hash" +) +@click.argument( + "ecmp-hash", + nargs=-1, + required=True, + callback=hash_field_validator, +) +@clicommon.pass_db +@click.pass_context +def SWITCH_HASH_GLOBAL_ecmp_hash(ctx, db, ecmp_hash): + """ Hash fields for hashing packets going through ECMP """ + + ecmp_hash_validator(ctx, db.db, ecmp_hash) + + table = CFG_SWITCH_HASH + key = SW_HASH_KEY + data = { + "ecmp_hash": ecmp_hash, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured switch global ECMP hash: {}".format(to_str(ecmp_hash))) + except Exception as e: + log.log_error("Failed to configure switch global ECMP hash: {}".format(str(e))) + ctx.fail(str(e)) + + +@SWITCH_HASH_GLOBAL.command( + name="lag-hash" +) +@click.argument( + "lag-hash", + nargs=-1, + required=True, + callback=hash_field_validator, +) +@clicommon.pass_db +@click.pass_context +def SWITCH_HASH_GLOBAL_lag_hash(ctx, db, lag_hash): + """ Hash fields for hashing packets going through LAG """ + + lag_hash_validator(ctx, db.db, lag_hash) + + table = CFG_SWITCH_HASH + key = SW_HASH_KEY + data = { + "lag_hash": lag_hash, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured switch global LAG hash: {}".format(to_str(lag_hash))) + except Exception as err: + log.log_error("Failed to configure switch global LAG hash: {}".format(str(err))) + ctx.fail(str(err)) + + +def register(cli): + """ Register new CLI nodes in root CLI. + + Args: + cli: Root CLI node. + Raises: + Exception: when root CLI already has a command + we are trying to register. + """ + cli_node = SWITCH_HASH + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command(SWITCH_HASH) diff --git a/show/plugins/sonic-hash.py b/show/plugins/sonic-hash.py new file mode 100644 index 0000000000..467b124c32 --- /dev/null +++ b/show/plugins/sonic-hash.py @@ -0,0 +1,155 @@ +""" +This CLI plugin was auto-generated by using 'sonic-cli-gen' utility +""" + +import click +import tabulate +import utilities_common.cli as clicommon + +from utilities_common.switch_hash import ( + CFG_SWITCH_HASH, + STATE_SWITCH_CAPABILITY, + SW_CAP_HASH_FIELD_LIST_KEY, + SW_CAP_ECMP_HASH_KEY, + SW_CAP_LAG_HASH_KEY, + SW_HASH_KEY, + SW_CAP_KEY, +) + +# +# Hash helpers -------------------------------------------------------------------------------------------------------- +# + +def format_attr_value(entry, attr): + """ Helper that formats attribute to be presented in the table output. + + Args: + entry (Dict[str, str]): CONFIG DB entry configuration. + attr (Dict): Attribute metadata. + + Returns: + str: formatted attribute value. + """ + + if attr["is-leaf-list"]: + value = entry.get(attr["name"], []) + return "\n".join(value) if value else "N/A" + return entry.get(attr["name"], "N/A") + +# +# Hash CLI ------------------------------------------------------------------------------------------------------------ +# + +@click.group( + name="switch-hash", + cls=clicommon.AliasedGroup +) +def SWITCH_HASH(): + """ Show switch hash feature configuration """ + + pass + + +@SWITCH_HASH.command( + name="global" +) +@clicommon.pass_db +def SWITCH_HASH_GLOBAL(db): + """ Show switch hash global configuration """ + + header = [ + "ECMP HASH", + "LAG HASH", + ] + body = [] + + table = db.cfgdb.get_table(CFG_SWITCH_HASH) + entry = table.get(SW_HASH_KEY, {}) + + if not entry: + click.echo(tabulate.tabulate(body, header)) + return + + row = [ + format_attr_value( + entry, + { + 'name': 'ecmp_hash', + 'is-leaf-list': True + } + ), + format_attr_value( + entry, + { + 'name': 'lag_hash', + 'is-leaf-list': True + } + ), + ] + body.append(row) + + click.echo(tabulate.tabulate(body, header)) + + +@SWITCH_HASH.command( + name="capabilities" +) +@clicommon.pass_db +def SWITCH_HASH_CAPABILITIES(db): + """ Show switch hash capabilities """ + + header = [ + "ECMP HASH", + "LAG HASH", + ] + body = [] + + entry = db.db.get_all(db.db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, SW_CAP_KEY)) + + if not entry: + click.echo(tabulate.tabulate(body, header)) + return + + entry.setdefault(SW_CAP_HASH_FIELD_LIST_KEY, 'N/A') + entry.setdefault(SW_CAP_ECMP_HASH_KEY, 'false') + entry.setdefault(SW_CAP_LAG_HASH_KEY, 'false') + + if not entry[SW_CAP_HASH_FIELD_LIST_KEY]: + entry[SW_CAP_HASH_FIELD_LIST_KEY] = "no capabilities" + + entry[SW_CAP_HASH_FIELD_LIST_KEY] = entry[SW_CAP_HASH_FIELD_LIST_KEY].split(',') + + row = [ + format_attr_value( + entry, + { + 'name': SW_CAP_HASH_FIELD_LIST_KEY, + 'is-leaf-list': True + } + ) if entry[SW_CAP_ECMP_HASH_KEY] == 'true' else 'not supported', + format_attr_value( + entry, + { + 'name': SW_CAP_HASH_FIELD_LIST_KEY, + 'is-leaf-list': True + } + ) if entry[SW_CAP_LAG_HASH_KEY] == 'true' else 'not supported', + ] + body.append(row) + + click.echo(tabulate.tabulate(body, header)) + + +def register(cli): + """ Register new CLI nodes in root CLI. + + Args: + cli (click.core.Command): Root CLI node. + Raises: + Exception: when root CLI already has a command + we are trying to register. + """ + cli_node = SWITCH_HASH + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command(SWITCH_HASH) diff --git a/tests/hash_input/assert_show_output.py b/tests/hash_input/assert_show_output.py new file mode 100644 index 0000000000..80edc1cf17 --- /dev/null +++ b/tests/hash_input/assert_show_output.py @@ -0,0 +1,158 @@ +""" +Module holding the correct values for show CLI command outputs for the hash_test.py +""" + +show_hash_empty="""\ +ECMP HASH LAG HASH +----------- ---------- +""" + +show_hash_ecmp="""\ +ECMP HASH LAG HASH +----------------- ---------- +DST_MAC N/A +SRC_MAC +ETHERTYPE +IP_PROTOCOL +DST_IP +SRC_IP +L4_DST_PORT +L4_SRC_PORT +INNER_DST_MAC +INNER_SRC_MAC +INNER_ETHERTYPE +INNER_IP_PROTOCOL +INNER_DST_IP +INNER_SRC_IP +INNER_L4_DST_PORT +INNER_L4_SRC_PORT +""" + +show_hash_lag="""\ +ECMP HASH LAG HASH +----------- ----------------- +N/A DST_MAC + SRC_MAC + ETHERTYPE + IP_PROTOCOL + DST_IP + SRC_IP + L4_DST_PORT + L4_SRC_PORT + INNER_DST_MAC + INNER_SRC_MAC + INNER_ETHERTYPE + INNER_IP_PROTOCOL + INNER_DST_IP + INNER_SRC_IP + INNER_L4_DST_PORT + INNER_L4_SRC_PORT +""" + +show_hash_ecmp_and_lag="""\ +ECMP HASH LAG HASH +----------------- ----------------- +DST_MAC DST_MAC +SRC_MAC SRC_MAC +ETHERTYPE ETHERTYPE +IP_PROTOCOL IP_PROTOCOL +DST_IP DST_IP +SRC_IP SRC_IP +L4_DST_PORT L4_DST_PORT +L4_SRC_PORT L4_SRC_PORT +INNER_DST_MAC INNER_DST_MAC +INNER_SRC_MAC INNER_SRC_MAC +INNER_ETHERTYPE INNER_ETHERTYPE +INNER_IP_PROTOCOL INNER_IP_PROTOCOL +INNER_DST_IP INNER_DST_IP +INNER_SRC_IP INNER_SRC_IP +INNER_L4_DST_PORT INNER_L4_DST_PORT +INNER_L4_SRC_PORT INNER_L4_SRC_PORT +""" + +show_hash_capabilities_no="""\ +ECMP HASH LAG HASH +--------------- --------------- +no capabilities no capabilities +""" + +show_hash_capabilities_na="""\ +ECMP HASH LAG HASH +----------- ---------- +N/A N/A +""" + +show_hash_capabilities_empty="""\ +ECMP HASH LAG HASH +------------- ------------- +not supported not supported +""" + +show_hash_capabilities_ecmp="""\ +ECMP HASH LAG HASH +----------------- ------------- +IN_PORT not supported +DST_MAC +SRC_MAC +ETHERTYPE +VLAN_ID +IP_PROTOCOL +DST_IP +SRC_IP +L4_DST_PORT +L4_SRC_PORT +INNER_DST_MAC +INNER_SRC_MAC +INNER_ETHERTYPE +INNER_IP_PROTOCOL +INNER_DST_IP +INNER_SRC_IP +INNER_L4_DST_PORT +INNER_L4_SRC_PORT +""" + +show_hash_capabilities_lag="""\ +ECMP HASH LAG HASH +------------- ----------------- +not supported IN_PORT + DST_MAC + SRC_MAC + ETHERTYPE + VLAN_ID + IP_PROTOCOL + DST_IP + SRC_IP + L4_DST_PORT + L4_SRC_PORT + INNER_DST_MAC + INNER_SRC_MAC + INNER_ETHERTYPE + INNER_IP_PROTOCOL + INNER_DST_IP + INNER_SRC_IP + INNER_L4_DST_PORT + INNER_L4_SRC_PORT +""" + +show_hash_capabilities_ecmp_and_lag="""\ +ECMP HASH LAG HASH +----------------- ----------------- +IN_PORT IN_PORT +DST_MAC DST_MAC +SRC_MAC SRC_MAC +ETHERTYPE ETHERTYPE +VLAN_ID VLAN_ID +IP_PROTOCOL IP_PROTOCOL +DST_IP DST_IP +SRC_IP SRC_IP +L4_DST_PORT L4_DST_PORT +L4_SRC_PORT L4_SRC_PORT +INNER_DST_MAC INNER_DST_MAC +INNER_SRC_MAC INNER_SRC_MAC +INNER_ETHERTYPE INNER_ETHERTYPE +INNER_IP_PROTOCOL INNER_IP_PROTOCOL +INNER_DST_IP INNER_DST_IP +INNER_SRC_IP INNER_SRC_IP +INNER_L4_DST_PORT INNER_L4_DST_PORT +INNER_L4_SRC_PORT INNER_L4_SRC_PORT +""" diff --git a/tests/hash_input/mock_config/ecmp.json b/tests/hash_input/mock_config/ecmp.json new file mode 100644 index 0000000000..329af0be66 --- /dev/null +++ b/tests/hash_input/mock_config/ecmp.json @@ -0,0 +1,5 @@ +{ + "SWITCH_HASH|GLOBAL": { + "ecmp_hash@": "DST_MAC,SRC_MAC,ETHERTYPE,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_config/ecmp_and_lag.json b/tests/hash_input/mock_config/ecmp_and_lag.json new file mode 100644 index 0000000000..caa095ac4e --- /dev/null +++ b/tests/hash_input/mock_config/ecmp_and_lag.json @@ -0,0 +1,6 @@ +{ + "SWITCH_HASH|GLOBAL": { + "ecmp_hash@": "DST_MAC,SRC_MAC,ETHERTYPE,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT", + "lag_hash@": "DST_MAC,SRC_MAC,ETHERTYPE,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_config/empty.json b/tests/hash_input/mock_config/empty.json new file mode 100644 index 0000000000..e4b16ce183 --- /dev/null +++ b/tests/hash_input/mock_config/empty.json @@ -0,0 +1,5 @@ +{ + "SWITCH_HASH|GLOBAL": { + "NULL": "NULL" + } +} diff --git a/tests/hash_input/mock_config/lag.json b/tests/hash_input/mock_config/lag.json new file mode 100644 index 0000000000..69b63bd0ab --- /dev/null +++ b/tests/hash_input/mock_config/lag.json @@ -0,0 +1,5 @@ +{ + "SWITCH_HASH|GLOBAL": { + "lag_hash@": "DST_MAC,SRC_MAC,ETHERTYPE,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_state/ecmp.json b/tests/hash_input/mock_state/ecmp.json new file mode 100644 index 0000000000..e9f7cf436d --- /dev/null +++ b/tests/hash_input/mock_state/ecmp.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "ECMP_HASH_CAPABLE": "true", + "LAG_HASH_CAPABLE": "false", + "HASH|NATIVE_HASH_FIELD_LIST": "IN_PORT,DST_MAC,SRC_MAC,ETHERTYPE,VLAN_ID,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_state/ecmp_and_lag.json b/tests/hash_input/mock_state/ecmp_and_lag.json new file mode 100644 index 0000000000..ee0e94169b --- /dev/null +++ b/tests/hash_input/mock_state/ecmp_and_lag.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "ECMP_HASH_CAPABLE": "true", + "LAG_HASH_CAPABLE": "true", + "HASH|NATIVE_HASH_FIELD_LIST": "IN_PORT,DST_MAC,SRC_MAC,ETHERTYPE,VLAN_ID,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_state/empty.json b/tests/hash_input/mock_state/empty.json new file mode 100644 index 0000000000..9e221a2fb7 --- /dev/null +++ b/tests/hash_input/mock_state/empty.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "ECMP_HASH_CAPABLE": "false", + "LAG_HASH_CAPABLE": "false", + "HASH|NATIVE_HASH_FIELD_LIST": "IN_PORT,DST_MAC,SRC_MAC,ETHERTYPE,VLAN_ID,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_state/lag.json b/tests/hash_input/mock_state/lag.json new file mode 100644 index 0000000000..3fb0008a18 --- /dev/null +++ b/tests/hash_input/mock_state/lag.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "ECMP_HASH_CAPABLE": "false", + "LAG_HASH_CAPABLE": "true", + "HASH|NATIVE_HASH_FIELD_LIST": "IN_PORT,DST_MAC,SRC_MAC,ETHERTYPE,VLAN_ID,IP_PROTOCOL,DST_IP,SRC_IP,L4_DST_PORT,L4_SRC_PORT,INNER_DST_MAC,INNER_SRC_MAC,INNER_ETHERTYPE,INNER_IP_PROTOCOL,INNER_DST_IP,INNER_SRC_IP,INNER_L4_DST_PORT,INNER_L4_SRC_PORT" + } +} diff --git a/tests/hash_input/mock_state/no_capabilities.json b/tests/hash_input/mock_state/no_capabilities.json new file mode 100644 index 0000000000..68f5962ca4 --- /dev/null +++ b/tests/hash_input/mock_state/no_capabilities.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "ECMP_HASH_CAPABLE": "true", + "LAG_HASH_CAPABLE": "true", + "HASH|NATIVE_HASH_FIELD_LIST": "" + } +} diff --git a/tests/hash_input/mock_state/not_applicable.json b/tests/hash_input/mock_state/not_applicable.json new file mode 100644 index 0000000000..de390e8961 --- /dev/null +++ b/tests/hash_input/mock_state/not_applicable.json @@ -0,0 +1,7 @@ +{ + "SWITCH_CAPABILITY|switch": { + "ECMP_HASH_CAPABLE": "true", + "LAG_HASH_CAPABLE": "true", + "HASH|NATIVE_HASH_FIELD_LIST": "N/A" + } +} diff --git a/tests/hash_test.py b/tests/hash_test.py new file mode 100644 index 0000000000..96e8558d19 --- /dev/null +++ b/tests/hash_test.py @@ -0,0 +1,244 @@ +import pytest +import os +import logging +import show.main as show +import config.main as config + +from click.testing import CliRunner +from utilities_common.db import Db +from .mock_tables import dbconnector +from .hash_input import assert_show_output + + +test_path = os.path.dirname(os.path.abspath(__file__)) +input_path = os.path.join(test_path, "hash_input") +mock_config_path = os.path.join(input_path, "mock_config") +mock_state_path = os.path.join(input_path, "mock_state") + +logger = logging.getLogger(__name__) + + +HASH_FIELD_LIST = [ + "DST_MAC", + "SRC_MAC", + "ETHERTYPE", + "IP_PROTOCOL", + "DST_IP", + "SRC_IP", + "L4_DST_PORT", + "L4_SRC_PORT" +] +INNER_HASH_FIELD_LIST = [ + "INNER_DST_MAC", + "INNER_SRC_MAC", + "INNER_ETHERTYPE", + "INNER_IP_PROTOCOL", + "INNER_DST_IP", + "INNER_SRC_IP", + "INNER_L4_DST_PORT", + "INNER_L4_SRC_PORT" +] + +SUCCESS = 0 +ERROR2 = 2 + + +class TestHash: + @classmethod + def setup_class(cls): + logger.info("Setup class: {}".format(cls.__name__)) + os.environ['UTILITIES_UNIT_TESTING'] = "1" + dbconnector.dedicated_dbs["STATE_DB"] = os.path.join(mock_state_path, "ecmp_and_lag") + + @classmethod + def teardown_class(cls): + logger.info("Teardown class: {}".format(cls.__name__)) + os.environ['UTILITIES_UNIT_TESTING'] = "0" + dbconnector.dedicated_dbs.clear() + + + ########## CONFIG SWITCH-HASH GLOBAL ########## + + + @pytest.mark.parametrize( + "hash", [ + "ecmp-hash", + "lag-hash" + ] + ) + @pytest.mark.parametrize( + "args", [ + pytest.param( + " ".join(HASH_FIELD_LIST), + id="outer-frame" + ), + pytest.param( + " ".join(INNER_HASH_FIELD_LIST), + id="inner-frame" + ) + ] + ) + def test_config_hash(self, hash, args): + db = Db() + runner = CliRunner() + + result = runner.invoke( + config.config.commands["switch-hash"].commands["global"]. + commands[hash], args, obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + assert result.exit_code == SUCCESS + + @pytest.mark.parametrize( + "hash", [ + "ecmp-hash", + "lag-hash" + ] + ) + @pytest.mark.parametrize( + "args,pattern", [ + pytest.param( + "DST_MAC1 SRC_MAC ETHERTYPE", + "invalid choice: DST_MAC1.", + id="INVALID,SRC_MAC,ETHERTYPE" + ), + pytest.param( + "DST_MAC SRC_MAC1 ETHERTYPE", + "invalid choice: SRC_MAC1.", + id="DST_MAC,INVALID,ETHERTYPE" + ), + pytest.param( + "DST_MAC SRC_MAC ETHERTYPE1", + "invalid choice: ETHERTYPE1.", + id="DST_MAC,SRC_MAC,INVALID" + ), + pytest.param( + "DST_MAC DST_MAC SRC_MAC ETHERTYPE", + "duplicate hash field(s) DST_MAC", + id="DUPLICATE,SRC_MAC,ETHERTYPE" + ), + pytest.param( + "DST_MAC DST_MAC SRC_MAC SRC_MAC ETHERTYPE", + "duplicate hash field(s) DST_MAC, SRC_MAC", + id="DUPLICATE,DUPLICATE,ETHERTYPE" + ), + pytest.param( + "DST_MAC DST_MAC SRC_MAC SRC_MAC ETHERTYPE ETHERTYPE", + "duplicate hash field(s) DST_MAC, SRC_MAC, ETHERTYPE", + id="DUPLICATE,DUPLICATE,DUPLICATE" + ) + ] + ) + def test_config_hash_neg(self, hash, args, pattern): + db = Db() + runner = CliRunner() + + result = runner.invoke( + config.config.commands["switch-hash"].commands["global"]. + commands[hash], args, obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + assert pattern in result.output + assert result.exit_code == ERROR2 + + + ########## SHOW SWITCH-HASH GLOBAL ########## + + + @pytest.mark.parametrize( + "cfgdb,output", [ + pytest.param( + os.path.join(mock_config_path, "empty"), + assert_show_output.show_hash_empty, + id="empty" + ), + pytest.param( + os.path.join(mock_config_path, "ecmp"), + assert_show_output.show_hash_ecmp, + id="ecmp" + ), + pytest.param( + os.path.join(mock_config_path, "lag"), + assert_show_output.show_hash_lag, + id="lag" + ), + pytest.param( + os.path.join(mock_config_path, "ecmp_and_lag"), + assert_show_output.show_hash_ecmp_and_lag, + id="all" + ) + ] + ) + def test_show_hash(self, cfgdb, output): + dbconnector.dedicated_dbs["CONFIG_DB"] = cfgdb + + db = Db() + runner = CliRunner() + + result = runner.invoke( + show.cli.commands["switch-hash"]. + commands["global"], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + assert result.output == output + assert result.exit_code == SUCCESS + + @pytest.mark.parametrize( + "statedb,output", [ + pytest.param( + os.path.join(mock_state_path, "no_capabilities"), + assert_show_output.show_hash_capabilities_no, + id="no" + ), + pytest.param( + os.path.join(mock_state_path, "not_applicable"), + assert_show_output.show_hash_capabilities_na, + id="na" + ), + pytest.param( + os.path.join(mock_state_path, "empty"), + assert_show_output.show_hash_capabilities_empty, + id="empty" + ), + pytest.param( + os.path.join(mock_state_path, "ecmp"), + assert_show_output.show_hash_capabilities_ecmp, + id="ecmp" + ), + pytest.param( + os.path.join(mock_state_path, "lag"), + assert_show_output.show_hash_capabilities_lag, + id="lag" + ), + pytest.param( + os.path.join(mock_state_path, "ecmp_and_lag"), + assert_show_output.show_hash_capabilities_ecmp_and_lag, + id="all" + ) + ] + ) + def test_show_hash_capabilities(self, statedb, output): + dbconnector.dedicated_dbs["STATE_DB"] = statedb + + db = Db() + runner = CliRunner() + + result = runner.invoke( + show.cli.commands["switch-hash"]. + commands["capabilities"], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + assert result.output == output + assert result.exit_code == SUCCESS diff --git a/utilities_common/switch_hash.py b/utilities_common/switch_hash.py new file mode 100644 index 0000000000..1f29ca4e30 --- /dev/null +++ b/utilities_common/switch_hash.py @@ -0,0 +1,64 @@ +from collections import Counter + +from swsscommon.swsscommon import CFG_SWITCH_HASH_TABLE_NAME as CFG_SWITCH_HASH +from swsscommon.swsscommon import STATE_SWITCH_CAPABILITY_TABLE_NAME as STATE_SWITCH_CAPABILITY + +# +# Hash constants ------------------------------------------------------------------------------------------------------ +# + +SW_CAP_HASH_FIELD_LIST_KEY = "HASH|NATIVE_HASH_FIELD_LIST" +SW_CAP_ECMP_HASH_KEY = "ECMP_HASH_CAPABLE" +SW_CAP_LAG_HASH_KEY = "LAG_HASH_CAPABLE" + +SW_HASH_KEY = "GLOBAL" +SW_CAP_KEY = "switch" + +HASH_FIELD_LIST = [ + "IN_PORT", + "DST_MAC", + "SRC_MAC", + "ETHERTYPE", + "VLAN_ID", + "IP_PROTOCOL", + "DST_IP", + "SRC_IP", + "L4_DST_PORT", + "L4_SRC_PORT", + "INNER_DST_MAC", + "INNER_SRC_MAC", + "INNER_ETHERTYPE", + "INNER_IP_PROTOCOL", + "INNER_DST_IP", + "INNER_SRC_IP", + "INNER_L4_DST_PORT", + "INNER_L4_SRC_PORT" +] + +SYSLOG_IDENTIFIER = "switch_hash" + +# +# Hash helpers -------------------------------------------------------------------------------------------------------- +# + +def get_param(ctx, name): + """ Get click parameter """ + for param in ctx.command.params: + if param.name == name: + return param + return None + + +def get_param_hint(ctx, name): + """ Get click parameter description """ + return get_param(ctx, name).get_error_hint(ctx) + + +def get_dupes(obj_list): + """ Get list duplicate items """ + return [k for k, v in Counter(obj_list).items() if v > 1] + + +def to_str(obj_list): + """ Convert list to comma-separated representation """ + return ", ".join(obj_list) From 08bf4b2b5605e390c00317833663d9974f9080cf Mon Sep 17 00:00:00 2001 From: Nazarii Hnydyn Date: Mon, 10 Jul 2023 12:59:09 +0300 Subject: [PATCH 2/2] [hash]: Add GH UM reference. Signed-off-by: Nazarii Hnydyn --- doc/Command-Reference.md | 98 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 2316441f9e..211074bc6c 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -64,6 +64,9 @@ * [Flow Counters config commands](#flow-counters-config-commands) * [Gearbox](#gearbox) * [Gearbox show commands](#gearbox-show-commands) +* [Hash](#hash) + * [Hash show commands](#hash-show-commands) + * [Hash config commands](#hash-config-commands) * [Interfaces](#interfaces) * [Interface Show Commands](#interface-show-commands) * [Interface Config Commands](#interface-config-commands) @@ -4087,6 +4090,101 @@ This command is used to change device hostname without traffic being impacted. Please note loaded setting will be lost after system reboot. To preserve setting, run `config save`. ``` +## Hash + +This section explains the various show and configuration commands available for user. + +### Hash Show Commands + +This subsection explains how to display switch hash configuration. + +**show switch-hash global** + +This command displays switch hash global configuration. + +- Usage: + ```bash + show switch-hash global + ``` + +- Example: + ```bash + admin@sonic:~$ show switch-hash global + ECMP HASH LAG HASH + ----------------- ----------------- + DST_MAC DST_MAC + SRC_MAC SRC_MAC + ETHERTYPE ETHERTYPE + IP_PROTOCOL IP_PROTOCOL + DST_IP DST_IP + SRC_IP SRC_IP + L4_DST_PORT L4_DST_PORT + L4_SRC_PORT L4_SRC_PORT + INNER_DST_MAC INNER_DST_MAC + INNER_SRC_MAC INNER_SRC_MAC + INNER_ETHERTYPE INNER_ETHERTYPE + INNER_IP_PROTOCOL INNER_IP_PROTOCOL + INNER_DST_IP INNER_DST_IP + INNER_SRC_IP INNER_SRC_IP + INNER_L4_DST_PORT INNER_L4_DST_PORT + INNER_L4_SRC_PORT INNER_L4_SRC_PORT + ``` + +### Hash Config Commands + +This subsection explains how to configure switch hash. + +**config switch-hash global** + +This command is used to manage switch hash global configuration. + +- Usage: + ```bash + config switch-hash global ecmp-hash + config switch-hash global lag-hash + ``` + +- Parameters: + - _hash_field_list_: hash fields for hashing packets going through ECMP/LAG + +- Examples: + ```bash + admin@sonic:~$ config switch-hash global ecmp-hash \ + 'DST_MAC' \ + 'SRC_MAC' \ + 'ETHERTYPE' \ + 'IP_PROTOCOL' \ + 'DST_IP' \ + 'SRC_IP' \ + 'L4_DST_PORT' \ + 'L4_SRC_PORT' \ + 'INNER_DST_MAC' \ + 'INNER_SRC_MAC' \ + 'INNER_ETHERTYPE' \ + 'INNER_IP_PROTOCOL' \ + 'INNER_DST_IP' \ + 'INNER_SRC_IP' \ + 'INNER_L4_DST_PORT' \ + 'INNER_L4_SRC_PORT' + admin@sonic:~$ config switch-hash global lag-hash \ + 'DST_MAC' \ + 'SRC_MAC' \ + 'ETHERTYPE' \ + 'IP_PROTOCOL' \ + 'DST_IP' \ + 'SRC_IP' \ + 'L4_DST_PORT' \ + 'L4_SRC_PORT' \ + 'INNER_DST_MAC' \ + 'INNER_SRC_MAC' \ + 'INNER_ETHERTYPE' \ + 'INNER_IP_PROTOCOL' \ + 'INNER_DST_IP' \ + 'INNER_SRC_IP' \ + 'INNER_L4_DST_PORT' \ + 'INNER_L4_SRC_PORT' + ``` + ## Interfaces ### Interface Show Commands