diff --git a/.azure-pipelines/pre-commit-check.yml b/.azure-pipelines/pre-commit-check.yml new file mode 100644 index 0000000000..5b25bef76f --- /dev/null +++ b/.azure-pipelines/pre-commit-check.yml @@ -0,0 +1,35 @@ +steps: +- checkout: self + clean: true + displayName: 'checkout sonic-utilities repo' + +- script: | + set -x + sudo pip install pre-commit + pre-commit install-hooks + displayName: 'Prepare pre-commit check' + +- script: | + # Run pre-commit check and capture the output + out=`pre-commit run --color never --from-ref HEAD^ --to-ref HEAD 2>&1` + RC=$? + if [[ $RC -ne 0 ]]; then + echo -e "The [pre-commit](http://pre-commit.com/) check detected issues in the files touched by this pull request.\n\ + The pre-commit check is a mandatory check, please fix detected issues.\n\ + \n\ + To run the pre-commit checks locally, you can follow below steps:\n\ + 1. Ensure that default python is python3.\n\ + 2. Ensure that the 'pre-commit' package is installed:\n\ + sudo pip install pre-commit\n\ + 3. Go to repository root folder\n\ + 4. Install the pre-commit hooks:\n\ + pre-commit install\n\ + 5. Use pre-commit to check staged file:\n\ + pre-commit\n\ + 6. Alternatively, you can check committed files using:\n\ + pre-commit run --from-ref --to-ref \n" + fi + echo "Pre-commit check results:" + echo "$out" + exit $RC + displayName: 'Run pre-commit check' diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 8ebe082f50..1686f20364 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -18,4 +18,4 @@ jobs: - uses: actions/checkout@v3 - run: semgrep ci env: - SEMGREP_RULES: p/default + SEMGREP_RULES: "p/default r/python.lang.security.audit.dangerous-system-call-audit.dangerous-system-call-audit" diff --git a/.gitignore b/.gitignore index 439e053c64..f324bc1216 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ dist/ coverage.xml htmlcov/ +# Dev tools +.vscode/ +.idea/ + # Ignores for sonic-utilities-data sonic-utilities-data/debian/* !sonic-utilities-data/debian/changelog diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..1f76ad9914 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + entry: bash -c 'git diff HEAD^ HEAD -U0 -- "$@" | flake8 --diff "$@"' -- + args: ["--max-line-length=120"] diff --git a/README.md b/README.md index f63b0832a2..d6f9a5e25a 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ A convenient alternative is to let the SONiC build system configure a build envi 2. Build the sonic-utilities Python wheel package inside the Bullseye slave container, and tell the build system to keep the container alive when finished ``` - make NOSTRETCH=1 NOBUSTER=1 KEEP_SLAVE_ON=yes target/python-wheels/bullseye/sonic_utilities-1.2-py3-none-any.whl + make -f Makefile.work BLDENV=bookworm KEEP_SLAVE_ON=yes target/python-wheels/bookworm/sonic_utilities-1.2-py3-none-any.whl ``` 3. When the build finishes, your prompt will change to indicate you are inside the slave container. Change into the `src/sonic-utilities/` directory @@ -66,6 +66,7 @@ A convenient alternative is to let the SONiC build system configure a build envi ``` python3 setup.py bdist_wheel ``` +Note: This command by default will not update the wheel package in target/. To specify the destination location of wheel package, use "-d" option. #### To run unit tests @@ -73,6 +74,12 @@ python3 setup.py bdist_wheel python3 setup.py test ``` +#### To install the package on a SONiC machine +``` +sudo pip uninstall sonic-utilities +sudo pip install YOUR_WHEEL_PACKAGE +``` +Note: Don't use "--force-reinstall". ### sonic-utilities-data diff --git a/acl_loader/main.py b/acl_loader/main.py index e81e05d9b7..c83ad6997b 100644 --- a/acl_loader/main.py +++ b/acl_loader/main.py @@ -73,8 +73,10 @@ class AclLoader(object): ACL_TABLE = "ACL_TABLE" ACL_RULE = "ACL_RULE" CFG_ACL_TABLE = "ACL_TABLE" + APPL_ACL_TABLE = "ACL_TABLE_TABLE" STATE_ACL_TABLE = "ACL_TABLE_TABLE" CFG_ACL_RULE = "ACL_RULE" + APPL_ACL_RULE = "ACL_RULE_TABLE" STATE_ACL_RULE = "ACL_RULE_TABLE" ACL_TABLE_TYPE_MIRROR = "MIRROR" ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE" @@ -135,6 +137,8 @@ def __init__(self): self.configdb.connect() self.statedb = SonicV2Connector(host="127.0.0.1") self.statedb.connect(self.statedb.STATE_DB) + self.appldb = SonicV2Connector(host="127.0.0.1") + self.appldb.connect(self.statedb.APPL_DB) # For multi-npu architecture we will have both global and per front asic namespace. # Global namespace will be used for Control plane ACL which are via IPTables. @@ -165,8 +169,8 @@ def __init__(self): self.read_rules_info() self.read_sessions_info() self.read_policers_info() - self.acl_table_status = self.read_acl_object_status_info(self.CFG_ACL_TABLE, self.STATE_ACL_TABLE) - self.acl_rule_status = self.read_acl_object_status_info(self.CFG_ACL_RULE, self.STATE_ACL_RULE) + self.acl_table_status = self.read_acl_object_status_info(self.tables_db_info.keys(), self.STATE_ACL_TABLE) + self.acl_rule_status = self.read_acl_object_status_info(self.rules_db_info.keys(), self.STATE_ACL_RULE) def read_tables_info(self): """ @@ -199,16 +203,51 @@ def read_tables_info(self): self.tables_db_info[table]['ports'] += entry.get( 'ports', []) + if self.per_npu_configdb: + # Note: Ability to read table information from APPL_DB is not yet supported for masic devices + return + + appl_db_keys = self.appldb.keys(self.appldb.APPL_DB, "{}:*".format(self.APPL_ACL_TABLE)) + if not appl_db_keys: + return + + for app_acl_tbl in appl_db_keys: + key = app_acl_tbl.split(":")[-1] + if key in self.tables_db_info: + # Shouldn't be hit, table is either programmed to APPL or CONFIG DB + continue + self.tables_db_info[key] = dict() + for f, v in self.appldb.get_all(self.appldb.APPL_DB, app_acl_tbl).items(): + if f.lower() == "ports": + v = v.split(",") + self.tables_db_info[key][f.lower()] = v + def get_tables_db_info(self): return self.tables_db_info def read_rules_info(self): """ - Read ACL_RULE table from configuration database + Read ACL_RULE table from CFG_DB and APPL_DB database :return: """ self.rules_db_info = self.configdb.get_table(self.ACL_RULE) + if self.per_npu_configdb: + # Note: Ability to read table information from APPL_DB is not yet supported for masic devices + return + + # Read rule information from APPL_DB + appl_db_keys = self.appldb.keys(self.appldb.APPL_DB, "{}:*".format(self.APPL_ACL_RULE)) + if not appl_db_keys: + return + + for app_acl_rule in appl_db_keys: + _, tid, rid = app_acl_rule.split(":") + if (tid, rid) in self.rules_db_info: + # Shouldn't be hit, table is either programmed to APPL or CONFIG DB + continue + self.rules_db_info[(tid, rid)] = self.appldb.get_all(self.appldb.APPL_DB, app_acl_rule) + def get_rules_db_info(self): return self.rules_db_info @@ -259,16 +298,10 @@ def read_sessions_info(self): self.sessions_db_info[key]["status"] = state_db_info.get("status", "inactive") if state_db_info else "error" self.sessions_db_info[key]["monitor_port"] = state_db_info.get("monitor_port", "") if state_db_info else "" - def read_acl_object_status_info(self, cfg_db_table_name, state_db_table_name): + def read_acl_object_status_info(self, keys, state_db_table_name): """ Read ACL_TABLE status or ACL_RULE status from STATE_DB """ - if self.per_npu_configdb: - namespace_configdb = list(self.per_npu_configdb.values())[0] - keys = namespace_configdb.get_table(cfg_db_table_name).keys() - else: - keys = self.configdb.get_table(cfg_db_table_name).keys() - status = {} for key in keys: # For ACL_RULE, the key is (acl_table_name, acl_rule_name) @@ -413,7 +446,7 @@ def parse_acl_json(filename): raise AclLoaderException("Invalid input file %s" % filename) return yang_acl - def load_rules_from_file(self, filename): + def load_rules_from_file(self, filename, skip_action_validation=False): """ Load file with ACL rules configuration in openconfig ACL format. Convert rules to Config DB schema. @@ -421,9 +454,9 @@ def load_rules_from_file(self, filename): :return: """ self.yang_acl = AclLoader.parse_acl_json(filename) - self.convert_rules() + self.convert_rules(skip_action_validation) - def convert_action(self, table_name, rule_idx, rule): + def convert_action(self, table_name, rule_idx, rule, skip_validation=False): rule_props = {} if rule.actions.config.forwarding_action == "ACCEPT": @@ -452,13 +485,13 @@ def convert_action(self, table_name, rule_idx, rule): raise AclLoaderException("Unknown rule action {} in table {}, rule {}".format( rule.actions.config.forwarding_action, table_name, rule_idx)) - if not self.validate_actions(table_name, rule_props): + if not self.validate_actions(table_name, rule_props, skip_validation): raise AclLoaderException("Rule action {} is not supported in table {}, rule {}".format( rule.actions.config.forwarding_action, table_name, rule_idx)) return rule_props - def validate_actions(self, table_name, action_props): + def validate_actions(self, table_name, action_props, skip_validation=False): if self.is_table_control_plane(table_name): return True @@ -481,6 +514,11 @@ def validate_actions(self, table_name, action_props): else: aclcapability = self.statedb.get_all(self.statedb.STATE_DB, "{}|{}".format(self.ACL_STAGE_CAPABILITY_TABLE, stage.upper())) switchcapability = self.statedb.get_all(self.statedb.STATE_DB, "{}|switch".format(self.SWITCH_CAPABILITY_TABLE)) + # In the load_minigraph path, it's possible that the STATE_DB entry haven't pop up because orchagent is stopped + # before loading acl.json. So we skip the validation if any table is empty + if skip_validation and (not aclcapability or not switchcapability): + warning("Skipped action validation as capability table is not present in STATE_DB") + return True for action_key in dict(action_props): action_list_key = self.ACL_ACTIONS_CAPABILITY_FIELD if action_list_key not in aclcapability: @@ -699,7 +737,7 @@ def validate_rule_fields(self, rule_props): if ("ICMPV6_TYPE" in rule_props or "ICMPV6_CODE" in rule_props) and protocol != 58: raise AclLoaderException("IP_PROTOCOL={} is not ICMPV6, but ICMPV6 fields were provided".format(protocol)) - def convert_rule_to_db_schema(self, table_name, rule): + def convert_rule_to_db_schema(self, table_name, rule, skip_action_validation=False): """ Convert rules format from openconfig ACL to Config DB schema :param table_name: ACL table name to which rule belong @@ -729,7 +767,7 @@ def convert_rule_to_db_schema(self, table_name, rule): elif self.is_table_l3(table_name): rule_props["ETHER_TYPE"] = str(self.ethertype_map["ETHERTYPE_IPV4"]) - deep_update(rule_props, self.convert_action(table_name, rule_idx, rule)) + deep_update(rule_props, self.convert_action(table_name, rule_idx, rule, skip_action_validation)) deep_update(rule_props, self.convert_l2(table_name, rule_idx, rule)) deep_update(rule_props, self.convert_ip(table_name, rule_idx, rule)) deep_update(rule_props, self.convert_icmp(table_name, rule_idx, rule)) @@ -761,7 +799,7 @@ def deny_rule(self, table_name): return {} # Don't add default deny rule if table is not [L3, L3V6] return rule_data - def convert_rules(self): + def convert_rules(self, skip_aciton_validation=False): """ Convert rules in openconfig ACL format to Config DB schema :return: @@ -780,7 +818,7 @@ def convert_rules(self): for acl_entry_name in acl_set.acl_entries.acl_entry: acl_entry = acl_set.acl_entries.acl_entry[acl_entry_name] try: - rule = self.convert_rule_to_db_schema(table_name, acl_entry) + rule = self.convert_rule_to_db_schema(table_name, acl_entry, skip_aciton_validation) deep_update(self.rules_info, rule) except AclLoaderException as ex: error("Error processing rule %s: %s. Skipped." % (acl_entry_name, ex)) @@ -917,19 +955,19 @@ def show_table(self, table_name): status = self.acl_table_status[key]['status'] else: status = 'N/A' - if val["type"] == AclLoader.ACL_TABLE_TYPE_CTRLPLANE: + if val.get("type", "N/A") == AclLoader.ACL_TABLE_TYPE_CTRLPLANE: services = natsorted(val["services"]) - data.append([key, val["type"], services[0], val["policy_desc"], stage, status]) + data.append([key, val.get("type", "N/A"), services[0], val.get("policy_desc", ""), stage, status]) if len(services) > 1: for service in services[1:]: data.append(["", "", service, "", "", ""]) else: - if not val["ports"]: - data.append([key, val["type"], "", val["policy_desc"], stage, status]) + if not val.get("ports", []): + data.append([key, val["type"], "", val.get("policy_desc", ""), stage, status]) else: ports = natsorted(val["ports"]) - data.append([key, val["type"], ports[0], val["policy_desc"], stage, status]) + data.append([key, val["type"], ports[0], val.get("policy_desc", ""), stage, status]) if len(ports) > 1: for port in ports[1:]: @@ -1149,8 +1187,9 @@ def update(ctx): @click.option('--session_name', type=click.STRING, required=False) @click.option('--mirror_stage', type=click.Choice(["ingress", "egress"]), default="ingress") @click.option('--max_priority', type=click.INT, required=False) +@click.option('--skip_action_validation', is_flag=True, default=False, help="Skip action validation") @click.pass_context -def full(ctx, filename, table_name, session_name, mirror_stage, max_priority): +def full(ctx, filename, table_name, session_name, mirror_stage, max_priority, skip_action_validation): """ Full update of ACL rules configuration. If a table_name is provided, the operation will be restricted in the specified table. @@ -1168,7 +1207,7 @@ def full(ctx, filename, table_name, session_name, mirror_stage, max_priority): if max_priority: acl_loader.set_max_priority(max_priority) - acl_loader.load_rules_from_file(filename) + acl_loader.load_rules_from_file(filename, skip_action_validation) acl_loader.full_update() diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9688eb2d5d..885f6cb736 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,6 +21,16 @@ variables: value: $(Build.SourceBranchName) stages: +- stage: Pretest + jobs: + - job: static_analysis + displayName: "Static Analysis" + timeoutInMinutes: 10 + continueOnError: true + pool: sonic-ubuntu-1c + steps: + - template: .azure-pipelines/pre-commit-check.yml + - stage: Build jobs: @@ -30,12 +40,20 @@ stages: DIFF_COVER_CHECK_THRESHOLD: 80 DIFF_COVER_ENABLE: 'true' pool: - vmImage: ubuntu-20.04 + vmImage: ubuntu-24.04 container: - image: sonicdev-microsoft.azurecr.io:443/sonic-slave-bullseye:$(BUILD_BRANCH) + image: sonicdev-microsoft.azurecr.io:443/sonic-slave-bookworm:$(BUILD_BRANCH) steps: + - script: | + set -ex + sudo apt-get update + sudo apt-get install -y python3-pip + sudo pip3 install requests==2.31.0 + sudo apt-get install -y python3-protobuf + displayName: "Install dependencies" + - script: | sourceBranch=$(Build.SourceBranchName) if [[ "$(Build.Reason)" == "PullRequest" ]];then @@ -67,7 +85,7 @@ stages: sudo dpkg -i libyang_1.0.73_amd64.deb sudo dpkg -i libyang-cpp_1.0.73_amd64.deb sudo dpkg -i python3-yang_1.0.73_amd64.deb - workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/ + workingDirectory: $(Pipeline.Workspace)/target/debs/bookworm/ displayName: 'Install Debian dependencies' - task: DownloadPipelineArtifact@2 @@ -75,7 +93,7 @@ stages: source: specific project: build pipeline: 9 - artifact: sonic-swss-common + artifact: sonic-swss-common-bookworm runVersion: 'latestFromBranch' runBranch: 'refs/heads/$(sourceBranch)' displayName: "Download sonic swss common deb packages" @@ -87,6 +105,27 @@ stages: workingDirectory: $(Pipeline.Workspace)/ displayName: 'Install swss-common dependencies' + + - task: DownloadPipelineArtifact@2 + inputs: + source: specific + project: build + pipeline: sonic-net.sonic-dash-api + artifact: sonic-dash-api + runVersion: 'latestFromBranch' + runBranch: 'refs/heads/$(BUILD_BRANCH)' + path: $(Build.ArtifactStagingDirectory)/download + patterns: | + libdashapi*.deb + displayName: "Download dash api" + + - script: | + set -xe + sudo apt-get update + sudo dpkg -i $(Build.ArtifactStagingDirectory)/download/libdashapi_*.deb + workingDirectory: $(Pipeline.Workspace)/ + displayName: 'Install libdashapi libraries' + - script: | set -xe sudo pip3 install swsssdk-2.0.1-py3-none-any.whl @@ -95,20 +134,22 @@ stages: sudo pip3 install sonic_yang_models-1.0-py3-none-any.whl sudo pip3 install sonic_config_engine-1.0-py3-none-any.whl sudo pip3 install sonic_platform_common-1.0-py3-none-any.whl - workingDirectory: $(Pipeline.Workspace)/target/python-wheels/bullseye/ + workingDirectory: $(Pipeline.Workspace)/target/python-wheels/bookworm/ displayName: 'Install Python dependencies' - script: | set -ex # Install .NET CORE curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - - sudo apt-add-repository https://packages.microsoft.com/debian/11/prod + sudo apt-add-repository https://packages.microsoft.com/debian/12/prod sudo apt-get update - sudo apt-get install -y dotnet-sdk-5.0 + sudo apt-get install -y dotnet-sdk-8.0 displayName: "Install .NET CORE" - script: | - python3 setup.py test + pip3 install ".[testing]" + pip3 uninstall --yes sonic-utilities + pytest displayName: 'Test Python 3' - task: PublishTestResults@2 @@ -128,7 +169,7 @@ stages: - script: | set -e - python3 setup.py bdist_wheel + python3 -m build -n displayName: 'Build Python 3 wheel' - publish: '$(System.DefaultWorkingDirectory)/dist/' diff --git a/clear/main.py b/clear/main.py index d09153533b..be04256d20 100755 --- a/clear/main.py +++ b/clear/main.py @@ -5,14 +5,14 @@ import click import utilities_common.cli as clicommon import utilities_common.multi_asic as multi_asic_util +from sonic_py_common import multi_asic from sonic_py_common.general import getstatusoutput_noshell_pipe from flow_counter_util.route import exit_if_route_flow_counter_not_support from utilities_common import util_base from show.plugins.pbh import read_pbh_counters from config.plugins.pbh import serialize_pbh_counters from . import plugins - - +from . import stp # This is from the aliases example: # https://github.com/pallets/click/blob/57c6f09611fc47ca80db0bd010f05998b3c0aa95/examples/aliases/aliases.py class Config(object): @@ -145,6 +145,10 @@ def ipv6(): pass +# 'STP' +# +cli.add_command(stp.spanning_tree) + # # Inserting BGP functionality into cli's clear parse-chain. # BGP commands are determined by the routing-stack being elected. @@ -214,6 +218,13 @@ def tunnelcounters(): command = ["tunnelstat", "-c"] run_command(command) + +@cli.command() +def srv6counters(): + """Clear SRv6 counters""" + command = ["srv6stat", "-c"] + run_command(command) + # # 'clear watermarks # @@ -229,16 +240,38 @@ def watermark(): if os.geteuid() != 0: sys.exit("Root privileges are required for this operation") + +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) @watermark.command('headroom') -def clear_wm_pg_headroom(): +def clear_wm_pg_headroom(namespace): """Clear user headroom WM for pg""" command = ['watermarkstat', '-c', '-t', 'pg_headroom'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @watermark.command('shared') -def clear_wm_pg_shared(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_wm_pg_shared(namespace): """Clear user shared WM for pg""" command = ['watermarkstat', '-c', '-t', 'pg_shared'] + if namespace: + command += ['-n', str(namespace)] run_command(command) @priority_group.group() @@ -261,93 +294,213 @@ def persistent_watermark(): if os.geteuid() != 0: sys.exit("Root privileges are required for this operation") + @persistent_watermark.command('headroom') -def clear_pwm_pg_headroom(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_pwm_pg_headroom(namespace): """Clear persistent headroom WM for pg""" command = ['watermarkstat', '-c', '-p', '-t', 'pg_headroom'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @persistent_watermark.command('shared') -def clear_pwm_pg_shared(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_pwm_pg_shared(namespace): """Clear persistent shared WM for pg""" command = ['watermarkstat', '-c', '-p', '-t', 'pg_shared'] + if namespace: + command += ['-n', str(namespace)] run_command(command) @cli.group() def queue(): - """Clear queue WM""" + """Clear queue""" pass + +@queue.command() +def wredcounters(): + """Clear queue wredcounters""" + command = ['wredstat', '-c'] + run_command(command) + + @queue.group() def watermark(): """Clear queue user WM. One does not simply clear WM, root is required""" if os.geteuid() != 0: sys.exit("Root privileges are required for this operation") + @watermark.command('unicast') -def clear_wm_q_uni(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_wm_q_uni(namespace): """Clear user WM for unicast queues""" command = ['watermarkstat', '-c', '-t', 'q_shared_uni'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @watermark.command('multicast') -def clear_wm_q_multi(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_wm_q_multi(namespace): """Clear user WM for multicast queues""" command = ['watermarkstat', '-c', '-t', 'q_shared_multi'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @watermark.command('all') -def clear_wm_q_all(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_wm_q_all(namespace): """Clear user WM for all queues""" command = ['watermarkstat', '-c', '-t', 'q_shared_all'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @queue.group(name='persistent-watermark') def persistent_watermark(): """Clear queue persistent WM. One does not simply clear WM, root is required""" if os.geteuid() != 0: sys.exit("Root privileges are required for this operation") + @persistent_watermark.command('unicast') -def clear_pwm_q_uni(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_pwm_q_uni(namespace): """Clear persistent WM for persistent queues""" command = ['watermarkstat', '-c', '-p', '-t', 'q_shared_uni'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @persistent_watermark.command('multicast') -def clear_pwm_q_multi(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_pwm_q_multi(namespace): """Clear persistent WM for multicast queues""" command = ['watermarkstat', '-c', '-p', '-t', 'q_shared_multi'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @persistent_watermark.command('all') -def clear_pwm_q_all(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def clear_pwm_q_all(namespace): """Clear persistent WM for all queues""" command = ['watermarkstat', '-c', '-p', '-t', 'q_shared_all'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @cli.group(name='headroom-pool') def headroom_pool(): """Clear headroom pool WM""" pass + @headroom_pool.command('watermark') -def watermark(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def watermark(namespace): """Clear headroom pool user WM. One does not simply clear WM, root is required""" if os.geteuid() != 0: sys.exit("Root privileges are required for this operation") command = ['watermarkstat', '-c', '-t', 'headroom_pool'] + if namespace: + command += ['-n', str(namespace)] run_command(command) + @headroom_pool.command('persistent-watermark') -def persistent_watermark(): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def persistent_watermark(namespace): """Clear headroom pool persistent WM. One does not simply clear WM, root is required""" if os.geteuid() != 0: sys.exit("Root privileges are required for this operation") command = ['watermarkstat', '-c', '-p', '-t', 'headroom_pool'] + if namespace: + command += ['-n', str(namespace)] run_command(command) # @@ -550,6 +703,28 @@ def route(prefix, vrf, namespace): helper = util_base.UtilHelper() helper.load_and_register_plugins(plugins, cli) +# ("sonic-clear asic-sdk-health-event") +@cli.command() +@click.option('--namespace', '-n', 'namespace', required=False, default=None, show_default=False, + help='Option needed for multi-asic only: provide namespace name', + type=click.Choice(multi_asic_util.multi_asic_ns_choices())) +@clicommon.pass_db +def asic_sdk_health_event(db, namespace): + """Clear received ASIC/SDK health events""" + if multi_asic.get_num_asics() > 1: + namespace_list = multi_asic.get_namespaces_from_linux() + else: + namespace_list = [multi_asic.DEFAULT_NAMESPACE] + + for ns in namespace_list: + if namespace and namespace != ns: + continue + + state_db = db.db_clients[ns] + keys = state_db.keys(db.db.STATE_DB, "ASIC_SDK_HEALTH_EVENT_TABLE*") + for key in keys: + state_db.delete(state_db.STATE_DB, key); + if __name__ == '__main__': cli() diff --git a/clear/stp.py b/clear/stp.py new file mode 100644 index 0000000000..c3e3a4b098 --- /dev/null +++ b/clear/stp.py @@ -0,0 +1,46 @@ +import click +import utilities_common.cli as clicommon + +# +# This group houses Spanning_tree commands and subgroups +# + + +@click.group(cls=clicommon.AliasedGroup) +@click.pass_context +def spanning_tree(ctx): + '''Clear Spanning-tree counters''' + pass + + +@spanning_tree.group('statistics', cls=clicommon.AliasedGroup, invoke_without_command=True) +@click.pass_context +def stp_clr_stats(ctx): + if ctx.invoked_subcommand is None: + command = 'sudo stpctl clrstsall' + clicommon.run_command(command) + + +@stp_clr_stats.command('interface') +@click.argument('interface_name', metavar='', required=True) +@click.pass_context +def stp_clr_stats_intf(ctx, interface_name): + command = 'sudo stpctl clrstsintf ' + interface_name + clicommon.run_command(command) + + +@stp_clr_stats.command('vlan') +@click.argument('vlan_id', metavar='', required=True) +@click.pass_context +def stp_clr_stats_vlan(ctx, vlan_id): + command = 'sudo stpctl clrstsvlan ' + vlan_id + clicommon.run_command(command) + + +@stp_clr_stats.command('vlan-interface') +@click.argument('vlan_id', metavar='', required=True) +@click.argument('interface_name', metavar='', required=True) +@click.pass_context +def stp_clr_stats_vlan_intf(ctx, vlan_id, interface_name): + command = 'sudo stpctl clrstsvlanintf ' + vlan_id + ' ' + interface_name + clicommon.run_command(command) diff --git a/config/aaa.py b/config/aaa.py index 3c76187126..c540c4ad56 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -6,18 +6,38 @@ from jsonpatch import JsonPatchConflict from jsonpointer import JsonPointerException import utilities_common.cli as clicommon +from sonic_py_common.security_cipher import master_key_mgr +import getpass ADHOC_VALIDATION = True RADIUS_MAXSERVERS = 8 RADIUS_PASSKEY_MAX_LEN = 65 VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','" +TACACS_PASSKEY_MAX_LEN = 65 + +def rotate_tacplus_key(table_info): + #Extract table and nested_key names + table = table_info.split('|')[0] + nested_key = table_info.split('|')[1] + + # Re-encrypt with updated password + value = secure_cipher.encrypt_passkey("TACPLUS", secret) + add_table_kv(table, nested_key, 'passkey', value) + +# Security cipher Callback dir +# Note: Required for Security Cipher - password rotation feature +security_cipher_clbk_lookup = { + #TACPLUS + "rotate_tacplus_key": rotate_tacplus_key +} +secure_cipher = master_key_mgr(security_cipher_clbk_lookup) def is_secret(secret): return bool(re.match('^' + '[^ #,]*' + '$', secret)) -def add_table_kv(table, entry, key, val): - config_db = ValidatedConfigDBConnector(ConfigDBConnector()) +def add_table_kv(db, table, entry, key, val): + config_db = ValidatedConfigDBConnector(db.cfgdb) config_db.connect() try: config_db.mod_entry(table, entry, {key:val}) @@ -26,8 +46,8 @@ def add_table_kv(table, entry, key, val): ctx.fail("Invalid ConfigDB. Error: {}".format(e)) -def del_table_key(table, entry, key): - config_db = ValidatedConfigDBConnector(ConfigDBConnector()) +def del_table_key(db, table, entry, key): + config_db = ValidatedConfigDBConnector(db.cfgdb) config_db.connect() data = config_db.get_entry(table, entry) if data: @@ -56,68 +76,73 @@ def authentication(): # cmd: aaa authentication failthrough @click.command() @click.argument('option', type=click.Choice(["enable", "disable", "default"])) -def failthrough(option): +@clicommon.pass_db +def failthrough(db, option): """Allow AAA fail-through [enable | disable | default]""" if option == 'default': - del_table_key('AAA', 'authentication', 'failthrough') + del_table_key(db, 'AAA', 'authentication', 'failthrough') else: if option == 'enable': - add_table_kv('AAA', 'authentication', 'failthrough', True) + add_table_kv(db, 'AAA', 'authentication', 'failthrough', True) elif option == 'disable': - add_table_kv('AAA', 'authentication', 'failthrough', False) + add_table_kv(db, 'AAA', 'authentication', 'failthrough', False) authentication.add_command(failthrough) # cmd: aaa authentication fallback @click.command() @click.argument('option', type=click.Choice(["enable", "disable", "default"])) -def fallback(option): +@clicommon.pass_db +def fallback(db, option): """Allow AAA fallback [enable | disable | default]""" if option == 'default': - del_table_key('AAA', 'authentication', 'fallback') + del_table_key(db, 'AAA', 'authentication', 'fallback') else: if option == 'enable': - add_table_kv('AAA', 'authentication', 'fallback', True) + add_table_kv(db, 'AAA', 'authentication', 'fallback', True) elif option == 'disable': - add_table_kv('AAA', 'authentication', 'fallback', False) + add_table_kv(db, 'AAA', 'authentication', 'fallback', False) authentication.add_command(fallback) # cmd: aaa authentication debug @click.command() @click.argument('option', type=click.Choice(["enable", "disable", "default"])) -def debug(option): +@clicommon.pass_db +def debug(db, option): """AAA debug [enable | disable | default]""" if option == 'default': - del_table_key('AAA', 'authentication', 'debug') + del_table_key(db, 'AAA', 'authentication', 'debug') else: if option == 'enable': - add_table_kv('AAA', 'authentication', 'debug', True) + add_table_kv(db, 'AAA', 'authentication', 'debug', True) elif option == 'disable': - add_table_kv('AAA', 'authentication', 'debug', False) + add_table_kv(db, 'AAA', 'authentication', 'debug', False) authentication.add_command(debug) # cmd: aaa authentication trace @click.command() @click.argument('option', type=click.Choice(["enable", "disable", "default"])) -def trace(option): +@clicommon.pass_db +def trace(db, option): """AAA packet trace [enable | disable | default]""" if option == 'default': - del_table_key('AAA', 'authentication', 'trace') + del_table_key(db, 'AAA', 'authentication', 'trace') else: if option == 'enable': - add_table_kv('AAA', 'authentication', 'trace', True) + add_table_kv(db, 'AAA', 'authentication', 'trace', True) elif option == 'disable': - add_table_kv('AAA', 'authentication', 'trace', False) + add_table_kv(db, 'AAA', 'authentication', 'trace', False) authentication.add_command(trace) @click.command() -@click.argument('auth_protocol', nargs=-1, type=click.Choice(["radius", "tacacs+", "local", "default"])) -def login(auth_protocol): - """Switch login authentication [ {radius, tacacs+, local} | default ]""" - if len(auth_protocol) is 0: +@click.argument('auth_protocol', nargs=-1, type=click.Choice(["ldap", "radius", "tacacs+", "local", "default"])) +@clicommon.pass_db +def login(db, auth_protocol): + """Switch login authentication [ {ldap, radius, tacacs+, local} | default ]""" + if len(auth_protocol) == 0: click.echo('Argument "auth_protocol" is required') return elif len(auth_protocol) > 2: @@ -128,16 +153,16 @@ def login(auth_protocol): if len(auth_protocol) !=1: click.echo('Not a valid command') return - del_table_key('AAA', 'authentication', 'login') + del_table_key(db, 'AAA', 'authentication', 'login') else: val = auth_protocol[0] if len(auth_protocol) == 2: val2 = auth_protocol[1] good_ap = False if val == 'local': - if val2 == 'radius' or val2 == 'tacacs+': + if val2 == 'radius' or val2 == 'tacacs+' or val2 == 'ldap': good_ap = True - elif val == 'radius' or val == 'tacacs+': + elif val == 'radius' or val == 'tacacs+' or val == 'ldap': if val2 == 'local': good_ap = True if good_ap == True: @@ -146,22 +171,23 @@ def login(auth_protocol): click.echo('Not a valid command') return - add_table_kv('AAA', 'authentication', 'login', val) + add_table_kv(db, 'AAA', 'authentication', 'login', val) authentication.add_command(login) # cmd: aaa authorization @click.command() @click.argument('protocol', nargs=-1, type=click.Choice([ "tacacs+", "local", "tacacs+ local"])) -def authorization(protocol): +@clicommon.pass_db +def authorization(db, protocol): """Switch AAA authorization [tacacs+ | local | '\"tacacs+ local\"']""" if len(protocol) == 0: click.echo('Argument "protocol" is required') return if len(protocol) == 1 and (protocol[0] == 'tacacs+' or protocol[0] == 'local'): - add_table_kv('AAA', 'authorization', 'login', protocol[0]) + add_table_kv(db, 'AAA', 'authorization', 'login', protocol[0]) elif len(protocol) == 1 and protocol[0] == 'tacacs+ local': - add_table_kv('AAA', 'authorization', 'login', 'tacacs+,local') + add_table_kv(db, 'AAA', 'authorization', 'login', 'tacacs+,local') else: click.echo('Not a valid command') aaa.add_command(authorization) @@ -169,7 +195,8 @@ def authorization(protocol): # cmd: aaa accounting @click.command() @click.argument('protocol', nargs=-1, type=click.Choice(["disable", "tacacs+", "local", "tacacs+ local"])) -def accounting(protocol): +@clicommon.pass_db +def accounting(db, protocol): """Switch AAA accounting [disable | tacacs+ | local | '\"tacacs+ local\"']""" if len(protocol) == 0: click.echo('Argument "protocol" is required') @@ -177,11 +204,11 @@ def accounting(protocol): if len(protocol) == 1: if protocol[0] == 'tacacs+' or protocol[0] == 'local': - add_table_kv('AAA', 'accounting', 'login', protocol[0]) + add_table_kv(db, 'AAA', 'accounting', 'login', protocol[0]) elif protocol[0] == 'tacacs+ local': - add_table_kv('AAA', 'accounting', 'login', 'tacacs+,local') + add_table_kv(db, 'AAA', 'accounting', 'login', 'tacacs+,local') elif protocol[0] == 'disable': - del_table_key('AAA', 'accounting', 'login') + del_table_key(db, 'AAA', 'accounting', 'login') else: click.echo('Not a valid command') else: @@ -205,12 +232,13 @@ def default(ctx): @click.command() @click.argument('second', metavar='', type=click.IntRange(0, 60), required=False) @click.pass_context -def timeout(ctx, second): +@clicommon.pass_db +def timeout(db, ctx, second): """Specify TACACS+ server global timeout <0 - 60>""" if ctx.obj == 'default': - del_table_key('TACPLUS', 'global', 'timeout') + del_table_key(db, 'TACPLUS', 'global', 'timeout') elif second: - add_table_kv('TACPLUS', 'global', 'timeout', second) + add_table_kv(db, 'TACPLUS', 'global', 'timeout', second) else: click.echo('Argument "second" is required') tacacs.add_command(timeout) @@ -220,12 +248,13 @@ def timeout(ctx, second): @click.command() @click.argument('type', metavar='', type=click.Choice(["chap", "pap", "mschap", "login"]), required=False) @click.pass_context -def authtype(ctx, type): +@clicommon.pass_db +def authtype(db, ctx, type): """Specify TACACS+ server global auth_type [chap | pap | mschap | login]""" if ctx.obj == 'default': - del_table_key('TACPLUS', 'global', 'auth_type') + del_table_key(db, 'TACPLUS', 'global', 'auth_type') elif type: - add_table_kv('TACPLUS', 'global', 'auth_type', type) + add_table_kv(db, 'TACPLUS', 'global', 'auth_type', type) else: click.echo('Argument "type" is required') tacacs.add_command(authtype) @@ -234,15 +263,64 @@ def authtype(ctx, type): @click.command() @click.argument('secret', metavar='', required=False) +@click.option('-e', '--encrypt', help='Enable secret encryption', is_flag=True) +@click.option('-r', '--rotate', help='Rotate encryption secret', is_flag=True) @click.pass_context -def passkey(ctx, secret): +@clicommon.pass_db +def passkey(db, ctx, secret, encrypt, rotate): """Specify TACACS+ server global passkey """ if ctx.obj == 'default': - del_table_key('TACPLUS', 'global', 'passkey') + del_table_key(db, 'TACPLUS', 'global', 'passkey') elif secret: - add_table_kv('TACPLUS', 'global', 'passkey', secret) + if len(secret) > TACACS_PASSKEY_MAX_LEN: + click.echo('Maximum of %d chars can be configured' % TACACS_PASSKEY_MAX_LEN) + return + elif not is_secret(secret): + click.echo(VALID_CHARS_MSG) + return + + if encrypt: + try: + # Set new passwd if not set already + if secure_cipher.is_key_encrypt_enabled("TACPLUS", "global") is False: + #Register feature with Security Cipher module for the 1st time + secure_cipher.register("TACPLUS", rotate_tacplus_key) + passwd = getpass.getpass() + #Set new password for encryption + secure_cipher.set_feature_password("TACPLUS", passwd) + else: + #Check if password rotation is enabled + if rotate: + passwd = getpass.getpass() + #Rotate password for TACPLUS feature and re-encrypt the secret + secure_cipher.rotate_feature_passwd("TACPLUS", "TACPLUS|global", secret, passwd) + return + b64_encoded = secure_cipher.encrypt_passkey("TACPLUS", secret) + if b64_encoded is not None: + # Update key_encrypt flag + add_table_kv('TACPLUS', 'global', 'key_encrypt', True) + add_table_kv('TACPLUS', 'global', 'passkey', b64_encoded) + else: + #Deregister feature with Security Cipher module + secure_cipher.deregister("TACPLUS", rotate_tacplus_key) + click.echo('Passkey encryption failed: %s' % errs) + return + except (EOFError, KeyboardInterrupt): + #Deregister feature with Security Cipher module + secure_cipher.deregister("TACPLUS", rotate_tacplus_key) + add_table_kv('TACPLUS', 'global', 'key_encrypt', False) + click.echo('Input cancelled') + return + except Exception as e: + #Deregister feature with Security Cipher module + secure_cipher.deregister("TACPLUS", rotate_tacplus_key) + add_table_kv('TACPLUS', 'global', 'key_encrypt', False) + click.echo('Unexpected error: %s' %e) + return else: - click.echo('Argument "secret" is required') + # Update key_encrypt flag to false + add_table_kv('TACPLUS', 'global', 'key_encrypt', False) + add_table_kv('TACPLUS', 'global', 'passkey', secret) tacacs.add_command(passkey) default.add_command(passkey) @@ -251,19 +329,22 @@ def passkey(ctx, secret): @click.command() @click.argument('address', metavar='') @click.option('-t', '--timeout', help='Transmission timeout interval, default 5', type=int) -@click.option('-k', '--key', help='Shared secret') +@click.option('-k', '--key', help='Shared secret, stored in plaintext') +@click.option('-K', '--encrypted_key', help='Shared secret, stored in encrypted format') +@click.option('-r', '--rotate', help='Rotate encryption secret', is_flag=True) @click.option('-a', '--auth_type', help='Authentication type, default pap', type=click.Choice(["chap", "pap", "mschap", "login"])) @click.option('-o', '--port', help='TCP port range is 1 to 65535, default 49', type=click.IntRange(1, 65535), default=49) @click.option('-p', '--pri', help="Priority, default 1", type=click.IntRange(1, 64), default=1) @click.option('-m', '--use-mgmt-vrf', help="Management vrf, default is no vrf", is_flag=True) -def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): +@clicommon.pass_db +def add(address, timeout, key, encrypted_key, rotate, auth_type, port, pri, use_mgmt_vrf, encrypt): """Specify a TACACS+ server""" if ADHOC_VALIDATION: if not clicommon.is_ipaddress(address): click.echo('Invalid ip address') # TODO: MISSING CONSTRAINT IN YANG MODEL return - config_db = ValidatedConfigDBConnector(ConfigDBConnector()) + config_db = ValidatedConfigDBConnector(db.cfgdb) config_db.connect() old_data = config_db.get_entry('TACPLUS_SERVER', address) if old_data != {}: @@ -277,8 +358,54 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): data['auth_type'] = auth_type if timeout is not None: data['timeout'] = str(timeout) - if key is not None: - data['passkey'] = key + + if key and secret_key: + raise click.UsageError("You must provide either --key or --secret_key") + + if encrypted_key is not None: + try: + # Set new passwd if not set already + if secure_cipher.is_key_encrypt_enabled("TACPLUS_SERVER", address) is False: + #Register feature with Security Cipher module for the 1st time + secure_cipher.register("TACPLUS", rotate_tacplus_key) + passwd = getpass.getpass() + #Set new password for encryption + secure_cipher.set_feature_password("TACPLUS", passwd) + else: + #Check if password rotation is enabled + if rotate: + passwd = getpass.getpass() + #Rotate password for TACPLUS feature and re-encrypt the secret + secure_cipher.rotate_feature_passwd("TACPLUS", ("TACPLUS_SERVER|" + address), secret, passwd) + return + b64_encoded = secure_cipher.encrypt_passkey("TACPLUS", secret) + if b64_encoded is not None: + # Update key_encrypt flag + add_table_kv('TACPLUS_SERVER', address, 'key_encrypt', True) + add_table_kv('TACPLUS_SERVER', address, 'passkey', b64_encoded) + else: + #Deregister feature with Security Cipher module + secure_cipher.deregister("TACPLUS", rotate_tacplus_key) + click.echo('Passkey encryption failed: %s' % errs) + return + except (EOFError, KeyboardInterrupt): + #Deregister feature with Security Cipher module + secure_cipher.deregister("TACPLUS", rotate_tacplus_key) + add_table_kv('TACPLUS_SERVER', address, 'key_encrypt', False) + click.echo('Input cancelled') + return + except Exception as e: + #Deregister feature with Security Cipher module + secure_cipher.deregister("TACPLUS", rotate_tacplus_key) + add_table_kv('TACPLUS_SERVER', address, 'key_encrypt', False) + click.echo('Unexpected error: %s' %e) + return + else: + if key is not None: + # Update key_encrypt flag to false + add_table_kv('TACPLUS_SERVER', address, 'key_encrypt', False) + data['passkey'] = key + if use_mgmt_vrf : data['vrf'] = "mgmt" try: @@ -293,14 +420,15 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): # 'del' is keyword, replace with 'delete' @click.command() @click.argument('address', metavar='') -def delete(address): +@clicommon.pass_db +def delete(db, address): """Delete a TACACS+ server""" if ADHOC_VALIDATION: if not clicommon.is_ipaddress(address): click.echo('Invalid ip address') return - config_db = ValidatedConfigDBConnector(ConfigDBConnector()) + config_db = ValidatedConfigDBConnector(db.cfgdb) config_db.connect() try: config_db.set_entry('TACPLUS_SERVER', address, None) @@ -327,12 +455,13 @@ def default(ctx): @click.command() @click.argument('second', metavar='', type=click.IntRange(1, 60), required=False) @click.pass_context -def timeout(ctx, second): +@clicommon.pass_db +def timeout(db, ctx, second): """Specify RADIUS server global timeout <1 - 60>""" if ctx.obj == 'default': - del_table_key('RADIUS', 'global', 'timeout') + del_table_key(db, 'RADIUS', 'global', 'timeout') elif second: - add_table_kv('RADIUS', 'global', 'timeout', second) + add_table_kv(db, 'RADIUS', 'global', 'timeout', second) else: click.echo('Not support empty argument') radius.add_command(timeout) @@ -342,12 +471,13 @@ def timeout(ctx, second): @click.command() @click.argument('retries', metavar='', type=click.IntRange(0, 10), required=False) @click.pass_context -def retransmit(ctx, retries): +@clicommon.pass_db +def retransmit(db, ctx, retries): """Specify RADIUS server global retry attempts <0 - 10>""" if ctx.obj == 'default': - del_table_key('RADIUS', 'global', 'retransmit') + del_table_key(db, 'RADIUS', 'global', 'retransmit') elif retries != None: - add_table_kv('RADIUS', 'global', 'retransmit', retries) + add_table_kv(db, 'RADIUS', 'global', 'retransmit', retries) else: click.echo('Not support empty argument') radius.add_command(retransmit) @@ -357,12 +487,13 @@ def retransmit(ctx, retries): @click.command() @click.argument('type', metavar='', type=click.Choice(["chap", "pap", "mschapv2"]), required=False) @click.pass_context -def authtype(ctx, type): +@clicommon.pass_db +def authtype(db, ctx, type): """Specify RADIUS server global auth_type [chap | pap | mschapv2]""" if ctx.obj == 'default': - del_table_key('RADIUS', 'global', 'auth_type') + del_table_key(db, 'RADIUS', 'global', 'auth_type') elif type: - add_table_kv('RADIUS', 'global', 'auth_type', type) + add_table_kv(db, 'RADIUS', 'global', 'auth_type', type) else: click.echo('Not support empty argument') radius.add_command(authtype) @@ -372,10 +503,11 @@ def authtype(ctx, type): @click.command() @click.argument('secret', metavar='', required=False) @click.pass_context -def passkey(ctx, secret): +@clicommon.pass_db +def passkey(db, ctx, secret): """Specify RADIUS server global passkey """ if ctx.obj == 'default': - del_table_key('RADIUS', 'global', 'passkey') + del_table_key(db, 'RADIUS', 'global', 'passkey') elif secret: if len(secret) > RADIUS_PASSKEY_MAX_LEN: click.echo('Maximum of %d chars can be configured' % RADIUS_PASSKEY_MAX_LEN) @@ -383,7 +515,7 @@ def passkey(ctx, secret): elif not is_secret(secret): click.echo(VALID_CHARS_MSG) return - add_table_kv('RADIUS', 'global', 'passkey', secret) + add_table_kv(db, 'RADIUS', 'global', 'passkey', secret) else: click.echo('Not support empty argument') radius.add_command(passkey) @@ -392,10 +524,11 @@ def passkey(ctx, secret): @click.command() @click.argument('src_ip', metavar='', required=False) @click.pass_context -def sourceip(ctx, src_ip): +@clicommon.pass_db +def sourceip(db, ctx, src_ip): """Specify RADIUS server global source ip """ if ctx.obj == 'default': - del_table_key('RADIUS', 'global', 'src_ip') + del_table_key(db, 'RADIUS', 'global', 'src_ip') return elif not src_ip: click.echo('Not support empty argument') @@ -426,17 +559,18 @@ def sourceip(ctx, src_ip): if (ip in v6_invalid_list): click.echo('Invalid ip address') return - add_table_kv('RADIUS', 'global', 'src_ip', src_ip) + add_table_kv(db, 'RADIUS', 'global', 'src_ip', src_ip) radius.add_command(sourceip) default.add_command(sourceip) @click.command() @click.argument('nas_ip', metavar='', required=False) @click.pass_context -def nasip(ctx, nas_ip): +@clicommon.pass_db +def nasip(db, ctx, nas_ip): """Specify RADIUS server global NAS-IP|IPV6-Address """ if ctx.obj == 'default': - del_table_key('RADIUS', 'global', 'nas_ip') + del_table_key(db, 'RADIUS', 'global', 'nas_ip') return elif not nas_ip: click.echo('Not support empty argument') @@ -467,21 +601,22 @@ def nasip(ctx, nas_ip): if (ip in v6_invalid_list): click.echo('Invalid ip address') return - add_table_kv('RADIUS', 'global', 'nas_ip', nas_ip) + add_table_kv(db, 'RADIUS', 'global', 'nas_ip', nas_ip) radius.add_command(nasip) default.add_command(nasip) @click.command() @click.argument('option', type=click.Choice(["enable", "disable", "default"])) -def statistics(option): +@clicommon.pass_db +def statistics(db, option): """Specify RADIUS server global statistics [enable | disable | default]""" if option == 'default': - del_table_key('RADIUS', 'global', 'statistics') + del_table_key(db, 'RADIUS', 'global', 'statistics') else: if option == 'enable': - add_table_kv('RADIUS', 'global', 'statistics', True) + add_table_kv(db, 'RADIUS', 'global', 'statistics', 'true') elif option == 'disable': - add_table_kv('RADIUS', 'global', 'statistics', False) + add_table_kv(db, 'RADIUS', 'global', 'statistics', 'false') radius.add_command(statistics) @@ -496,7 +631,8 @@ def statistics(option): @click.option('-p', '--pri', help="Priority, default 1", type=click.IntRange(1, 64), default=1) @click.option('-m', '--use-mgmt-vrf', help="Management vrf, default is no vrf", is_flag=True) @click.option('-s', '--source-interface', help='Source Interface') -def add(address, retransmit, timeout, key, auth_type, auth_port, pri, use_mgmt_vrf, source_interface): +@clicommon.pass_db +def add(db, address, retransmit, timeout, key, auth_type, auth_port, pri, use_mgmt_vrf, source_interface): """Specify a RADIUS server""" if ADHOC_VALIDATION: @@ -508,7 +644,7 @@ def add(address, retransmit, timeout, key, auth_type, auth_port, pri, use_mgmt_v click.echo('--key: ' + VALID_CHARS_MSG) return - config_db = ValidatedConfigDBConnector(ConfigDBConnector()) + config_db = ValidatedConfigDBConnector(db.cfgdb) config_db.connect() old_data = config_db.get_table('RADIUS_SERVER') if address in old_data : @@ -556,10 +692,11 @@ def add(address, retransmit, timeout, key, auth_type, auth_port, pri, use_mgmt_v # 'del' is keyword, replace with 'delete' @click.command() @click.argument('address', metavar='') -def delete(address): +@clicommon.pass_db +def delete(db, address): """Delete a RADIUS server""" - config_db = ValidatedConfigDBConnector(ConfigDBConnector()) + config_db = ValidatedConfigDBConnector(db.cfgdb) config_db.connect() try: config_db.set_entry('RADIUS_SERVER', address, None) diff --git a/config/bgp_cli.py b/config/bgp_cli.py new file mode 100644 index 0000000000..a5a565359a --- /dev/null +++ b/config/bgp_cli.py @@ -0,0 +1,192 @@ +import click +import utilities_common.cli as clicommon + +from sonic_py_common import logger +from utilities_common.bgp import ( + CFG_BGP_DEVICE_GLOBAL, + BGP_DEVICE_GLOBAL_KEY, + SYSLOG_IDENTIFIER, + to_str, +) + + +log = logger.Logger(SYSLOG_IDENTIFIER) +log.set_min_log_priority_info() + + +# +# BGP 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]) + + +# +# BGP handlers -------------------------------------------------------------------------------------------------------- +# + + +def tsa_handler(ctx, db, state): + """ Handle config updates for Traffic-Shift-Away (TSA) feature """ + + table = CFG_BGP_DEVICE_GLOBAL + key = BGP_DEVICE_GLOBAL_KEY + data = { + "tsa_enabled": state, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured TSA state: {}".format(to_str(state))) + except Exception as e: + log.log_error("Failed to configure TSA state: {}".format(str(e))) + ctx.fail(str(e)) + + +def wcmp_handler(ctx, db, state): + """ Handle config updates for Weighted-Cost Multi-Path (W-ECMP) feature """ + + table = CFG_BGP_DEVICE_GLOBAL + key = BGP_DEVICE_GLOBAL_KEY + data = { + "wcmp_enabled": state, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured W-ECMP state: {}".format(to_str(state))) + except Exception as e: + log.log_error("Failed to configure W-ECMP state: {}".format(str(e))) + ctx.fail(str(e)) + + +# +# BGP device-global --------------------------------------------------------------------------------------------------- +# + + +@click.group( + name="device-global", + cls=clicommon.AliasedGroup +) +def DEVICE_GLOBAL(): + """ Configure BGP device global state """ + + pass + + +# +# BGP device-global tsa ----------------------------------------------------------------------------------------------- +# + + +@DEVICE_GLOBAL.group( + name="tsa", + cls=clicommon.AliasedGroup +) +def DEVICE_GLOBAL_TSA(): + """ Configure Traffic-Shift-Away (TSA) feature """ + + pass + + +@DEVICE_GLOBAL_TSA.command( + name="enabled" +) +@clicommon.pass_db +@click.pass_context +def DEVICE_GLOBAL_TSA_ENABLED(ctx, db): + """ Enable Traffic-Shift-Away (TSA) feature """ + + tsa_handler(ctx, db, "true") + + +@DEVICE_GLOBAL_TSA.command( + name="disabled" +) +@clicommon.pass_db +@click.pass_context +def DEVICE_GLOBAL_TSA_DISABLED(ctx, db): + """ Disable Traffic-Shift-Away (TSA) feature """ + + tsa_handler(ctx, db, "false") + + +# +# BGP device-global w-ecmp -------------------------------------------------------------------------------------------- +# + + +@DEVICE_GLOBAL.group( + name="w-ecmp", + cls=clicommon.AliasedGroup +) +def DEVICE_GLOBAL_WCMP(): + """ Configure Weighted-Cost Multi-Path (W-ECMP) feature """ + + pass + + +@DEVICE_GLOBAL_WCMP.command( + name="enabled" +) +@clicommon.pass_db +@click.pass_context +def DEVICE_GLOBAL_WCMP_ENABLED(ctx, db): + """ Enable Weighted-Cost Multi-Path (W-ECMP) feature """ + + wcmp_handler(ctx, db, "true") + + +@DEVICE_GLOBAL_WCMP.command( + name="disabled" +) +@clicommon.pass_db +@click.pass_context +def DEVICE_GLOBAL_WCMP_DISABLED(ctx, db): + """ Disable Weighted-Cost Multi-Path (W-ECMP) feature """ + + wcmp_handler(ctx, db, "false") diff --git a/config/chassis_modules.py b/config/chassis_modules.py old mode 100644 new mode 100755 index e640779d16..20b8c9093a --- a/config/chassis_modules.py +++ b/config/chassis_modules.py @@ -1,8 +1,14 @@ #!/usr/sbin/env python import click - +import time +import re +import subprocess import utilities_common.cli as clicommon +from utilities_common.chassis import is_smartswitch, get_all_dpus + +TIMEOUT_SECS = 10 + # # 'chassis_modules' group ('config chassis_modules ...') @@ -17,12 +23,94 @@ def modules(): """Configure chassis modules""" pass + +def get_config_module_state(db, chassis_module_name): + config_db = db.cfgdb + fvs = config_db.get_entry('CHASSIS_MODULE', chassis_module_name) + if not fvs: + if is_smartswitch(): + return 'down' + else: + return 'up' + else: + return fvs['admin_status'] + + +# +# Name: check_config_module_state_with_timeout +# return: True: timeout, False: not timeout +# +def check_config_module_state_with_timeout(ctx, db, chassis_module_name, state): + counter = 0 + while get_config_module_state(db, chassis_module_name) != state: + time.sleep(1) + counter += 1 + if counter >= TIMEOUT_SECS: + ctx.fail("get_config_module_state {} timeout".format(chassis_module_name)) + return True + return False + + +def get_asic_list_from_db(chassisdb, chassis_module_name): + asic_list = [] + asics_keys_list = chassisdb.keys("CHASSIS_STATE_DB", "CHASSIS_FABRIC_ASIC_TABLE*") + for asic_key in asics_keys_list: + name = chassisdb.get("CHASSIS_STATE_DB", asic_key, "name") + if name == chassis_module_name: + asic_id = int(re.search(r"(\d+)$", asic_key).group()) + asic_list.append(asic_id) + return asic_list + + +# +# Syntax: fabric_module_set_admin_status <'up'/'down'> +# +def fabric_module_set_admin_status(db, chassis_module_name, state): + chassisdb = db.db + chassisdb.connect("CHASSIS_STATE_DB") + asic_list = get_asic_list_from_db(chassisdb, chassis_module_name) + + if len(asic_list) == 0: + return + + if state == "down": + for asic in asic_list: + click.echo("Stop swss@{} and peer services".format(asic)) + clicommon.run_command(['sudo', 'systemctl', 'stop', 'swss@{}.service'.format(asic)]) + + is_active = subprocess.call(["systemctl", "is-active", "--quiet", "swss@{}.service".format(asic)]) + + if is_active == 0: # zero active, non-zero, inactive + click.echo("Stop swss@{} and peer services failed".format(asic)) + return + + click.echo("Delete related CAHSSIS_FABRIC_ASIC_TABLE entries") + + for asic in asic_list: + chassisdb.delete("CHASSIS_STATE_DB", "CHASSIS_FABRIC_ASIC_TABLE|asic" + str(asic)) + + # Start the services in case of the users just execute issue command "systemctl stop swss@/syncd@" + # without bring down the hardware + for asic in asic_list: + # To address systemd service restart limit by resetting the count + clicommon.run_command(['sudo', 'systemctl', 'reset-failed', 'swss@{}.service'.format(asic)]) + click.echo("Start swss@{} and peer services".format(asic)) + clicommon.run_command(['sudo', 'systemctl', 'start', 'swss@{}.service'.format(asic)]) + elif state == "up": + for asic in asic_list: + click.echo("Start swss@{} and peer services".format(asic)) + clicommon.run_command(['sudo', 'systemctl', 'start', 'swss@{}.service'.format(asic)]) + # # 'shutdown' subcommand ('config chassis_modules shutdown ...') # @modules.command('shutdown') @clicommon.pass_db -@click.argument('chassis_module_name', metavar='', required=True) +@click.argument('chassis_module_name', + metavar='', + required=True, + type=click.Choice(get_all_dpus(), case_sensitive=False) if is_smartswitch() else str + ) def shutdown_chassis_module(db, chassis_module_name): """Chassis-module shutdown of module""" config_db = db.cfgdb @@ -30,20 +118,49 @@ def shutdown_chassis_module(db, chassis_module_name): if not chassis_module_name.startswith("SUPERVISOR") and \ not chassis_module_name.startswith("LINE-CARD") and \ - not chassis_module_name.startswith("FABRIC-CARD"): - ctx.fail("'module_name' has to begin with 'SUPERVISOR', 'LINE-CARD' or 'FABRIC-CARD'") + not chassis_module_name.startswith("FABRIC-CARD") and \ + not chassis_module_name.startswith("DPU"): + ctx.fail("'module_name' has to begin with 'SUPERVISOR', 'LINE-CARD', 'FABRIC-CARD', 'DPU'") + + # To avoid duplicate operation + if get_config_module_state(db, chassis_module_name) == 'down': + click.echo("Module {} is already in down state".format(chassis_module_name)) + return + click.echo("Shutting down chassis module {}".format(chassis_module_name)) fvs = {'admin_status': 'down'} config_db.set_entry('CHASSIS_MODULE', chassis_module_name, fvs) + if chassis_module_name.startswith("FABRIC-CARD"): + if not check_config_module_state_with_timeout(ctx, db, chassis_module_name, 'down'): + fabric_module_set_admin_status(db, chassis_module_name, 'down') # # 'startup' subcommand ('config chassis_modules startup ...') # @modules.command('startup') @clicommon.pass_db -@click.argument('chassis_module_name', metavar='', required=True) +@click.argument('chassis_module_name', + metavar='', + required=True, + type=click.Choice(get_all_dpus(), case_sensitive=False) if is_smartswitch() else str + ) def startup_chassis_module(db, chassis_module_name): """Chassis-module startup of module""" config_db = db.cfgdb + ctx = click.get_current_context() + + # To avoid duplicate operation + if get_config_module_state(db, chassis_module_name) == 'up': + click.echo("Module {} is already set to up state".format(chassis_module_name)) + return + + click.echo("Starting up chassis module {}".format(chassis_module_name)) + if is_smartswitch(): + fvs = {'admin_status': 'up'} + config_db.set_entry('CHASSIS_MODULE', chassis_module_name, fvs) + else: + config_db.set_entry('CHASSIS_MODULE', chassis_module_name, None) - config_db.set_entry('CHASSIS_MODULE', chassis_module_name, None) + if chassis_module_name.startswith("FABRIC-CARD"): + if not check_config_module_state_with_timeout(ctx, db, chassis_module_name, 'up'): + fabric_module_set_admin_status(db, chassis_module_name, 'up') diff --git a/config/fabric.py b/config/fabric.py index a3870589ae..84607d9ebe 100644 --- a/config/fabric.py +++ b/config/fabric.py @@ -2,7 +2,10 @@ import utilities_common.cli as clicommon import utilities_common.multi_asic as multi_asic_util from sonic_py_common import multi_asic -from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector +from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector, APP_FABRIC_PORT_TABLE_NAME + +FABRIC_PORT_STATUS_TABLE_PREFIX = APP_FABRIC_PORT_TABLE_NAME+"|" + # # 'config fabric ...' @@ -66,19 +69,13 @@ def isolate(portid, namespace): # @port.command() @click.argument('portid', metavar='', required=True) +@click.option('-f', '--force', is_flag=True, default=False, help='Force to unisolate a link even if it is auto isolated.') @multi_asic_util.multi_asic_click_option_namespace -def unisolate(portid, namespace): +def unisolate(portid, namespace, force): """FABRIC PORT unisolate """ ctx = click.get_current_context() - if not portid.isdigit(): - ctx.fail("Invalid portid") - - n_asics = multi_asic.get_num_asics() - if n_asics > 1 and namespace is None: - ctx.fail('Must specify asic') - # Connect to config database config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) config_db.connect() @@ -87,6 +84,37 @@ def unisolate(portid, namespace): state_db = SonicV2Connector(use_unix_socket_path=True, namespace=namespace) state_db.connect(state_db.STATE_DB, False) + n_asics = multi_asic.get_num_asics() + if n_asics > 1 and namespace is None: + ctx.fail( 'Must specify asic' ) + + # If "all" is specified then unisolate all ports. + if portid == "all": + port_keys = state_db.keys(state_db.STATE_DB, FABRIC_PORT_STATUS_TABLE_PREFIX + '*') + for port_key in port_keys: + port_data = state_db.get_all(state_db.STATE_DB, port_key) + if "REMOTE_PORT" in port_data: + port_number = int( port_key.replace( "FABRIC_PORT_TABLE|PORT", "" ) ) + + # Make sure configuration data exists + portName = f'Fabric{port_number}' + portConfigData = config_db.get_all(config_db.CONFIG_DB, "FABRIC_PORT|" + portName) + if not bool( portConfigData ): + ctx.fail( "Fabric monitor configuration data not present" ) + + # Update entry + config_db.mod_entry( "FABRIC_PORT", portName, {'isolateStatus': False} ) + if force: + forceShutCnt = int( portConfigData['forceUnisolateStatus'] ) + forceShutCnt += 1 + config_db.mod_entry( "FABRIC_PORT", portName, + {'forceUnisolateStatus': forceShutCnt}) + + return + + if not portid.isdigit(): + ctx.fail( "Invalid portid" ) + # check if the port is actually in use portName = f'PORT{portid}' portStateData = state_db.get_all(state_db.STATE_DB, "FABRIC_PORT_TABLE|" + portName) @@ -102,6 +130,15 @@ def unisolate(portid, namespace): # Update entry config_db.mod_entry("FABRIC_PORT", portName, {'isolateStatus': False}) + if force: + forceShutCnt = int( portConfigData['forceUnisolateStatus'] ) + forceShutCnt += 1 + config_db.mod_entry( "FABRIC_PORT", portName, + {'forceUnisolateStatus': forceShutCnt}) + + click.echo("Force unisolate the link.") + click.echo("It will clear all fabric link monitoring status for this link!") + # # 'config fabric port monitor ...' # @@ -157,6 +194,39 @@ def error_threshold(crccells, rxcells, namespace): config_db.mod_entry("FABRIC_MONITOR", "FABRIC_MONITOR_DATA", {'monErrThreshCrcCells': crccells, 'monErrThreshRxCells': rxcells}) +def setFabricPortMonitorState(state, namespace, ctx): + """ set the fabric port monitor state""" + # Connect to config database + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_db.connect() + + # Make sure configuration data exists + monitorData = config_db.get_all(config_db.CONFIG_DB, "FABRIC_MONITOR|FABRIC_MONITOR_DATA") + if not bool(monitorData): + ctx.fail("Fabric monitor configuration data not present") + + # Update entry + config_db.mod_entry("FABRIC_MONITOR", "FABRIC_MONITOR_DATA", + {'monState': state}) + +# +# 'config fabric port montior state ' +# +@monitor.command() +@click.argument('state', metavar='', required=True) +@multi_asic_util.multi_asic_click_option_namespace +def state(state, namespace): + """FABRIC PORT MONITOR STATE configuration tasks""" + ctx = click.get_current_context() + + n_asics = multi_asic.get_num_asics() + if n_asics > 1 and namespace is None: + ns_list = multi_asic.get_namespace_list() + for namespace in ns_list: + setFabricPortMonitorState(state, namespace, ctx) + else: + setFabricPortMonitorState(state, namespace, ctx) + # # 'config fabric port monitor poll ...' # @@ -245,3 +315,45 @@ def recovery(pollcount, namespace): {"monPollThreshRecovery": pollcount}) +# +# 'config fabric monitor ...' +# +@fabric.group(cls=clicommon.AbbreviationGroup, name='monitor') +def capacity_monitor(): + """FABRIC MONITOR configuration tasks""" + pass + +# +# 'config fabric monitor capacity...' +# +@capacity_monitor.group(cls=clicommon.AbbreviationGroup) +def capacity(): + """FABRIC MONITOR CAPACITY configuration tasks""" + pass + +# +# 'config fabric monitor capacity threshold ' +# +@capacity.command() +@click.argument('capacitythreshold', metavar='', required=True, type=int) +def threshold(capacitythreshold): + """FABRIC CAPACITY MONITOR THRESHOLD configuration tasks""" + ctx = click.get_current_context() + + if capacitythreshold < 5 or capacitythreshold > 250: + ctx.fail("threshold must be in range 5...250") + + namespaces = multi_asic.get_namespace_list() + for idx, namespace in enumerate(namespaces, start=1): + # Connect to config database + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_db.connect() + + # Make sure configuration data exists + monitorData = config_db.get_all(config_db.CONFIG_DB, "FABRIC_MONITOR|FABRIC_MONITOR_DATA") + if not bool(monitorData): + ctx.fail("Fabric monitor configuration data not present") + + # Update entry + config_db.mod_entry("FABRIC_MONITOR", "FABRIC_MONITOR_DATA", + {"monCapacityThreshWarn": capacitythreshold}) diff --git a/config/kdump.py b/config/kdump.py index c61e79099d..f3e981ace9 100644 --- a/config/kdump.py +++ b/config/kdump.py @@ -1,18 +1,18 @@ import sys - import click +import os from utilities_common.cli import AbbreviationGroup, pass_db - - +from ipaddress import ip_address, AddressValueError +import re # # 'kdump' group ('sudo config kdump ...') # + @click.group(cls=AbbreviationGroup, name="kdump") def kdump(): """Configure the KDUMP mechanism""" pass - def check_kdump_table_existence(kdump_table): """Checks whether the 'KDUMP' table is configured in Config DB. @@ -32,10 +32,16 @@ def check_kdump_table_existence(kdump_table): sys.exit(2) +def echo_reboot_warning(): + """Prints the warning message about reboot requirements.""" + click.echo("KDUMP configuration changes may require a reboot to take effect.") + click.echo("Save SONiC configuration using 'config save' before issuing the reboot command.") # # 'disable' command ('sudo config kdump disable') # -@kdump.command(name="disable", short_help="Disable the KDUMP mechanism") + + +@kdump.command(name="disable", help="Disable the KDUMP mechanism") @pass_db def kdump_disable(db): """Disable the KDUMP mechanism""" @@ -43,14 +49,14 @@ def kdump_disable(db): check_kdump_table_existence(kdump_table) db.cfgdb.mod_entry("KDUMP", "config", {"enabled": "false"}) - click.echo("KDUMP configuration changes may require a reboot to take effect.") - click.echo("Save SONiC configuration using 'config save' before issuing the reboot command.") - + echo_reboot_warning() # # 'enable' command ('sudo config kdump enable') # -@kdump.command(name="enable", short_help="Enable the KDUMP mechanism") + + +@kdump.command(name="enable", help="Enable the KDUMP mechanism") @pass_db def kdump_enable(db): """Enable the KDUMP mechanism""" @@ -58,14 +64,14 @@ def kdump_enable(db): check_kdump_table_existence(kdump_table) db.cfgdb.mod_entry("KDUMP", "config", {"enabled": "true"}) - click.echo("KDUMP configuration changes may require a reboot to take effect.") - click.echo("Save SONiC configuration using 'config save' before issuing the reboot command.") - + echo_reboot_warning() # # 'memory' command ('sudo config kdump memory ...') # -@kdump.command(name="memory", short_help="Configure the memory for KDUMP mechanism") + + +@kdump.command(name="memory", help="Configure the memory for KDUMP mechanism") @click.argument('kdump_memory', metavar='', required=True) @pass_db def kdump_memory(db, kdump_memory): @@ -74,14 +80,14 @@ def kdump_memory(db, kdump_memory): check_kdump_table_existence(kdump_table) db.cfgdb.mod_entry("KDUMP", "config", {"memory": kdump_memory}) - click.echo("KDUMP configuration changes may require a reboot to take effect.") - click.echo("Save SONiC configuration using 'config save' before issuing the reboot command.") - + echo_reboot_warning() # -# 'num_dumps' command ('sudo config keump num_dumps ...') +# 'num_dumps' command ('sudo config kdump num_dumps ...') # -@kdump.command(name="num_dumps", short_help="Configure the maximum dump files of KDUMP mechanism") + + +@kdump.command(name="num_dumps", help="Configure the maximum dump files of KDUMP mechanism") @click.argument('kdump_num_dumps', metavar='', required=True, type=int) @pass_db def kdump_num_dumps(db, kdump_num_dumps): @@ -90,3 +96,160 @@ def kdump_num_dumps(db, kdump_num_dumps): check_kdump_table_existence(kdump_table) db.cfgdb.mod_entry("KDUMP", "config", {"num_dumps": kdump_num_dumps}) + echo_reboot_warning() + + +@kdump.command(name="remote", help="Configure the remote enable/disable for KDUMP mechanism") +@click.argument('action', metavar='', required=True, type=click.STRING) # Corrected this line +@pass_db +def remote(db, action): + """Enable or disable remote kdump feature""" + kdump_table = db.cfgdb.get_table("KDUMP") + check_kdump_table_existence(kdump_table) + + # Get the current status of the remote feature as string + current_status = kdump_table["config"].get("remote", "false").lower() + + if action.lower() == 'enable': + if current_status == "true": + click.echo("Remote kdump feature is already enabled.") + else: + db.cfgdb.mod_entry("KDUMP", "config", {"remote": "true"}) + click.echo("Remote kdump feature enabled.") + echo_reboot_warning() + elif action.lower() == 'disable': + if current_status == "false": + click.echo("Remote kdump feature is already disabled.") + else: + db.cfgdb.mod_entry("KDUMP", "config", {"remote": "false"}) + click.echo("Remote kdump feature disabled.") + echo_reboot_warning() + else: + click.echo("Invalid action. Use 'enable' or 'disable'.") + + +@kdump.group(name="add", help="Add configuration items to KDUMP") +def add(): + """Group of commands to add configuration items to KDUMP""" + pass + + +@add.command(name="ssh_string", help="Add an SSH string to the KDUMP configuration") +@click.argument('ssh_string', metavar='', required=True) +@pass_db +def add_ssh_key(db, ssh_string): + """Add an SSH string to KDUMP configuration""" + + def is_valid_ssh_key(ssh_string): + """Validate the SSH key format""" + # Check if it contains username and hostname/IP (format: username@host) + if "@" not in ssh_string: + return "Invalid format. SSH key must be in 'username@host' format." + + username, host = ssh_string.split("@", 1) + + # Validate username + if not username or not username.isalnum(): + return "Invalid username. Ensure it contains only alphanumeric characters." + + # Validate host (IP or hostname) + try: + # Check if it's a valid IP address + ip_address(host) + except AddressValueError: + # If not an IP, validate hostname + hostname_regex = r'^[a-zA-Z0-9.-]+$' + if not re.match(hostname_regex, host) or host.startswith('-') or host.endswith('-'): + return "Invalid host. Must be a valid IP or hostname." + + return None # Validation successful + + kdump_table = db.cfgdb.get_table("KDUMP") + check_kdump_table_existence(kdump_table) + current_status = kdump_table["config"].get("remote", "false").lower() + + if current_status == 'false': + click.echo("Remote feature is not enabled. Please enable the remote feature first.") + return + + # Validate SSH key + validation_error = is_valid_ssh_key(ssh_string) + if validation_error: + click.echo(f"Error: {validation_error}") + return + + # Add or update the 'ssh_key' entry in the KDUMP table + db.cfgdb.mod_entry("KDUMP", "config", {"ssh_string": ssh_string}) + click.echo(f"SSH string added to KDUMP configuration: {ssh_string}") + + +@add.command(name="ssh_path", help="Add an SSH path to the KDUMP configuration") +@click.argument('ssh_path', metavar='', required=True) +@pass_db +def add_ssh_path(db, ssh_path): + """Add an SSH path to KDUMP configuration""" + + def is_valid_ssh_path(ssh_path): + """Validate the SSH path""" + # Check if the path is absolute + if not os.path.isabs(ssh_path): + return "Invalid path. SSH path must be an absolute path." + + # (Optional) Check if the path exists on the system + if not os.path.exists(ssh_path): + return f"Invalid path. The path '{ssh_path}' does not exist." + + return None # Validation successful + + kdump_table = db.cfgdb.get_table("KDUMP") + check_kdump_table_existence(kdump_table) + current_status = kdump_table["config"].get("remote", "false").lower() + if current_status == 'false': + click.echo("Remote feature is not enabled. Please enable the remote feature first.") + return + + # Validate SSH path + validation_error = is_valid_ssh_path(ssh_path) + if validation_error: + click.echo(f"Error: {validation_error}") + return + + # Add or update the 'ssh_path' entry in the KDUMP table + db.cfgdb.mod_entry("KDUMP", "config", {"ssh_path": ssh_path}) + click.echo(f"SSH path added to KDUMP configuration: {ssh_path}") + + +@kdump.group(name="remove", help="remove configuration items to KDUMP") +def remove(): + """Group of commands to remove configuration items to KDUMP""" + pass + + +@remove.command(name="ssh_string", help="Remove the SSH string from the KDUMP configuration") +@pass_db +def remove_ssh_string(db): + """Remove the SSH string from KDUMP configuration""" + kdump_table = db.cfgdb.get_table("KDUMP") + check_kdump_table_existence(kdump_table) + + # Check if ssh_string exists + if "ssh_string" in kdump_table["config"]: + db.cfgdb.mod_entry("KDUMP", "config", {"ssh_string": None}) + click.echo("SSH string removed from KDUMP configuration.") + else: + click.echo("SSH string not found in KDUMP configuration.") + + +@remove.command(name="ssh_path", help="Remove the SSH path from the KDUMP configuration") +@pass_db +def remove_ssh_path(db): + """Remove the SSH path from KDUMP configuration""" + kdump_table = db.cfgdb.get_table("KDUMP") + check_kdump_table_existence(kdump_table) + + # Check if ssh_string exists + if "ssh_path" in kdump_table["config"]: + db.cfgdb.mod_entry("KDUMP", "config", {"ssh_path": None}) + click.echo("SSH path removed from KDUMP configuration.") + else: + click.echo("SSH path not found in KDUMP configuration.") diff --git a/config/main.py b/config/main.py index f336fb5818..45224de7b8 100644 --- a/config/main.py +++ b/config/main.py @@ -1,6 +1,8 @@ #!/usr/sbin/env python +import threading import click +import concurrent.futures import datetime import ipaddress import json @@ -14,11 +16,14 @@ import time import itertools import copy +import tempfile +import sonic_yang from jsonpatch import JsonPatchConflict from jsonpointer import JsonPointerException from collections import OrderedDict -from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat +from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat, extract_scope +from generic_config_updater.gu_common import HOST_NAMESPACE, GenericConfigUpdaterError from minigraph import parse_device_desc_xml, minigraph_encoder from natsort import natsorted from portconfig import get_child_ports @@ -26,17 +31,20 @@ from sonic_py_common import device_info, multi_asic from sonic_py_common.general import getstatusoutput_noshell from sonic_py_common.interface import get_interface_table_name, get_port_table_name, get_intf_longname +from sonic_yang_cfg_generator import SonicYangCfgDbGenerator from utilities_common import util_base from swsscommon import swsscommon -from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector +from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector, ConfigDBPipeConnector, \ + isInterfaceNameValid, IFACE_NAME_MAX_LEN from utilities_common.db import Db from utilities_common.intf_filter import parse_interface_in_filter from utilities_common import bgp_util import utilities_common.cli as clicommon -from utilities_common.helper import get_port_pbh_binding, get_port_acl_binding +from utilities_common.helper import get_port_pbh_binding, get_port_acl_binding, update_config from utilities_common.general import load_db_config, load_module_from_source from .validated_config_db_connector import ValidatedConfigDBConnector import utilities_common.multi_asic as multi_asic_util +from utilities_common.flock import try_lock from .utils import log @@ -53,10 +61,13 @@ from . import vlan from . import vxlan from . import plugins -from .config_mgmt import ConfigMgmtDPB, ConfigMgmt +from .config_mgmt import ConfigMgmtDPB, ConfigMgmt, YANG_DIR from . import mclag from . import syslog +from . import switchport from . import dns +from . import bgp_cli +from . import stp # mock masic APIs for unit test try: @@ -96,7 +107,6 @@ CFG_PORTCHANNEL_PREFIX = "PortChannel" CFG_PORTCHANNEL_PREFIX_LEN = 11 -CFG_PORTCHANNEL_NAME_TOTAL_LEN_MAX = 15 CFG_PORTCHANNEL_MAX_VAL = 9999 CFG_PORTCHANNEL_NO="<0-9999>" @@ -104,6 +114,12 @@ PORT_SPEED = "speed" PORT_TPID = "tpid" DEFAULT_TPID = "0x8100" +PORT_MODE = "switchport_mode" + +DOM_CONFIG_SUPPORTED_SUBPORTS = ['0', '1'] + +VNET_NAME_MAX_LEN = 15 +GUID_MAX_LEN = 255 asic_type = None @@ -113,6 +129,12 @@ GRE_TYPE_RANGE = click.IntRange(min=0, max=65535) ADHOC_VALIDATION = True +if os.environ.get("UTILITIES_UNIT_TESTING", "0") in ("1", "2"): + temp_system_reload_lockfile = tempfile.NamedTemporaryFile() + SYSTEM_RELOAD_LOCK = temp_system_reload_lockfile.name +else: + SYSTEM_RELOAD_LOCK = "/etc/sonic/reload.lock" + # Load sonic-cfggen from source since /usr/local/bin/sonic-cfggen does not have .py extension. sonic_cfggen = load_module_from_source('sonic_cfggen', '/usr/local/bin/sonic-cfggen') @@ -142,6 +164,14 @@ def read_json_file(fileName): raise Exception(str(e)) return result +# write given JSON file +def write_json_file(json_input, fileName): + try: + with open(fileName, 'w') as f: + json.dump(json_input, f, indent=4) + except Exception as e: + raise Exception(str(e)) + def _get_breakout_options(ctx, args, incomplete): """ Provides dynamic mode option as per user argument i.e. interface name """ all_mode_options = [] @@ -232,7 +262,7 @@ def breakout_Ports(cm, delPorts=list(), portJson=dict(), force=False, \ click.echo("*** Printing dependencies ***") for dep in deps: click.echo(dep) - sys.exit(0) + sys.exit(1) else: click.echo("[ERROR] Port breakout Failed!!! Opting Out") raise click.Abort() @@ -391,6 +421,51 @@ def is_vrf_exists(config_db, vrf_name): return False + +def is_vnet_exists(config_db, vnet_name): + """Check if VNET exists + """ + keys = config_db.get_keys("VNET") + if keys: + if vnet_name in keys: + return True + + return False + + +def is_specific_vnet_route_exists(config_db, vnet_name, prefix): + """Check if VNET ROUTE WITH PREFIX exists + """ + keys = config_db.get_keys("VNET_ROUTE_TUNNEL") + if keys: + for k in keys: + if k[0] == vnet_name and k[1] == prefix: + return True + + return False + + +def is_vnet_route_exists(config_db, vnet_name): + """Check if VNET ROUTE exists + """ + keys = config_db.get_keys("VNET_ROUTE_TUNNEL") + if keys: + for k in keys: + if k[0] == vnet_name: + return True + + return False + + +def vnet_name_is_valid(ctx, vnet_name): + """Check if the vnet name is valid + """ + if not vnet_name.startswith("Vnet"): + ctx.fail("'vnet_name' must begin with 'Vnet'.") + if len(vnet_name) > VNET_NAME_MAX_LEN: + ctx.fail("'vnet_name' length should not exceed {} characters".format(VNET_NAME_MAX_LEN)) + + def is_interface_bind_to_vrf(config_db, interface_name): """Get interface if bind to vrf or not """ @@ -398,7 +473,7 @@ def is_interface_bind_to_vrf(config_db, interface_name): if table_name == "": return False entry = config_db.get_entry(table_name, interface_name) - if entry and entry.get("vrf_name"): + if entry and (entry.get("vrf_name") or entry.get("vnet_name")): return True return False @@ -412,7 +487,7 @@ def is_portchannel_name_valid(portchannel_name): if (portchannel_name[CFG_PORTCHANNEL_PREFIX_LEN:].isdigit() is False or int(portchannel_name[CFG_PORTCHANNEL_PREFIX_LEN:]) > CFG_PORTCHANNEL_MAX_VAL) : return False - if len(portchannel_name) > CFG_PORTCHANNEL_NAME_TOTAL_LEN_MAX: + if not isInterfaceNameValid(portchannel_name): return False return True @@ -477,7 +552,7 @@ def get_port_namespace(port): def del_interface_bind_to_vrf(config_db, vrf_name): """del interface bind to vrf """ - tables = ['INTERFACE', 'PORTCHANNEL_INTERFACE', 'VLAN_INTERFACE', 'LOOPBACK_INTERFACE'] + tables = ['INTERFACE', 'PORTCHANNEL_INTERFACE', 'VLAN_INTERFACE', 'LOOPBACK_INTERFACE', 'VLAN_SUB_INTERFACE'] for table_name in tables: interface_dict = config_db.get_table(table_name) if interface_dict: @@ -488,6 +563,35 @@ def del_interface_bind_to_vrf(config_db, vrf_name): remove_router_interface_ip_address(config_db, interface_name, ipaddress) config_db.set_entry(table_name, interface_name, None) + +def del_interface_bind_to_vnet(config_db, vnet_name): + """del interface bind to vnet + """ + tables = ['INTERFACE', 'PORTCHANNEL_INTERFACE', 'VLAN_INTERFACE', 'LOOPBACK_INTERFACE', 'VLAN_SUB_INTERFACE'] + for table_name in tables: + interface_dict = config_db.get_table(table_name) + if interface_dict: + for interface_name in interface_dict: + if ('vnet_name' in interface_dict[interface_name] and + vnet_name == interface_dict[interface_name]['vnet_name']): + interface_ipaddresses = get_interface_ipaddresses(config_db, interface_name) + for ip in interface_ipaddresses: + remove_router_interface_ip_address(config_db, interface_name, ip) + config_db.set_entry(table_name, interface_name, None) + + +def del_route_bind_to_vnet(config_db, vnet_name): + """del all routes bind to vnet + """ + tables = ['VNET_ROUTE_TUNNEL', 'VNET_ROUTE'] + for table_name in tables: + table_dict = config_db.get_table(table_name) + if table_dict: + for key in table_dict: + if key[0] == vnet_name: + config_db.set_entry(table_name, key, None) + + def set_interface_naming_mode(mode): """Modify SONIC_CLI_IFACE_MODE env variable in user .bashrc """ @@ -751,6 +855,8 @@ def storm_control_delete_entry(port_name, storm_type): def _wait_until_clear(tables, interval=0.5, timeout=30, verbose=False): + if timeout == 0: + return True start = time.time() empty = False app_db = SonicV2Connector(host='127.0.0.1') @@ -766,12 +872,14 @@ def _wait_until_clear(tables, interval=0.5, timeout=30, verbose=False): click.echo("Some entries matching {} still exist: {}".format(table, keys[0])) time.sleep(interval) empty = (non_empty_table_count == 0) + if not empty: click.echo("Operation not completed successfully, please save and reload configuration.") return empty def _clear_qos(delay=False, verbose=False): + status = True QOS_TABLE_NAMES = [ 'PORT_QOS_MAP', 'QUEUE', @@ -811,7 +919,8 @@ def _clear_qos(delay=False, verbose=False): device_metadata = config_db.get_entry('DEVICE_METADATA', 'localhost') # Traditional buffer manager do not remove buffer tables in any case, no need to wait. timeout = 120 if device_metadata and device_metadata.get('buffer_model') == 'dynamic' else 0 - _wait_until_clear(["BUFFER_*_TABLE:*", "BUFFER_*_SET"], interval=0.5, timeout=timeout, verbose=verbose) + status = _wait_until_clear(["BUFFER_*_TABLE:*", "BUFFER_*_SET"], interval=0.5, timeout=timeout, verbose=verbose) + return status def _get_sonic_generated_services(num_asic): if not os.path.isfile(SONIC_GENERATED_SERVICE_PATH): @@ -862,8 +971,9 @@ def _get_disabled_services_list(config_db): def _stop_services(): try: subprocess.check_call(['sudo', 'monit', 'status'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - click.echo("Disabling container monitoring ...") + click.echo("Disabling container and routeCheck monitoring ...") clicommon.run_command(['sudo', 'monit', 'unmonitor', 'container_checker']) + clicommon.run_command(['sudo', 'monit', 'unmonitor', 'routeCheck']) except subprocess.CalledProcessError as err: pass @@ -881,17 +991,54 @@ def _reset_failed_services(): for service in _get_sonic_services(): clicommon.run_command(['systemctl', 'reset-failed', str(service)]) + +def get_service_finish_timestamp(service): + out, _ = clicommon.run_command(['sudo', + 'systemctl', + 'show', + '--no-pager', + service, + '-p', + 'ExecMainExitTimestamp', + '--value'], + return_cmd=True) + return out.strip(' \t\n\r') + + +def wait_service_restart_finish(service, last_timestamp, timeout=30): + start_time = time.time() + elapsed_time = 0 + while elapsed_time < timeout: + current_timestamp = get_service_finish_timestamp(service) + if current_timestamp and (current_timestamp != last_timestamp): + return + + time.sleep(1) + elapsed_time = time.time() - start_time + + log.log_warning("Service: {} does not restart in {} seconds, stop waiting".format(service, timeout)) + + def _restart_services(): + last_interface_config_timestamp = get_service_finish_timestamp('interfaces-config') + last_networking_timestamp = get_service_finish_timestamp('networking') + click.echo("Restarting SONiC target ...") clicommon.run_command(['sudo', 'systemctl', 'restart', 'sonic.target']) + # These service will restart eth0 and cause device lost network for 10 seconds + # When enable TACACS, every remote user commands will authorize by TACACS service via network + # If load_minigraph exit before eth0 restart, commands after load_minigraph may failed + wait_service_restart_finish('interfaces-config', last_interface_config_timestamp) + wait_service_restart_finish('networking', last_networking_timestamp) try: subprocess.check_call(['sudo', 'monit', 'status'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - click.echo("Enabling container monitoring ...") + click.echo("Enabling container and routeCheck monitoring ...") + clicommon.run_command(['sudo', 'monit', 'monitor', 'routeCheck']) clicommon.run_command(['sudo', 'monit', 'monitor', 'container_checker']) + time.sleep(1) except subprocess.CalledProcessError as err: pass - # Reload Monit configuration to pick up new hostname in case it changed click.echo("Reloading Monit configuration ...") clicommon.run_command(['sudo', 'monit', 'reload']) @@ -1016,8 +1163,9 @@ def validate_mirror_session_config(config_db, session_name, dst_port, src_port, return True def cli_sroute_to_config(ctx, command_str, strict_nh = True): - if len(command_str) < 2 or len(command_str) > 9: - ctx.fail("argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>!") + if len(command_str) < 2 or len(command_str) > 10: + ctx.fail("argument is not in pattern prefix [vrf ] nexthop [vrf ] \ + |>|>!") if "prefix" not in command_str: ctx.fail("argument is incomplete, prefix not found!") if "nexthop" not in command_str and strict_nh: @@ -1050,36 +1198,51 @@ def cli_sroute_to_config(ctx, command_str, strict_nh = True): ctx.fail("prefix is not in pattern!") if nexthop_str: - if 'nexthop' in nexthop_str and 'vrf' in nexthop_str: - # nexthop_str: ['nexthop', 'vrf', Vrf-name, ip] - config_entry["nexthop"] = nexthop_str[3] - if not is_vrf_exists(config_db, nexthop_str[2]): - ctx.fail("VRF %s does not exist!"%(nexthop_str[2])) + idx = 1 + if 'vrf' in nexthop_str: + # extract nexthop vrf + for vrf in nexthop_str[2].split(','): + if not is_vrf_exists(config_db, vrf): + ctx.fail("Nexthop VRF %s does not exist!" % (vrf)) config_entry["nexthop-vrf"] = nexthop_str[2] - elif 'nexthop' in nexthop_str and 'dev' in nexthop_str: - # nexthop_str: ['nexthop', 'dev', ifname] - config_entry["ifname"] = nexthop_str[2] - elif 'nexthop' in nexthop_str: - # nexthop_str: ['nexthop', ip] - config_entry["nexthop"] = nexthop_str[1] + idx = 3 + + if nexthop_str[idx] == 'dev': + # no nexthop IPs but interface name + config_entry["ifname"] = nexthop_str[idx + 1] else: - ctx.fail("nexthop is not in pattern!") + # nexthop IPs present, extract them first + config_entry["nexthop"] = nexthop_str[idx] + if len(nexthop_str) > idx + 1: + if nexthop_str[idx + 1] == 'dev' and len(nexthop_str) > idx + 2: + # extract interface name + config_entry["ifname"] = nexthop_str[idx + 2] + else: + ctx.fail("nexthop is not in pattern!") try: ipaddress.ip_network(ip_prefix) if 'nexthop' in config_entry: nh_list = config_entry['nexthop'].split(',') for nh in nh_list: - # Nexthop to portchannel - if nh.startswith('PortChannel'): - config_db = ctx.obj['config_db'] - if not nh in config_db.get_keys('PORTCHANNEL'): - ctx.fail("portchannel does not exist.") - else: - ipaddress.ip_address(nh) + ipaddress.ip_address(nh) except ValueError: ctx.fail("ip address is not valid.") + if 'ifname' in config_entry: + ifname_list = config_entry['ifname'].split(',') + for ifname in ifname_list: + if ifname.startswith('PortChannel'): + if ifname not in config_db.get_keys('PORTCHANNEL'): + ctx.fail("portchannel does not exist.") + elif ifname.startswith("Vlan"): + if ifname not in config_db.get_keys('VLAN_INTERFACE'): + ctx.fail("vlan interface does not exist.") + elif ifname not in config_db.get_keys('INTERFACE') and \ + ifname not in config_db.get_keys('VLAN_SUB_INTERFACE') and \ + ifname != 'null': + ctx.fail("interface {} does not exist.".format(ifname)) + if not vrf_name == "": key = vrf_name + "|" + ip_prefix else: @@ -1142,6 +1305,215 @@ def validate_gre_type(ctx, _, value): except ValueError: raise click.UsageError("{} is not a valid GRE type".format(value)) + +def multiasic_save_to_singlefile(db, filename): + """A function to save all asic's config to single file + """ + all_current_config = {} + cfgdb_clients = db.cfgdb_clients + + for ns, config_db in cfgdb_clients.items(): + current_config = config_db.get_config() + sonic_cfggen.FormatConverter.to_serialized(current_config) + asic_name = "localhost" if ns == DEFAULT_NAMESPACE else ns + all_current_config[asic_name] = sort_dict(current_config) + click.echo("Integrate each ASIC's config into a single JSON file {}.".format(filename)) + with open(filename, 'w') as file: + json.dump(all_current_config, file, indent=4) + + +def apply_patch_wrapper(args): + return apply_patch_for_scope(*args) + + +# Function to apply patch for a single ASIC. +def apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path): + scope, changes = scope_changes + # Replace localhost to DEFAULT_NAMESPACE which is db definition of Host + if scope.lower() == HOST_NAMESPACE or scope == "": + scope = multi_asic.DEFAULT_NAMESPACE + + scope_for_log = scope if scope else HOST_NAMESPACE + thread_id = threading.get_ident() + log.log_notice(f"apply_patch_for_scope started for {scope_for_log} by {changes} in thread:{thread_id}") + + try: + # Call apply_patch with the ASIC-specific changes and predefined parameters + GenericUpdater(scope=scope).apply_patch(jsonpatch.JsonPatch(changes), + config_format, + verbose, + dry_run, + ignore_non_yang_tables, + ignore_path) + results[scope_for_log] = {"success": True, "message": "Success"} + log.log_notice(f"'apply-patch' executed successfully for {scope_for_log} by {changes} in thread:{thread_id}") + except Exception as e: + results[scope_for_log] = {"success": False, "message": str(e)} + log.log_error(f"'apply-patch' executed failed for {scope_for_log} by {changes} due to {str(e)}") + + +def validate_patch(patch): + try: + command = ["show", "runningconfiguration", "all"] + proc = subprocess.Popen(command, text=True, stdout=subprocess.PIPE) + all_running_config, returncode = proc.communicate() + if returncode: + log.log_notice(f"Fetch all runningconfiguration failed as output:{all_running_config}") + return False + + # Structure validation and simulate apply patch. + all_target_config = patch.apply(json.loads(all_running_config)) + + # Verify target config by YANG models + target_config = all_target_config.pop(HOST_NAMESPACE) if multi_asic.is_multi_asic() else all_target_config + target_config.pop("bgpraw", None) + if not SonicYangCfgDbGenerator().validate_config_db_json(target_config): + return False + + if multi_asic.is_multi_asic(): + for asic in multi_asic.get_namespace_list(): + target_config = all_target_config.pop(asic) + target_config.pop("bgpraw", None) + if not SonicYangCfgDbGenerator().validate_config_db_json(target_config): + return False + + return True + except Exception as e: + raise GenericConfigUpdaterError(f"Validate json patch: {patch} failed due to:{e}") + + +def multiasic_validate_single_file(filename): + ns_list = [DEFAULT_NAMESPACE, *multi_asic.get_namespace_list()] + file_input = read_json_file(filename) + file_ns_list = [DEFAULT_NAMESPACE if key == HOST_NAMESPACE else key for key in file_input] + if set(ns_list) != set(file_ns_list): + click.echo( + "Input file {} must contain all asics config. ns_list: {} file ns_list: {}".format( + filename, ns_list, file_ns_list) + ) + raise click.Abort() + + +def load_sysinfo_if_missing(asic_config): + device_metadata = asic_config.get('DEVICE_METADATA', {}) + platform = device_metadata.get("localhost", {}).get("platform") + mac = device_metadata.get("localhost", {}).get("mac") + if not platform: + log.log_warning("platform is missing from Input file") + return True + elif not mac: + log.log_warning("mac is missing from Input file") + return True + else: + return False + + +def flush_configdb(namespace=DEFAULT_NAMESPACE): + if namespace is DEFAULT_NAMESPACE: + config_db = ConfigDBConnector() + else: + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + + config_db.connect() + client = config_db.get_redis_client(config_db.CONFIG_DB) + client.flushdb() + return client, config_db + + +def delete_transceiver_tables(): + tables = ["TRANSCEIVER_INFO", "TRANSCEIVER_STATUS", "TRANSCEIVER_PM", + "TRANSCEIVER_FIRMWARE_INFO", "TRANSCEIVER_DOM_SENSOR", "TRANSCEIVER_DOM_THRESHOLD"] + state_db_del_pattern = "|*" + + # delete TRANSCEIVER tables from State DB + state_db = SonicV2Connector(use_unix_socket_path=True) + state_db.connect(state_db.STATE_DB, False) + for table in tables: + state_db.delete_all_by_pattern(state_db.STATE_DB, table + state_db_del_pattern) + + +def migrate_db_to_lastest(namespace=DEFAULT_NAMESPACE): + # Migrate DB contents to latest version + db_migrator = '/usr/local/bin/db_migrator.py' + if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK): + if namespace is DEFAULT_NAMESPACE: + command = [db_migrator, '-o', 'migrate'] + else: + command = [db_migrator, '-o', 'migrate', '-n', namespace] + clicommon.run_command(command, display_cmd=True) + + +def multiasic_write_to_db(filename, load_sysinfo): + file_input = read_json_file(filename) + for ns in [DEFAULT_NAMESPACE, *multi_asic.get_namespace_list()]: + asic_name = HOST_NAMESPACE if ns == DEFAULT_NAMESPACE else ns + asic_config = file_input[asic_name] + + asic_load_sysinfo = True if load_sysinfo else False + if not asic_load_sysinfo: + asic_load_sysinfo = load_sysinfo_if_missing(asic_config) + + if asic_load_sysinfo: + cfg_hwsku = asic_config.get("DEVICE_METADATA", {}).\ + get("localhost", {}).get("hwsku") + if not cfg_hwsku: + click.secho("Could not get the HWSKU from config file, Exiting!", fg='magenta') + sys.exit(1) + + client, _ = flush_configdb(ns) + + if asic_load_sysinfo: + if ns is DEFAULT_NAMESPACE: + command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '--write-to-db'] + else: + command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '-n', str(ns), '--write-to-db'] + clicommon.run_command(command, display_cmd=True) + + if ns is DEFAULT_NAMESPACE: + config_db = ConfigDBPipeConnector(use_unix_socket_path=True) + else: + config_db = ConfigDBPipeConnector(use_unix_socket_path=True, namespace=ns) + + config_db.connect(False) + sonic_cfggen.FormatConverter.to_deserialized(asic_config) + data = sonic_cfggen.FormatConverter.output_to_db(asic_config) + config_db.mod_config(sonic_cfggen.FormatConverter.output_to_db(data)) + client.set(config_db.INIT_INDICATOR, 1) + + migrate_db_to_lastest(ns) + + +def config_file_yang_validation(filename): + config = read_json_file(filename) + + # Check if the config is not a dictionary + if not isinstance(config, dict): + return False + + sy = sonic_yang.SonicYang(YANG_DIR) + sy.loadYangModel() + asic_list = [HOST_NAMESPACE] + if multi_asic.is_multi_asic(): + asic_list.extend(multi_asic.get_namespace_list()) + for scope in asic_list: + config_to_check = config.get(scope) if multi_asic.is_multi_asic() else config + if config_to_check: + try: + sy.loadData(configdbJson=config_to_check) + sy.validate_data_tree() + except sonic_yang.SonicYangException as e: + click.secho("{} fails YANG validation! Error: {}".format(filename, str(e)), + fg='magenta') + raise click.Abort() + + sy.tablesWithOutYang.pop('bgpraw', None) + if len(sy.tablesWithOutYang): + click.secho("Config tables are missing yang models: {}".format(str(sy.tablesWithOutYang.keys())), + fg='magenta') + raise click.Abort() + return True + + # This is our main entrypoint - the main 'config' command @click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS) @click.pass_context @@ -1191,7 +1563,10 @@ def config(ctx): config.add_command(vlan.vlan) config.add_command(vxlan.vxlan) -#add mclag commands +# add stp commands +config.add_command(stp.spanning_tree) + +# add mclag commands config.add_command(mclag.mclag) config.add_command(mclag.mclag_member) config.add_command(mclag.mclag_unique_ip) @@ -1202,11 +1577,15 @@ def config(ctx): # DNS module config.add_command(dns.dns) +# Switchport module +config.add_command(switchport.switchport) + @config.command() @click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, expose_value=False, prompt='Existing files will be overwritten, continue?') @click.argument('filename', required=False) -def save(filename): +@clicommon.pass_db +def save(db, filename): """Export current config DB to a file on disk.\n : Names of configuration file(s) to save, separated by comma with no spaces in between """ @@ -1221,7 +1600,13 @@ def save(filename): if filename is not None: cfg_files = filename.split(',') - if len(cfg_files) != num_cfg_file: + # If only one filename is provided in multi-ASIC mode, + # save all ASIC configurations to that single file. + if len(cfg_files) == 1 and multi_asic.is_multi_asic(): + filename = cfg_files[0] + multiasic_save_to_singlefile(db, filename) + return + elif len(cfg_files) != num_cfg_file: click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file)) return @@ -1327,11 +1712,12 @@ def print_dry_run_message(dry_run): help='format of config of the patch is either ConfigDb(ABNF) or SonicYang', show_default=True) @click.option('-d', '--dry-run', is_flag=True, default=False, help='test out the command without affecting config state') +@click.option('-p', '--parallel', is_flag=True, default=False, help='applying the change to all ASICs parallelly') @click.option('-n', '--ignore-non-yang-tables', is_flag=True, default=False, help='ignore validation for tables without YANG models', hidden=True) @click.option('-i', '--ignore-path', multiple=True, help='ignore validation for config specified by given path which is a JsonPointer', hidden=True) @click.option('-v', '--verbose', is_flag=True, default=False, help='print additional details of what the operation is doing') @click.pass_context -def apply_patch(ctx, patch_file_path, format, dry_run, ignore_non_yang_tables, ignore_path, verbose): +def apply_patch(ctx, patch_file_path, format, dry_run, parallel, ignore_non_yang_tables, ignore_path, verbose): """Apply given patch of updates to Config. A patch is a JsonPatch which follows rfc6902. This command can be used do partial updates to the config with minimum disruption to running processes. It allows addition as well as deletion of configs. The patch file represents a diff of ConfigDb(ABNF) @@ -1346,12 +1732,69 @@ def apply_patch(ctx, patch_file_path, format, dry_run, ignore_non_yang_tables, i patch_as_json = json.loads(text) patch = jsonpatch.JsonPatch(patch_as_json) + if not validate_patch(patch): + raise GenericConfigUpdaterError(f"Failed validating patch:{patch}") + + results = {} config_format = ConfigFormat[format.upper()] - GenericUpdater().apply_patch(patch, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path) + # Initialize a dictionary to hold changes categorized by scope + changes_by_scope = {} + + # Iterate over each change in the JSON Patch + for change in patch: + scope, modified_path = extract_scope(change["path"]) + + # Modify the 'path' in the change to remove the scope + change["path"] = modified_path + # Check if the scope is already in our dictionary, if not, initialize it + if scope not in changes_by_scope: + changes_by_scope[scope] = [] + + # Add the modified change to the appropriate list based on scope + changes_by_scope[scope].append(change) + + # Empty case to force validate YANG model. + if not changes_by_scope: + asic_list = [multi_asic.DEFAULT_NAMESPACE] + if multi_asic.is_multi_asic(): + asic_list.extend(multi_asic.get_namespace_list()) + for asic in asic_list: + changes_by_scope[asic] = [] + + # Apply changes for each scope + if parallel: + with concurrent.futures.ThreadPoolExecutor() as executor: + # Prepare the argument tuples + arguments = [(scope_changes, results, config_format, + verbose, dry_run, ignore_non_yang_tables, ignore_path) + for scope_changes in changes_by_scope.items()] + + # Submit all tasks and wait for them to complete + futures = [executor.submit(apply_patch_wrapper, args) for args in arguments] + + # Wait for all tasks to complete + concurrent.futures.wait(futures) + else: + for scope_changes in changes_by_scope.items(): + apply_patch_for_scope(scope_changes, + results, + config_format, + verbose, dry_run, + ignore_non_yang_tables, + ignore_path) + + # Check if any updates failed + failures = [scope for scope, result in results.items() if not result['success']] + + if failures: + failure_messages = '\n'.join([f"- {failed_scope}: {results[failed_scope]['message']}" for failed_scope in failures]) + raise GenericConfigUpdaterError(f"Failed to apply patch on the following scopes:\n{failure_messages}") + + log.log_notice(f"Patch applied successfully for {patch}.") click.secho("Patch applied successfully.", fg="cyan", underline=True) except Exception as ex: - click.secho("Failed to apply patch", fg="red", underline=True, err=True) + click.secho("Failed to apply patch due to: {}".format(ex), fg="red", underline=True, err=True) ctx.fail(ex) @config.command() @@ -1445,12 +1888,14 @@ def delete_checkpoint(ctx, checkpoint_name, verbose): ctx.fail(ex) @config.command('list-checkpoints') +@click.option('-t', '--time', is_flag=True, default=False, + help='Add extra last modified time information for each checkpoint') @click.option('-v', '--verbose', is_flag=True, default=False, help='print additional details of what the operation is doing') @click.pass_context -def list_checkpoints(ctx, verbose): +def list_checkpoints(ctx, time, verbose): """List the config checkpoints available.""" try: - checkpoints_list = GenericUpdater().list_checkpoints(verbose) + checkpoints_list = GenericUpdater().list_checkpoints(time, verbose) formatted_output = json.dumps(checkpoints_list, indent=4) click.echo(formatted_output) except Exception as ex: @@ -1463,9 +1908,11 @@ def list_checkpoints(ctx, verbose): @click.option('-n', '--no_service_restart', default=False, is_flag=True, help='Do not restart docker services') @click.option('-f', '--force', default=False, is_flag=True, help='Force config reload without system checks') @click.option('-t', '--file_format', default='config_db',type=click.Choice(['config_yang', 'config_db']),show_default=True,help='specify the file format') +@click.option('-b', '--bypass-lock', default=False, is_flag=True, help='Do reload without acquiring lock') @click.argument('filename', required=False) @clicommon.pass_db -def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_format): +@try_lock(SYSTEM_RELOAD_LOCK, timeout=0) +def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_format, bypass_lock): """Clear current configuration and import a previous saved config DB dump file. : Names of configuration file(s) to load, separated by comma with no spaces in between """ @@ -1498,114 +1945,136 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_form if multi_asic.is_multi_asic() and file_format == 'config_db': num_cfg_file += num_asic + multiasic_single_file_mode = False # If the user give the filename[s], extract the file names. if filename is not None: - cfg_files = filename.split(',') + # strip whitespaces and filter out empty strings + cfg_files = [s.strip() for s in filename.split(',') if s.strip()] - if len(cfg_files) != num_cfg_file: + if len(cfg_files) == 1 and multi_asic.is_multi_asic(): + multiasic_validate_single_file(cfg_files[0]) + multiasic_single_file_mode = True + elif len(cfg_files) != num_cfg_file: click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file)) return + if filename is not None and filename != "/dev/stdin": + if multi_asic.is_multi_asic(): + for cfg_file in cfg_files: + if cfg_file is not None: + config_file_yang_validation(cfg_file) + else: + config_file_yang_validation(filename) + #Stop services before config push if not no_service_restart: log.log_notice("'reload' stopping services...") _stop_services() - # In Single ASIC platforms we have single DB service. In multi-ASIC platforms we have a global DB - # service running in the host + DB services running in each ASIC namespace created per ASIC. - # In the below logic, we get all namespaces in this platform and add an empty namespace '' - # denoting the current namespace which we are in ( the linux host ) - for inst in range(-1, num_cfg_file-1): - # Get the namespace name, for linux host it is None - if inst == -1: - namespace = None - else: - namespace = "{}{}".format(NAMESPACE_PREFIX, inst) - - # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json - if cfg_files: - file = cfg_files[inst+1] - else: - if file_format == 'config_db': - if namespace is None: - file = DEFAULT_CONFIG_DB_FILE - else: - file = "/etc/sonic/config_db{}.json".format(inst) + if multiasic_single_file_mode: + multiasic_write_to_db(cfg_files[0], load_sysinfo) + else: + # In Single ASIC platforms we have single DB service. In multi-ASIC platforms we have a global DB + # service running in the host + DB services running in each ASIC namespace created per ASIC. + # In the below logic, we get all namespaces in this platform and add an empty namespace '' + # denoting the current namespace which we are in ( the linux host ) + for inst in range(-1, num_cfg_file-1): + # Get the namespace name, for linux host it is DEFAULT_NAMESPACE + if inst == -1: + namespace = DEFAULT_NAMESPACE + else: + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) + + # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json + if cfg_files: + file = cfg_files[inst+1] + # Save to tmpfile in case of stdin input which can only be read once + if file == "/dev/stdin": + file_input = read_json_file(file) + (_, tmpfname) = tempfile.mkstemp(dir="/tmp", suffix="_configReloadStdin") + write_json_file(file_input, tmpfname) + file = tmpfname else: - file = DEFAULT_CONFIG_YANG_FILE + if file_format == 'config_db': + if namespace is DEFAULT_NAMESPACE: + file = DEFAULT_CONFIG_DB_FILE + else: + file = "/etc/sonic/config_db{}.json".format(inst) + else: + file = DEFAULT_CONFIG_YANG_FILE + # Check the file exists before proceeding. + if not os.path.exists(file): + click.echo("The config file {} doesn't exist".format(file)) + continue - # Check the file exists before proceeding. - if not os.path.exists(file): - click.echo("The config file {} doesn't exist".format(file)) - continue + if file_format == 'config_db': + file_input = read_json_file(file) + if not load_sysinfo: + load_sysinfo = load_sysinfo_if_missing(file_input) + + if load_sysinfo: + try: + command = [SONIC_CFGGEN_PATH, "-j", file, '-v', "DEVICE_METADATA.localhost.hwsku"] + proc = subprocess.Popen(command, text=True, stdout=subprocess.PIPE) + output, err = proc.communicate() + + except FileNotFoundError as e: + click.echo("{}".format(str(e)), err=True) + raise click.Abort() + except Exception as e: + click.echo("{}\n{}".format(type(e), str(e)), err=True) + raise click.Abort() + + if not output: + click.secho("Could not get the HWSKU from config file, Exiting!!!", fg='magenta') + sys.exit(1) - if load_sysinfo: - try: - command = [SONIC_CFGGEN_PATH, "-j", file, '-v', "DEVICE_METADATA.localhost.hwsku"] - proc = subprocess.Popen(command, text=True, stdout=subprocess.PIPE) - output, err = proc.communicate() + cfg_hwsku = output.strip() - except FileNotFoundError as e: - click.echo("{}".format(str(e)), err=True) - raise click.Abort() - except Exception as e: - click.echo("{}\n{}".format(type(e), str(e)), err=True) - raise click.Abort() + client, config_db = flush_configdb(namespace) + delete_transceiver_tables() - if not output: - click.secho("Could not get the HWSKU from config file, Exiting!!!", fg='magenta') - sys.exit(1) + if load_sysinfo: + if namespace is DEFAULT_NAMESPACE: + command = [ + str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '--write-to-db'] + else: + command = [ + str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '-n', str(namespace), '--write-to-db'] + clicommon.run_command(command, display_cmd=True) - cfg_hwsku = output.strip() + # For the database service running in linux host we use the file user gives as input + # or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace, + # the default config_db.json format is used. - if namespace is None: - config_db = ConfigDBConnector() - else: - config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_gen_opts = [] - config_db.connect() - client = config_db.get_redis_client(config_db.CONFIG_DB) - client.flushdb() + if os.path.isfile(INIT_CFG_FILE): + config_gen_opts += ['-j', str(INIT_CFG_FILE)] - if load_sysinfo: - if namespace is None: - command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '--write-to-db'] + if file_format == 'config_db': + config_gen_opts += ['-j', str(file)] else: - command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '-n', str(namespace), '--write-to-db'] - clicommon.run_command(command, display_cmd=True) - - # For the database service running in linux host we use the file user gives as input - # or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace, - # the default config_db.json format is used. - - - config_gen_opts = [] + config_gen_opts += ['-Y', str(file)] - if os.path.isfile(INIT_CFG_FILE): - config_gen_opts += ['-j', str(INIT_CFG_FILE)] + if namespace is not DEFAULT_NAMESPACE: + config_gen_opts += ['-n', str(namespace)] - if file_format == 'config_db': - config_gen_opts += ['-j', str(file)] - else: - config_gen_opts += ['-Y', str(file)] - - if namespace is not None: - config_gen_opts += ['-n', str(namespace)] + command = [SONIC_CFGGEN_PATH] + config_gen_opts + ['--write-to-db'] - command = [SONIC_CFGGEN_PATH] + config_gen_opts + ['--write-to-db'] + clicommon.run_command(command, display_cmd=True) + client.set(config_db.INIT_INDICATOR, 1) - clicommon.run_command(command, display_cmd=True) - client.set(config_db.INIT_INDICATOR, 1) + if os.path.exists(file) and file.endswith("_configReloadStdin"): + # Remove tmpfile + try: + os.remove(file) + except OSError as e: + click.echo("An error occurred while removing the temporary file: {}".format(str(e)), err=True) - # Migrate DB contents to latest version - db_migrator='/usr/local/bin/db_migrator.py' - if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK): - if namespace is None: - command = [db_migrator, '-o', 'migrate'] - else: - command = [db_migrator, '-o', 'migrate', '-n', str(namespace)] - clicommon.run_command(command, display_cmd=True) + # Migrate DB contents to latest version + migrate_db_to_lastest(namespace) # Re-generate the environment variable in case config_db.json was edited update_sonic_environment() @@ -1648,17 +2117,15 @@ def load_mgmt_config(filename): clicommon.run_command(command, display_cmd=True, ignore_error=True) if len(config_data['MGMT_INTERFACE'].keys()) > 0: filepath = '/var/run/dhclient.eth0.pid' - if not os.path.isfile(filepath): - sys.exit('File {} does not exist'.format(filepath)) - - out0, rc0 = clicommon.run_command(['cat', filepath], display_cmd=True, return_cmd=True) - if rc0 != 0: - sys.exit('Exit: {}. Command: cat {} failed.'.format(rc0, filepath)) - - out1, rc1 = clicommon.run_command(['kill', str(out0).strip('\n')], return_cmd=True) - if rc1 != 0: - sys.exit('Exit: {}. Command: kill {} failed.'.format(rc1, out0)) - clicommon.run_command(['rm', '-f', filepath], display_cmd=True, return_cmd=True) + if os.path.isfile(filepath): + out0, rc0 = clicommon.run_command(['cat', filepath], display_cmd=True, return_cmd=True) + if rc0 != 0: + sys.exit('Exit: {}. Command: cat {} failed.'.format(rc0, filepath)) + + out1, rc1 = clicommon.run_command(['kill', str(out0).strip('\n')], display_cmd=True, return_cmd=True) + if rc1 != 0: + sys.exit('Exit: {}. Command: kill {} failed.'.format(rc1, out0)) + clicommon.run_command(['rm', '-f', filepath], display_cmd=True, return_cmd=True) click.echo("Please note loaded setting will be lost after system reboot. To preserve setting, run `config save`.") @config.command("load_minigraph") @@ -1668,15 +2135,42 @@ def load_mgmt_config(filename): @click.option('-t', '--traffic_shift_away', default=False, is_flag=True, help='Keep device in maintenance with TSA') @click.option('-o', '--override_config', default=False, is_flag=True, help='Enable config override. Proceed with default path.') @click.option('-p', '--golden_config_path', help='Provide golden config path to override. Use with --override_config') +@click.option('-b', '--bypass-lock', default=False, is_flag=True, help='Do load minigraph without acquiring lock') @clicommon.pass_db -def load_minigraph(db, no_service_restart, traffic_shift_away, override_config, golden_config_path): +@try_lock(SYSTEM_RELOAD_LOCK, timeout=0) +def load_minigraph(db, no_service_restart, traffic_shift_away, override_config, golden_config_path, bypass_lock): """Reconfigure based on minigraph.""" argv_str = ' '.join(['config', *sys.argv[1:]]) log.log_notice(f"'load_minigraph' executing with command: {argv_str}") - #Stop services before config push + # check if golden_config exists if override flag is set + if override_config: + if golden_config_path is None: + golden_config_path = DEFAULT_GOLDEN_CONFIG_DB_FILE + if not os.path.isfile(golden_config_path): + click.secho("Cannot find '{}'!".format(golden_config_path), + fg='magenta') + raise click.Abort() + + if not config_file_yang_validation(golden_config_path): + click.secho("Invalid golden config file:'{}'!".format(golden_config_path), + fg='magenta') + raise click.Abort() + + config_to_check = read_json_file(golden_config_path) + # Dependency check golden config json + asic_list = [HOST_NAMESPACE] + if multi_asic.is_multi_asic(): + asic_list.extend(multi_asic.get_namespace_list()) + for scope in asic_list: + host_config = config_to_check.get(scope) if multi_asic.is_multi_asic() else config_to_check + if host_config: + table_hard_dependency_check(host_config) + + # Stop services before config push if not no_service_restart: log.log_notice("'load_minigraph' stopping services...") + delete_transceiver_tables() _stop_services() # For Single Asic platform the namespace list has the empty string @@ -1710,7 +2204,7 @@ def load_minigraph(db, no_service_restart, traffic_shift_away, override_config, update_sonic_environment() if os.path.isfile('/etc/sonic/acl.json'): - clicommon.run_command(['acl-loader', 'update', 'full', '/etc/sonic/acl.json'], display_cmd=True) + clicommon.run_command(['acl-loader', 'update', 'full', '/etc/sonic/acl.json', '--skip_action_validation'], display_cmd=True) # Load port_config.json try: @@ -1724,7 +2218,16 @@ def load_minigraph(db, no_service_restart, traffic_shift_away, override_config, # get the device type device_type = _get_device_type() if device_type != 'MgmtToRRouter' and device_type != 'MgmtTsToR' and device_type != 'BmcMgmtToRRouter' and device_type != 'EPMS': - clicommon.run_command(['pfcwd', 'start_default'], display_cmd=True) + # default behavior to call pfcwd start_default for all platforms + default_pfcwd_status = 'enable' + if override_config and config_to_check: + # If pfcwd is disabled in the golden config, skip starting pfcwd + # This is needed for platform which doesn't support lossless traffic. + override_metadata = config_to_check.get('DEVICE_METADATA', {}).get('localhost', {}) + if 'default_pfcwd_status' in override_metadata: + default_pfcwd_status = override_metadata['default_pfcwd_status'].lower() + if default_pfcwd_status == 'enable': + clicommon.run_command(['pfcwd', 'start_default'], display_cmd=True) # Write latest db version string into db db_migrator = '/usr/local/bin/db_migrator.py' @@ -1745,12 +2248,6 @@ def load_minigraph(db, no_service_restart, traffic_shift_away, override_config, # Load golden_config_db.json if override_config: - if golden_config_path is None: - golden_config_path = DEFAULT_GOLDEN_CONFIG_DB_FILE - if not os.path.isfile(golden_config_path): - click.secho("Cannot find '{}'!".format(golden_config_path), - fg='magenta') - raise click.Abort() override_config_by(golden_config_path) # Invoke platform script if available before starting the services @@ -1831,12 +2328,16 @@ def generate_sysinfo(cur_config, config_input, ns=None): mac = None platform = None + asic_id = None cur_device_metadata = cur_config.get('DEVICE_METADATA') - # Reuse current config's mac and platform. Generate if absent + # Reuse the existing configuration's MAC address, platform, and asic_id + # if available; generate these values only if they are missing. if cur_device_metadata is not None: mac = cur_device_metadata.get('localhost', {}).get('mac') platform = cur_device_metadata.get('localhost', {}).get('platform') + if ns != DEFAULT_NAMESPACE and ns != HOST_NAMESPACE: + asic_id = cur_device_metadata.get('localhost', {}).get('asic_id') if not mac: if ns: @@ -1854,8 +2355,14 @@ def generate_sysinfo(cur_config, config_input, ns=None): if not platform: platform = device_info.get_platform() - device_metadata['localhost']['mac'] = mac - device_metadata['localhost']['platform'] = platform + if not asic_id and ns != DEFAULT_NAMESPACE and ns != HOST_NAMESPACE: + asic_name = multi_asic.get_asic_id_from_name(ns) + asic_id = multi_asic.get_asic_device_id(asic_name) + + device_metadata['localhost']['mac'] = mac.rstrip('\n') + device_metadata['localhost']['platform'] = platform.rstrip('\n') + if ns != DEFAULT_NAMESPACE and ns != HOST_NAMESPACE and asic_id: + device_metadata['localhost']['asic_id'] = asic_id.rstrip('\n') return @@ -1898,8 +2405,8 @@ def override_config_table(db, input_config_db, dry_run): if multi_asic.is_multi_asic() and len(config_input): # Golden Config will use "localhost" to represent host name if ns == DEFAULT_NAMESPACE: - if "localhost" in config_input.keys(): - ns_config_input = config_input["localhost"] + if HOST_NAMESPACE in config_input.keys(): + ns_config_input = config_input[HOST_NAMESPACE] else: click.secho("Wrong config format! 'localhost' not found in host config! cannot override.. abort") sys.exit(1) @@ -1915,8 +2422,12 @@ def override_config_table(db, input_config_db, dry_run): ns_config_input = config_input # Generate sysinfo if missing in ns_config_input generate_sysinfo(current_config, ns_config_input, ns) + # Use deepcopy by default to avoid modifying input config updated_config = update_config(current_config, ns_config_input) + # Enable YANG hard dependecy check to exit early if not satisfied + table_hard_dependency_check(updated_config) + yang_enabled = device_info.is_yang_config_validation_enabled(config_db) if yang_enabled: # The ConfigMgmt will load YANG and running @@ -1932,6 +2443,18 @@ def override_config_table(db, input_config_db, dry_run): validate_config_by_cm(cm, ns_config_input, "config_input") # Validate updated whole config validate_config_by_cm(cm, updated_config, "updated_config") + else: + cm = None + try: + # YANG validate of config minigraph generated + cm = ConfigMgmt(configdb=config_db) + cm.validateConfigData() + except Exception as ex: + log.log_warning("Failed to validate running config. Alerting: {}".format(ex)) + + # YANG validate config of minigraph generated overriden by golden config + if cm: + validate_config_by_cm_alerting(cm, updated_config, "updated_config") if dry_run: print(json.dumps(updated_config, sort_keys=True, @@ -1950,12 +2473,13 @@ def validate_config_by_cm(cm, config_json, jname): sys.exit(1) -def update_config(current_config, config_input): - updated_config = copy.deepcopy(current_config) - # Override current config with golden config - for table in config_input: - updated_config[table] = config_input[table] - return updated_config +def validate_config_by_cm_alerting(cm, config_json, jname): + tmp_config_json = copy.deepcopy(config_json) + try: + cm.loadData(tmp_config_json) + cm.validateConfigData() + except Exception as ex: + log.log_warning("Failed to validate {}. Alerting: {}".format(jname, ex)) def override_config_db(config_db, config_input): @@ -1971,6 +2495,22 @@ def override_config_db(config_db, config_input): click.echo("Overriding completed. No service is restarted.") +def table_hard_dependency_check(config_json): + aaa_table_hard_dependency_check(config_json) + + +def aaa_table_hard_dependency_check(config_json): + AAA_TABLE = config_json.get("AAA", {}) + TACPLUS_TABLE = config_json.get("TACPLUS", {}) + + aaa_authentication_login = AAA_TABLE.get("authentication", {}).get("login", "") + tacacs_enable = "tacacs+" in aaa_authentication_login.split(",") + tacplus_passkey = TACPLUS_TABLE.get("global", {}).get("passkey", "") + if tacacs_enable and len(tacplus_passkey) == 0: + click.secho("Authentication with 'tacacs+' is not allowed when passkey not exists.", fg="magenta") + sys.exit(1) + + # # 'hostname' command # @@ -2007,7 +2547,7 @@ def synchronous_mode(sync_mode): if ADHOC_VALIDATION: if sync_mode != 'enable' and sync_mode != 'disable': raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % sync_mode) - + config_db = ValidatedConfigDBConnector(ConfigDBConnector()) config_db.connect() try: @@ -2015,12 +2555,13 @@ def synchronous_mode(sync_mode): except ValueError as e: ctx = click.get_current_context() ctx.fail("Error: Invalid argument %s, expect either enable or disable" % sync_mode) - + click.echo("""Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n Option 1. config save -y \n config reload -y \n Option 2. systemctl restart swss""" % sync_mode) + # # 'suppress-fib-pending' command ('config suppress-fib-pending ...') # @@ -2028,10 +2569,11 @@ def synchronous_mode(sync_mode): @click.argument('state', metavar='', required=True, type=click.Choice(['enabled', 'disabled'])) @clicommon.pass_db def suppress_pending_fib(db, state): - ''' Enable or disable pending FIB suppression. Once enabled, BGP will not advertise routes that are not yet installed in the hardware ''' + ''' Enable or disable pending FIB suppression. Once enabled, + BGP will not advertise routes that are not yet installed in the hardware ''' config_db = db.cfgdb - config_db.mod_entry('DEVICE_METADATA' , 'localhost', {"suppress-fib-pending" : state}) + config_db.mod_entry('DEVICE_METADATA', 'localhost', {"suppress-fib-pending": state}) # # 'yang_config_validation' command ('config yang_config_validation ...') @@ -2081,7 +2623,7 @@ def portchannel(db, ctx, namespace): @click.pass_context def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate): """Add port channel""" - + fvs = { 'admin_status': 'up', 'mtu': '9100', @@ -2093,26 +2635,27 @@ def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate): fvs['min_links'] = str(min_links) if fallback != 'false': fvs['fallback'] = 'true' - + db = ValidatedConfigDBConnector(ctx.obj['db']) if ADHOC_VALIDATION: if is_portchannel_name_valid(portchannel_name) != True: - ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'" - .format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO)) + ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}' " + "and its length should not exceed {} characters" + .format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO, IFACE_NAME_MAX_LEN)) if is_portchannel_present_in_db(db, portchannel_name): ctx.fail("{} already exists!".format(portchannel_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL - + try: db.set_entry('PORTCHANNEL', portchannel_name, fvs) except ValueError: ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'".format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO)) - + @portchannel.command('del') @click.argument('portchannel_name', metavar='', required=True) @click.pass_context def remove_portchannel(ctx, portchannel_name): """Remove port channel""" - + db = ValidatedConfigDBConnector(ctx.obj['db']) if ADHOC_VALIDATION: if is_portchannel_name_valid(portchannel_name) != True: @@ -2130,7 +2673,7 @@ def remove_portchannel(ctx, portchannel_name): if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name)) - + try: db.set_entry('PORTCHANNEL', portchannel_name, None) except JsonPatchConflict: @@ -2148,7 +2691,7 @@ def portchannel_member(ctx): def add_portchannel_member(ctx, portchannel_name, port_name): """Add member to port channel""" db = ValidatedConfigDBConnector(ctx.obj['db']) - + if ADHOC_VALIDATION: if clicommon.is_port_mirror_dst_port(db, port_name): ctx.fail("{} is configured as mirror destination port".format(port_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL @@ -2165,7 +2708,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name): # Dont proceed if the port channel does not exist if is_portchannel_present_in_db(db, portchannel_name) is False: ctx.fail("{} is not present.".format(portchannel_name)) - + # Don't allow a port to be member of port channel if it is configured with an IP address for key,value in db.get_table('INTERFACE').items(): if type(key) == tuple: @@ -2203,7 +2746,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name): member_port_speed = member_port_entry.get(PORT_SPEED) port_speed = port_entry.get(PORT_SPEED) # TODO: MISSING CONSTRAINT IN YANG MODEL - if member_port_speed != port_speed: + if member_port_speed != port_speed: ctx.fail("Port speed of {} is different than the other members of the portchannel {}" .format(port_name, portchannel_name)) @@ -2276,7 +2819,7 @@ def del_portchannel_member(ctx, portchannel_name, port_name): # Dont proceed if the the port is not an existing member of the port channel if not is_port_member_of_this_portchannel(db, port_name, portchannel_name): ctx.fail("{} is not a member of portchannel {}".format(port_name, portchannel_name)) - + try: db.set_entry('PORTCHANNEL_MEMBER', portchannel_name + '|' + port_name, None) except JsonPatchConflict: @@ -2463,7 +3006,7 @@ def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer if not namespaces['front_ns']: config_db = ValidatedConfigDBConnector(ConfigDBConnector()) config_db.connect() - if ADHOC_VALIDATION: + if ADHOC_VALIDATION: if validate_mirror_session_config(config_db, session_name, None, src_port, direction) is False: return try: @@ -2772,6 +3315,7 @@ def _update_buffer_calculation_model(config_db, model): help="Dry run, writes config to the given file" ) def reload(ctx, no_dynamic_buffer, no_delay, dry_run, json_data, ports, verbose): + status = True """Reload QoS configuration""" if ports: log.log_info("'qos reload --ports {}' executing...".format(ports)) @@ -2780,11 +3324,11 @@ def reload(ctx, no_dynamic_buffer, no_delay, dry_run, json_data, ports, verbose) log.log_info("'qos reload' executing...") if not dry_run: - _clear_qos(delay = not no_delay, verbose=verbose) + status = _clear_qos(delay=not no_delay, verbose=verbose) _, hwsku_path = device_info.get_paths_to_platform_and_hwsku_dirs() sonic_version_file = device_info.get_sonic_version_file() - from_db = ['-d', '--write-to-db'] + from_db = ['-d'] if dry_run: from_db = ['--additional-data'] + [str(json_data)] if json_data else [] @@ -2830,11 +3374,27 @@ def reload(ctx, no_dynamic_buffer, no_delay, dry_run, json_data, ports, verbose) ) if os.path.isfile(qos_template_file): cmd_ns = [] if ns is DEFAULT_NAMESPACE else ['-n', str(ns)] - fname = "{}{}".format(dry_run, asic_id_suffix) if dry_run else "config-db" - command = [SONIC_CFGGEN_PATH] + cmd_ns + from_db + ['-t', '{},{}'.format(buffer_template_file, fname), '-t', '{},{}'.format(qos_template_file, fname), '-y', sonic_version_file] - # Apply the configurations only when both buffer and qos - # configuration files are present + buffer_fname = "/tmp/cfg_buffer{}.json".format(asic_id_suffix) + qos_fname = "/tmp/cfg_qos{}.json".format(asic_id_suffix) + + command = [SONIC_CFGGEN_PATH] + cmd_ns + from_db + [ + '-t', '{},{}'.format(buffer_template_file, buffer_fname), + '-t', '{},{}'.format(qos_template_file, qos_fname), + '-y', sonic_version_file + ] clicommon.run_command(command, display_cmd=True) + + command = [SONIC_CFGGEN_PATH] + cmd_ns + ["-j", buffer_fname, "-j", qos_fname] + if dry_run: + out, rc = clicommon.run_command(command + ["--print-data"], display_cmd=True, return_cmd=True) + if rc != 0: + # clicommon.run_command does this by default when rc != 0 and return_cmd=False + sys.exit(rc) + with open("{}{}".format(dry_run, asic_id_suffix), 'w') as f: + json.dump(json.loads(out), f, sort_keys=True, indent=4) + else: + clicommon.run_command(command + ["--write-to-db"], display_cmd=True) + else: click.secho("QoS definition template not found at {}".format( qos_template_file @@ -2847,6 +3407,9 @@ def reload(ctx, no_dynamic_buffer, no_delay, dry_run, json_data, ports, verbose) if buffer_model_updated: print("Buffer calculation model updated, restarting swss is required to take effect") + if not status: + sys.exit(1) + def _qos_update_ports(ctx, ports, dry_run, json_data): """Reload QoS configuration""" _, hwsku_path = device_info.get_paths_to_platform_and_hwsku_dirs() @@ -2909,11 +3472,23 @@ def _qos_update_ports(ctx, ports, dry_run, json_data): click.secho("QoS definition template not found at {}".format(qos_template_file), fg="yellow") ctx.abort() - # Remove multi indexed entries first + # Remove entries first + for table_name in tables_single_index: + for port in portset_to_handle: + if config_db.get_entry(table_name, port): + config_db.set_entry(table_name, port, None) + for table_name in tables_multi_index: entries = config_db.get_keys(table_name) for key in entries: - port, _ = key + # Add support for chassis/multi-dut: + # on a single-dut, key = ('Ethernet136', '6') + # while on a chassis, key = ('str2-chassis-lcx-1', 'Asic0', 'Ethernet84', '5') + for element in key: + if element.startswith('Eth'): + port = element + break + assert port is not None, "Port is not found in config DB" if not port in portset_to_handle: continue config_db.set_entry(table_name, '|'.join(key), None) @@ -3154,7 +3729,10 @@ def add_snmp_agent_address(ctx, agentip, port, vrf): """Add the SNMP agent listening IP:Port%Vrf configuration""" #Construct SNMP_AGENT_ADDRESS_CONFIG table key in the format ip|| - if not clicommon.is_ipaddress(agentip): + # Link local IP address should be provided along with zone id + # % for ex fe80::1%eth0 + agent_ip_addr = agentip.split('%')[0] + if not clicommon.is_ipaddress(agent_ip_addr): click.echo("Invalid IP address") return False config_db = ctx.obj['db'] @@ -3164,7 +3742,7 @@ def add_snmp_agent_address(ctx, agentip, port, vrf): click.echo("ManagementVRF is Enabled. Provide vrf.") return False found = 0 - ip = ipaddress.ip_address(agentip) + ip = ipaddress.ip_address(agent_ip_addr) for intf in netifaces.interfaces(): ipaddresses = netifaces.ifaddresses(intf) if ip_family[ip.version] in ipaddresses: @@ -3421,7 +3999,7 @@ def del_community(db, community): if community not in snmp_communities: click.echo("SNMP community {} is not configured".format(community)) sys.exit(1) - + config_db = ValidatedConfigDBConnector(db.cfgdb) try: config_db.set_entry('SNMP_COMMUNITY', community, None) @@ -3833,6 +4411,105 @@ def del_user(db, user): click.echo("Restart service snmp failed with error {}".format(e)) raise click.Abort() + +# +# 'bmp' group ('config bmp ...') +# +@config.group() +@clicommon.pass_db +def bmp(db): + """BMP-related configuration""" + pass + + +# +# common function to update bmp config table +# +@clicommon.pass_db +def update_bmp_table(db, table_name, value): + log.log_info(f"'bmp {value} {table_name}' executing...") + bmp_table = db.cfgdb.get_table('BMP') + if not bmp_table: + bmp_table = {'table': {table_name: value}} + else: + bmp_table['table'][table_name] = value + db.cfgdb.mod_entry('BMP', 'table', bmp_table['table']) + + +# +# 'enable' subgroup ('config bmp enable ...') +# +@bmp.group() +@clicommon.pass_db +def enable(db): + """Enable BMP table dump """ + pass + + +# +# 'bgp-neighbor-table' command ('config bmp enable bgp-neighbor-table') +# +@enable.command('bgp-neighbor-table') +@clicommon.pass_db +def enable_bgp_neighbor_table(db): + update_bmp_table('bgp_neighbor_table', 'true') + + +# +# 'bgp-rib-out-table' command ('config bmp enable bgp-rib-out-table') +# +@enable.command('bgp-rib-out-table') +@clicommon.pass_db +def enable_bgp_rib_out_table(db): + update_bmp_table('bgp_rib_out_table', 'true') + + +# +# 'bgp-rib-in-table' command ('config bmp enable bgp-rib-in-table') +# +@enable.command('bgp-rib-in-table') +@clicommon.pass_db +def enable_bgp_rib_in_table(db): + update_bmp_table('bgp_rib_in_table', 'true') + + +# +# 'disable' subgroup ('config bmp disable ...') +# +@bmp.group() +@clicommon.pass_db +def disable(db): + """Disable BMP table dump """ + pass + + +# +# 'bgp-neighbor-table' command ('config bmp disable bgp-neighbor-table') +# +@disable.command('bgp-neighbor-table') +@clicommon.pass_db +def disable_bgp_neighbor_table(db): + update_bmp_table('bgp_neighbor_table', 'false') + + +# +# 'bgp-rib-out-table' command ('config bmp disable bgp-rib-out-table') +# +@disable.command('bgp-rib-out-table') +@clicommon.pass_db +def diable_bgp_rib_out_table(db): + update_bmp_table('bgp_rib_out_table', 'false') + + +# +# 'bgp-rib-in-table' command ('config bmp disable bgp-rib-in-table') +# +@disable.command('bgp-rib-in-table') +@clicommon.pass_db +def disable_bgp_rib_in_table(db): + update_bmp_table('bgp_rib_in_table', 'false') + + # # 'bgp' group ('config bgp ...') # @@ -3842,6 +4519,11 @@ def bgp(): """BGP-related configuration tasks""" pass + + +# BGP module extensions +config.commands['bgp'].add_command(bgp_cli.DEVICE_GLOBAL) + # # 'shutdown' subgroup ('config bgp shutdown ...') # @@ -4046,6 +4728,11 @@ def startup(ctx, interface_name): if sp_name in intf_fs: config_db.mod_entry("VLAN_SUB_INTERFACE", sp_name, {"admin_status": "up"}) + lo_list = config_db.get_table("LOOPBACK_INTERFACE") + for lo in lo_list: + if lo in intf_fs: + config_db.mod_entry("LOOPBACK_INTERFACE", lo, {"admin_status": "up"}) + # # 'shutdown' subcommand # @@ -4086,6 +4773,11 @@ def shutdown(ctx, interface_name): if sp_name in intf_fs: config_db.mod_entry("VLAN_SUB_INTERFACE", sp_name, {"admin_status": "down"}) + lo_list = config_db.get_table("LOOPBACK_INTERFACE") + for lo in lo_list: + if lo in intf_fs: + config_db.mod_entry("LOOPBACK_INTERFACE", lo, {"admin_status": "down"}) + # # 'speed' subcommand # @@ -4377,7 +5069,7 @@ def breakout(ctx, interface_name, mode, verbose, force_remove_dependencies, load except Exception as e: click.secho("Failed to break out Port. Error: {}".format(str(e)), fg='magenta') - sys.exit(0) + sys.exit(1) def _get_all_mgmtinterface_keys(): """Returns list of strings containing mgmt interface keys @@ -4480,16 +5172,22 @@ def ip(ctx): """Set IP interface attributes""" pass +def validate_vlan_exists(db,text): + data = db.get_table('VLAN') + keys = list(data.keys()) + return text in keys # # 'add' subcommand # -@ip.command() + +@ip.command('add') @click.argument('interface_name', metavar='', required=True) @click.argument("ip_addr", metavar="", required=True) @click.argument('gw', metavar='', required=False) +@click.option('--secondary', "-s", is_flag=True, default=False) @click.pass_context -def add(ctx, interface_name, ip_addr, gw): +def add_interface_ip(ctx, interface_name, ip_addr, gw, secondary): """Add an IP address towards the interface""" # Get the config_db connector config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) @@ -4498,20 +5196,47 @@ def add(ctx, interface_name, ip_addr, gw): interface_name = interface_alias_to_name(config_db, interface_name) if interface_name is None: ctx.fail("'interface_name' is None!") - - # Add a validation to check this interface is not a member in vlan before - # changing it to a router port + # Add a validation to check this interface is not a member in vlan before + # changing it to a router port mode vlan_member_table = config_db.get_table('VLAN_MEMBER') + if (interface_is_in_vlan(vlan_member_table, interface_name)): click.echo("Interface {} is a member of vlan\nAborting!".format(interface_name)) return + portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER') if interface_is_in_portchannel(portchannel_member_table, interface_name): ctx.fail("{} is configured as a member of portchannel." .format(interface_name)) + # Add a validation to check this interface is in routed mode before + # assigning an IP address to it + + sub_intf = False + + if clicommon.is_valid_port(config_db, interface_name): + is_port = True + elif clicommon.is_valid_portchannel(config_db, interface_name): + is_port = False + else: + sub_intf = True + + if not sub_intf: + interface_mode = None + if is_port: + interface_data = config_db.get_entry('PORT', interface_name) + else: + interface_data = config_db.get_entry('PORTCHANNEL', interface_name) + + if "mode" in interface_data: + interface_mode = interface_data["mode"] + + if interface_mode == "trunk" or interface_mode == "access": + click.echo("Interface {} is in {} mode and needs to be in routed mode!".format( + interface_name, interface_mode)) + return try: ip_address = ipaddress.ip_interface(ip_addr) except ValueError as err: @@ -4543,13 +5268,37 @@ def add(ctx, interface_name, ip_addr, gw): table_name = get_interface_table_name(interface_name) if table_name == "": ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]") + + if table_name == "VLAN_INTERFACE": + if not validate_vlan_exists(config_db, interface_name): + ctx.fail(f"Error: {interface_name} does not exist. Vlan must be created before adding an IP address") + return + interface_entry = config_db.get_entry(table_name, interface_name) if len(interface_entry) == 0: if table_name == "VLAN_SUB_INTERFACE": config_db.set_entry(table_name, interface_name, {"admin_status": "up"}) else: config_db.set_entry(table_name, interface_name, {"NULL": "NULL"}) - config_db.set_entry(table_name, (interface_name, str(ip_address)), {"NULL": "NULL"}) + + if secondary: + # We update the secondary flag only in case of VLAN Interface. + if table_name == "VLAN_INTERFACE": + vlan_interface_table = config_db.get_table(table_name) + contains_primary = False + for key, value in vlan_interface_table.items(): + if not isinstance(key, tuple): + continue + name, prefix = key + if name == interface_name and "secondary" not in value: + contains_primary = True + if contains_primary: + config_db.set_entry(table_name, (interface_name, str(ip_address)), {"secondary": "true"}) + else: + ctx.fail("Primary for the interface {} is not set, so skipping adding the interface" + .format(interface_name)) + else: + config_db.set_entry(table_name, (interface_name, str(ip_address)), {"NULL": "NULL"}) # # 'del' subcommand @@ -4599,17 +5348,16 @@ def remove(ctx, interface_name, ip_addr): if output != "": if any(interface_name in output_line for output_line in output.splitlines()): ctx.fail("Cannot remove the last IP entry of interface {}. A static {} route is still bound to the RIF.".format(interface_name, ip_ver)) - remove_router_interface_ip_address(config_db, interface_name, ip_address) - interface_addresses = get_interface_ipaddresses(config_db, interface_name) - if len(interface_addresses) == 0 and is_interface_bind_to_vrf(config_db, interface_name) is False and get_intf_ipv6_link_local_mode(ctx, interface_name, table_name) != "enable": - if table_name != "VLAN_SUB_INTERFACE": - config_db.set_entry(table_name, interface_name, None) - if multi_asic.is_multi_asic(): command = ['sudo', 'ip', 'netns', 'exec', str(ctx.obj['namespace']), 'ip', 'neigh', 'flush', 'dev', str(interface_name), str(ip_address)] else: command = ['ip', 'neigh', 'flush', 'dev', str(interface_name), str(ip_address)] clicommon.run_command(command) + remove_router_interface_ip_address(config_db, interface_name, ip_address) + interface_addresses = get_interface_ipaddresses(config_db, interface_name) + if len(interface_addresses) == 0 and is_interface_bind_to_vrf(config_db, interface_name) is False and get_intf_ipv6_link_local_mode(ctx, interface_name, table_name) != "enable": + if table_name != "VLAN_SUB_INTERFACE": + config_db.set_entry(table_name, interface_name, None) # # 'loopback-action' subcommand @@ -4638,6 +5386,105 @@ def loopback_action(ctx, interface_name, action): table_name = get_interface_table_name(interface_name) config_db.mod_entry(table_name, interface_name, {"loopback_action": action}) +# +# 'dhcp-mitigation-rate' subgroup ('config interface dhcp-mitigation-rate ...') +# + + +@interface.group(cls=clicommon.AbbreviationGroup, name='dhcp-mitigation-rate') +@click.pass_context +def dhcp_mitigation_rate(ctx): + """Set interface DHCP rate limit attribute""" + pass + +# +# 'add' subcommand +# + + +@dhcp_mitigation_rate.command(name='add') +@click.argument('interface_name', metavar='', required=True) +@click.argument('packet_rate', metavar='', required=True, type=int) +@click.pass_context +@clicommon.pass_db +def add_dhcp_mitigation_rate(db, ctx, interface_name, packet_rate): + """Add a new DHCP mitigation rate on an interface""" + # Get the config_db connector + config_db = ValidatedConfigDBConnector(db.cfgdb) + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + + if clicommon.is_valid_port(config_db, interface_name): + pass + elif clicommon.is_valid_portchannel(config_db, interface_name): + ctx.fail("{} is a PortChannel!".format(interface_name)) + else: + ctx.fail("{} does not exist".format(interface_name)) + + if packet_rate <= 0: + ctx.fail("DHCP rate limit is not valid. \nIt must be greater than 0.") + + port_data = config_db.get_entry('PORT', interface_name) + + if 'dhcp_rate_limit' in port_data: + rate = port_data["dhcp_rate_limit"] + else: + rate = '0' + + if rate != '0': + ctx.fail("{} has DHCP rate limit configured. \nRemove it to add new DHCP rate limit.".format(interface_name)) + + try: + config_db.mod_entry('PORT', interface_name, {"dhcp_rate_limit": "{}".format(str(packet_rate))}) + except ValueError as e: + ctx.fail("{} invalid or does not exist. Error: {}".format(interface_name, e)) + +# +# 'del' subcommand +# + + +@dhcp_mitigation_rate.command(name='del') +@click.argument('interface_name', metavar='', required=True) +@click.argument('packet_rate', metavar='', required=True, type=int) +@click.pass_context +@clicommon.pass_db +def del_dhcp_mitigation_rate(db, ctx, interface_name, packet_rate): + """Delete an existing DHCP mitigation rate on an interface""" + # Get the config_db connector + config_db = ValidatedConfigDBConnector(db.cfgdb) + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + + if clicommon.is_valid_port(config_db, interface_name): + pass + elif clicommon.is_valid_portchannel(config_db, interface_name): + ctx.fail("{} is a PortChannel!".format(interface_name)) + else: + ctx.fail("{} does not exist".format(interface_name)) + + if packet_rate <= 0: + ctx.fail("DHCP rate limit is not valid. \nIt must be greater than 0.") + + port_data = config_db.get_entry('PORT', interface_name) + + if 'dhcp_rate_limit' in port_data: + rate = port_data["dhcp_rate_limit"] + else: + rate = '0' + + if rate != str(packet_rate): + ctx.fail("{} DHCP rate limit does not exist on {}.".format(packet_rate, interface_name)) + + port_data["dhcp_rate_limit"] = "0" + + try: + config_db.mod_entry('PORT', interface_name, {"dhcp_rate_limit": "0"}) + except ValueError as e: + ctx.fail("{} invalid or does not exist. Error: {}".format(interface_name, e)) + # # buffer commands and utilities # @@ -4964,7 +5811,7 @@ def cable_length(ctx, interface_name, length): if not is_dynamic_buffer_enabled(config_db): ctx.fail("This command can only be supported on a system with dynamic buffer enabled") - + if ADHOC_VALIDATION: # Check whether port is legal ports = config_db.get_entry("PORT", interface_name) @@ -5103,6 +5950,43 @@ def reset(ctx, interface_name): cmd = ['sudo', 'sfputil', 'reset', str(interface_name)] clicommon.run_command(cmd) +# +# 'dom' subcommand ('config interface transceiver dom ...') +# This command is supported only for +# 1. non-breakout ports (subport = 0 or subport field is absent in CONFIG_DB) +# 2. first subport of breakout ports (subport = 1) + +@transceiver.command() +@click.argument('interface_name', metavar='', required=True) +@click.argument('desired_config', metavar='(enable|disable)', type=click.Choice(['enable', 'disable'])) +@click.pass_context +def dom(ctx, interface_name, desired_config): + """Enable/disable DOM monitoring for SFP transceiver module""" + log.log_info("interface transceiver dom {} {} executing...".format(interface_name, desired_config)) + # Get the config_db connector + config_db = ctx.obj['config_db'] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + if interface_name_is_valid(config_db, interface_name) is False: + ctx.fail("Interface name is invalid. Please enter a valid interface name!!") + + port_table_entry = config_db.get_entry("PORT", interface_name) + if not port_table_entry: + ctx.fail("Interface {} does not exist".format(interface_name)) + + # We are handling port configuration only for the below mentioned scenarios + # Port is a non-breakout port (subport = 0 or subport field is absent in CONFIG_DB) + # Port is first subport of breakout ports (subport = 1) + # If the port is not in the above mentioned scenarios, then fail the command + if port_table_entry.get("subport", '0') not in DOM_CONFIG_SUPPORTED_SUBPORTS: + ctx.fail("DOM monitoring config only supported for subports {}".format(DOM_CONFIG_SUPPORTED_SUBPORTS)) + else: + config_db.mod_entry("PORT", interface_name, {"dom_polling": "disabled" if desired_config == "disable" else "enabled"}) + # # 'mpls' subgroup ('config interface mpls ...') # @@ -5185,23 +6069,40 @@ def bind(ctx, interface_name, vrf_name): if interface_name is None: ctx.fail("'interface_name' is None!") - if not is_vrf_exists(config_db, vrf_name): - ctx.fail("VRF %s does not exist!"%(vrf_name)) + isVnet = False + if (vrf_name.startswith('Vnet_')): + isVnet = True + + if (isVnet): + if not is_vnet_exists(config_db, vrf_name): + ctx.fail("VNET %s does not exist!" % (vrf_name)) + else: + if not is_vrf_exists(config_db, vrf_name): + ctx.fail("VRF %s does not exist!" % (vrf_name)) table_name = get_interface_table_name(interface_name) if table_name == "": ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]") - if is_interface_bind_to_vrf(config_db, interface_name) is True and \ - config_db.get_entry(table_name, interface_name).get('vrf_name') == vrf_name: - return + if is_interface_bind_to_vrf(config_db, interface_name) is True: + if (isVnet): + if (config_db.get_entry(table_name, interface_name).get('vnet_name') == vrf_name): + return + else: + if (config_db.get_entry(table_name, interface_name).get('vrf_name') == vrf_name): + return + # Clean ip addresses if interface configured interface_addresses = get_interface_ipaddresses(config_db, interface_name) - for ipaddress in interface_addresses: - remove_router_interface_ip_address(config_db, interface_name, ipaddress) + for ip_address in interface_addresses: + remove_router_interface_ip_address(config_db, interface_name, ip_address) if table_name == "VLAN_SUB_INTERFACE": subintf_entry = config_db.get_entry(table_name, interface_name) - if 'vrf_name' in subintf_entry: - subintf_entry.pop('vrf_name') + if (isVnet): + if 'vnet_name' in subintf_entry: + subintf_entry.pop('vnet_name') + else: + if 'vrf_name' in subintf_entry: + subintf_entry.pop('vrf_name') config_db.set_entry(table_name, interface_name, None) # When config_db del entry and then add entry with same key, the DEL will lost. @@ -5215,16 +6116,24 @@ def bind(ctx, interface_name, vrf_name): time.sleep(0.01) state_db.close(state_db.STATE_DB) if table_name == "VLAN_SUB_INTERFACE": - subintf_entry['vrf_name'] = vrf_name - config_db.set_entry(table_name, interface_name, subintf_entry) + if (isVnet): + subintf_entry['vnet_name'] = vrf_name + config_db.set_entry(table_name, interface_name, subintf_entry) + else: + subintf_entry['vrf_name'] = vrf_name + config_db.set_entry(table_name, interface_name, subintf_entry) else: - config_db.set_entry(table_name, interface_name, {"vrf_name": vrf_name}) + if (isVnet): + config_db.set_entry(table_name, interface_name, {"vnet_name": vrf_name}) + else: + config_db.set_entry(table_name, interface_name, {"vrf_name": vrf_name}) click.echo("Interface {} IP disabled and address(es) removed due to binding VRF {}.".format(interface_name, vrf_name)) + + # # 'unbind' subcommand # - @vrf.command() @click.argument('interface_name', metavar='', required=True) @click.pass_context @@ -5248,6 +6157,8 @@ def unbind(ctx, interface_name): subintf_entry = config_db.get_entry(table_name, interface_name) if 'vrf_name' in subintf_entry: subintf_entry.pop('vrf_name') + elif 'vnet_name' in subintf_entry: + subintf_entry.pop('vnet_name') interface_ipaddresses = get_interface_ipaddresses(config_db, interface_name) for ipaddress in interface_ipaddresses: @@ -5272,7 +6183,7 @@ def unbind(ctx, interface_name): config_db.set_entry(table_name, interface_name, subintf_entry) else: config_db.set_entry(table_name, interface_name, None) - + click.echo("Interface {} IP disabled and address(es) removed due to unbinding VRF.".format(interface_name)) # # 'ipv6' subgroup ('config interface ipv6 ...') @@ -5362,41 +6273,888 @@ def disable_use_link_local_only(ctx, interface_name): db = ctx.obj["config_db"] if clicommon.get_interface_naming_mode() == "alias": - interface_name = interface_alias_to_name(db, interface_name) + interface_name = interface_alias_to_name(db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + interface_type = "" + if interface_name.startswith("Ethernet"): + interface_type = "INTERFACE" + elif interface_name.startswith("PortChannel"): + interface_type = "PORTCHANNEL_INTERFACE" + elif interface_name.startswith("Vlan"): + interface_type = "VLAN_INTERFACE" + else: + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + + if (interface_type == "INTERFACE" ) or (interface_type == "PORTCHANNEL_INTERFACE"): + if interface_name_is_valid(db, interface_name) is False: + ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + + if (interface_type == "VLAN_INTERFACE"): + if not clicommon.is_valid_vlan_interface(db, interface_name): + ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + + portchannel_member_table = db.get_table('PORTCHANNEL_MEMBER') + + if interface_is_in_portchannel(portchannel_member_table, interface_name): + ctx.fail("{} is configured as a member of portchannel. Cannot configure the IPv6 link local mode!" + .format(interface_name)) + + vlan_member_table = db.get_table('VLAN_MEMBER') + if interface_is_in_vlan(vlan_member_table, interface_name): + ctx.fail("{} is configured as a member of vlan. Cannot configure the IPv6 link local mode!" + .format(interface_name)) + + interface_dict = db.get_table(interface_type) + set_ipv6_link_local_only_on_interface(db, interface_dict, interface_type, interface_name, "disable") + + +def is_vaild_intf_ip_addr(ip_addr) -> bool: + """Check whether the ip address is valid""" + try: + ip_address = ipaddress.ip_interface(ip_addr) + except ValueError as err: + click.echo("IP address {} is not valid: {}".format(ip_addr, err)) + return False + + if ip_address.version == 6: + if ip_address.is_unspecified: + click.echo("IPv6 address {} is unspecified".format(str(ip_address))) + return False + elif ip_address.version == 4: + if str(ip_address.ip) == "0.0.0.0": + click.echo("IPv4 address {} is Zero".format(str(ip_address))) + return False + + if ip_address.is_multicast: + click.echo("IP address {} is multicast".format(str(ip_address))) + return False + + ip = ip_address.ip + if ip.is_loopback: + click.echo("IP address {} is loopback address".format(str(ip_address))) + return False + + return True + + +# +# 'vrrp' subgroup ('config interface vrrp ...') +# +@interface.group(cls=clicommon.AbbreviationGroup) +@click.pass_context +def vrrp(ctx): + """Vrrp configuration""" + pass + + +# +# ip subgroup ('config interface vrrp ip ...') +# +@vrrp.group(cls=clicommon.AbbreviationGroup, name='ip') +@click.pass_context +def ip(ctx): + """vrrp ip configuration """ + pass + + +@ip.command('add') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("ip_addr", metavar="", required=True) +@click.pass_context +def add_vrrp_ip(ctx, interface_name, vrrp_id, ip_addr): + """Add IPv4 address to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + if not is_vaild_intf_ip_addr(ip_addr): + ctx.abort() + if check_vrrp_ip_exist(config_db, ip_addr): + ctx.abort() + + if "/" not in ip_addr: + ctx.fail("IP address {} is missing a mask. Such as xx.xx.xx.xx/yy or xx:xx::xx/yy".format(str(ip_addr))) + + # check vip exist + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + address_list = [] + if vrrp_entry: + # update vrrp + if "vip" in vrrp_entry: + address_list = vrrp_entry.get("vip") + # add ip address + if len(address_list) >= 4: + ctx.fail("The vrrp instance {} has already configured 4 IP addresses".format(vrrp_id)) + + else: + # create new vrrp + vrrp_entry = {} + vrrp_keys = config_db.get_keys("VRRP") + if len(vrrp_keys) >= 254: + ctx.fail("Has already configured 254 vrrp instances") + intf_cfg = 0 + for key in vrrp_keys: + if key[1] == str(vrrp_id): + ctx.fail("The vrrp instance {} has already configured!".format(vrrp_id)) + if key[0] == interface_name: + intf_cfg += 1 + if intf_cfg >= 16: + ctx.fail("{} has already configured 16 vrrp instances!".format(interface_name)) + vrrp_entry["vid"] = vrrp_id + + address_list.append(ip_addr) + vrrp_entry['vip'] = address_list + + config_db.set_entry("VRRP", (interface_name, str(vrrp_id)), vrrp_entry) + + +@ip.command('remove') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("ip_addr", metavar="", required=True) +@click.pass_context +def remove_vrrp_ip(ctx, interface_name, vrrp_id, ip_addr): + """Remove IPv4 address to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + try: + ipaddress.ip_interface(ip_addr) + except ValueError as err: + ctx.fail("IP address is not valid: {}".format(err)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("{} is not configured on the vrrp instance {}!".format(ip_addr, vrrp_id)) + + address_list = vrrp_entry.get("vip") + # address_list = vrrp_entry.get("vip") + if not address_list: + ctx.fail("{} is not configured on the vrrp instance {}!".format(ip_addr, vrrp_id)) + + # del ip address + if ip_addr in address_list: + address_list.remove(ip_addr) + else: + ctx.fail("{} is not configured on the vrrp instance {}!".format(ip_addr, vrrp_id)) + vrrp_entry['vip'] = address_list + config_db.set_entry("VRRP", (interface_name, str(vrrp_id)), vrrp_entry) + + +# +# track interface subgroup ('config interface vrrp track_interface ...') +# +@vrrp.group(cls=clicommon.AbbreviationGroup, name='track_interface') +@click.pass_context +def track_interface(ctx): + """ vrrp track_interface configuration """ + pass + + +@track_interface.command('add') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("track_interface", metavar="", required=True) +@click.argument('priority_increment', metavar='', required=True, type=click.IntRange(10, 50), + default=20) +@click.pass_context +def add_track_interface(ctx, interface_name, vrrp_id, track_interface, priority_increment): + """add track_interface to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + track_interface = interface_alias_to_name(config_db, track_interface) + if interface_name is None: + ctx.fail("'interface_name' is None!") + if track_interface is None: + ctx.fail("'track_interface' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + table_name_t = get_interface_table_name(track_interface) + if table_name_t == "" or table_name_t == "LOOPBACK_INTERFACE": + ctx.fail("'track_interface' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if track_interface not in config_db.get_table(table_name_t): + ctx.fail("Router Interface '{}' not found".format(track_interface)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp instance {} not found on interface {}".format(vrrp_id, interface_name)) + + track_entry = config_db.get_entry("VRRP_TRACK", (interface_name, str(vrrp_id), track_interface)) + if track_entry: + track_entry['priority_increment'] = priority_increment + else: + track_entry = {} + track_entry["priority_increment"] = priority_increment + + vrrp_track_keys = config_db.get_keys("VRRP_TRACK") + if vrrp_track_keys: + track_key = (interface_name, str(vrrp_id)) + count = 0 + for item in vrrp_track_keys: + subtuple1 = item[:2] + if subtuple1 == track_key: + count += 1 + + if count >= 8: + ctx.fail("The Vrrpv instance {} has already configured 8 track interfaces".format(vrrp_id)) + + config_db.set_entry("VRRP_TRACK", (interface_name, str(vrrp_id), track_interface), track_entry) + + +@track_interface.command('remove') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("track_interface", metavar="", required=True) +@click.pass_context +def remove_track_interface(ctx, interface_name, vrrp_id, track_interface): + """Remove track_interface to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + track_interface = interface_alias_to_name(config_db, track_interface) + if interface_name is None: + ctx.fail("'interface_name' is None!") + if track_interface is None: + ctx.fail("'track_interface' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + table_name_t = get_interface_table_name(track_interface) + if table_name_t == "" or table_name_t == "LOOPBACK_INTERFACE": + ctx.fail("'track_interface' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp instance {} not found on interface {}".format(vrrp_id, interface_name)) + + track_entry = config_db.get_entry("VRRP_TRACK", (interface_name, str(vrrp_id), track_interface)) + if not track_entry: + ctx.fail("{} is not configured on the vrrp instance {}!".format(track_interface, vrrp_id)) + config_db.set_entry('VRRP_TRACK', (interface_name, str(vrrp_id), track_interface), None) + + +# +# 'vrrp' subcommand ('config interface vrrp priority ...') +# +@vrrp.command("priority") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('priority', metavar='', required=True, type=click.IntRange(1, 254), default=100) +@click.pass_context +def priority(ctx, interface_name, vrrp_id, priority): + """config priority to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp_entry['priority'] = priority + config_db.set_entry("VRRP", (interface_name, str(vrrp_id)), vrrp_entry) + + +# +# 'vrrp' subcommand ('config interface vrrp adv_interval ...') +# +@vrrp.command("adv_interval") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('interval', metavar='', required=True, type=click.IntRange(1, 255), default=1) +@click.pass_context +def adv_interval(ctx, interface_name, vrrp_id, interval): + """config adv_interval to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp_entry['adv_interval'] = interval + config_db.set_entry("VRRP", (interface_name, str(vrrp_id)), vrrp_entry) + + +# +# 'vrrp' subcommand ('config interface vrrp pre_empt ...') +# +@vrrp.command("pre_empt") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('mode', metavar='', required=True, type=click.Choice(["enabled", "disabled"])) +@click.pass_context +def pre_empt(ctx, interface_name, vrrp_id, mode): + """Config pre_empt mode to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp_entry['preempt'] = mode + config_db.set_entry("VRRP", (interface_name, str(vrrp_id)), vrrp_entry) + + +# +# 'vrrp' subcommand ('config interface vrrp version...') +# +@vrrp.command("version") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('version', metavar='', required=True, type=click.Choice(["2", "3"]), default=3) +@click.pass_context +def version(ctx, interface_name, vrrp_id, version): + """Config vrrp packet version to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp_entry['version'] = version + config_db.set_entry("VRRP", (interface_name, str(vrrp_id)), vrrp_entry) + + +# +# 'vrrp' subcommand +# +@vrrp.command("add") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.pass_context +def add_vrrp(ctx, interface_name, vrrp_id): + """Add vrrp instance to the interface""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if vrrp_entry: + ctx.fail("{} has already configured the vrrp instance {}!".format(interface_name, vrrp_id)) + else: + vrrp_keys = config_db.get_keys("VRRP") + if len(vrrp_keys) >= 254: + ctx.fail("Has already configured 254 vrrp instances!") + intf_cfg = 0 + for key in vrrp_keys: + if key[1] == str(vrrp_id): + ctx.fail("The vrrp instance {} has already configured!".format(vrrp_id)) + if key[0] == interface_name: + intf_cfg += 1 + if intf_cfg >= 16: + ctx.fail("{} has already configured 16 vrrp instances!".format(interface_name)) + + config_db.set_entry('VRRP', (interface_name, str(vrrp_id)), {"vid": vrrp_id}) + + +@vrrp.command("remove") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.pass_context +def remove_vrrp(ctx, interface_name, vrrp_id): + """Remove vrrp instance to the interface""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp_entry = config_db.get_entry("VRRP", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("{} dose not configured the vrrp instance {}!".format(interface_name, vrrp_id)) + config_db.set_entry('VRRP', (interface_name, str(vrrp_id)), None) + + +# +# 'vrrp6' subgroup ('config interface vrrp6 ...') +# +@interface.group(cls=clicommon.AbbreviationGroup) +@click.pass_context +def vrrp6(ctx): + """Vrrpv6 configuration""" + pass + + +# +# ip subgroup ('config interface vrrp6 ipv6 ...') +# +@vrrp6.group(cls=clicommon.AbbreviationGroup, name='ipv6') +@click.pass_context +def ipv6(ctx): + """Vrrpv6 ipv6 configuration """ + pass + + +@ipv6.command('add') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("ipv6_addr", metavar="", required=True) +@click.pass_context +def add_vrrp6_ipv6(ctx, interface_name, vrrp_id, ipv6_addr): + """Add IPv6 address to the Vrrpv6 instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + if not is_vaild_intf_ip_addr(ipv6_addr): + ctx.abort() + if check_vrrp_ip_exist(config_db, ipv6_addr): + ctx.abort() + + if "/" not in ipv6_addr: + ctx.fail("IPv6 address {} is missing a mask. Such as xx:xx::xx/yy".format(str(ipv6_addr))) + + # check vip exist + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + address_list = [] + if vrrp6_entry: + # update vrrp + if "vip" in vrrp6_entry: + address_list = vrrp6_entry.get("vip") + # add ip address + if len(address_list) >= 4: + ctx.fail("The vrrp instance {} has already configured 4 IPv6 addresses".format(vrrp_id)) + + else: + # create new vrrp + vrrp6_entry = {} + vrrp6_keys = config_db.get_keys("VRRP6") + if len(vrrp6_keys) >= 254: + ctx.fail("Has already configured 254 Vrrpv6 instances.") + intf_cfg = 0 + for key in vrrp6_keys: + if key[1] == str(vrrp_id): + ctx.fail("The Vrrpv6 instance {} has already configured!".format(vrrp_id)) + if key[0] == interface_name: + intf_cfg += 1 + if intf_cfg >= 16: + ctx.fail("{} has already configured 16 Vrrpv6 instances!".format(interface_name)) + vrrp6_entry["vid"] = vrrp_id + + address_list.append(ipv6_addr) + vrrp6_entry['vip'] = address_list + + config_db.set_entry("VRRP6", (interface_name, str(vrrp_id)), vrrp6_entry) + + +@ipv6.command('remove') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("ipv6_addr", metavar="", required=True) +@click.pass_context +def remove_vrrp_ipv6(ctx, interface_name, vrrp_id, ipv6_addr): + """Remove IPv6 address to the Vrrpv6 instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + try: + ipaddress.ip_interface(ipv6_addr) + except ValueError as err: + ctx.fail("IPv6 address is not valid: {}".format(err)) + + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp6_entry: + ctx.fail("{} is not configured on the Vrrpv6 instance {}!".format(ipv6_addr, vrrp_id)) + + address_list = vrrp6_entry.get("vip") + # address_list = vrrp6_entry.get("vip") + if not address_list: + ctx.fail("{} is not configured on the Vrrpv6 instance {}!".format(ipv6_addr, vrrp_id)) + + # del ip address + if ipv6_addr in address_list: + address_list.remove(ipv6_addr) + else: + ctx.fail("{} is not configured on the Vrrpv6 instance {}!".format(ipv6_addr, vrrp_id)) + vrrp6_entry['vip'] = address_list + config_db.set_entry("VRRP6", (interface_name, str(vrrp_id)), vrrp6_entry) + + +def check_vrrp_ip_exist(config_db, ip_addr) -> bool: + addr_type = ipaddress.ip_interface(ip_addr).version + vrrp_table = "VRRP" if addr_type == 4 else "VRRP6" + vrrp_keys = config_db.get_keys(vrrp_table) + for vrrp_key in vrrp_keys: + vrrp_entry = config_db.get_entry(vrrp_table, vrrp_key) + if "vip" not in vrrp_entry: + continue + if ip_addr in vrrp_entry["vip"]: + click.echo("{} has already configured on the {} vrrp instance {}!".format(ip_addr, vrrp_key[0], + vrrp_key[1])) + return True + return False + + +# +# track interface subgroup ('config interface vrrp6 track_interface ...') +# +@vrrp6.group(cls=clicommon.AbbreviationGroup, name='track_interface') +@click.pass_context +def vrrp6_track_interface(ctx): + """ Vrrpv6 track_interface configuration """ + pass + + +@vrrp6_track_interface.command('add') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("track_interface", metavar="", required=True) +@click.argument('priority_increment', metavar='', required=True, type=click.IntRange(10, 50), + default=20) +@click.pass_context +def add_track_interface_v6(ctx, interface_name, vrrp_id, track_interface, priority_increment): + """add track_interface to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + track_interface = interface_alias_to_name(config_db, track_interface) + if interface_name is None: + ctx.fail("'interface_name' is None!") + if track_interface is None: + ctx.fail("'track_interface' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + table_name_t = get_interface_table_name(track_interface) + if table_name_t == "" or table_name_t == "LOOPBACK_INTERFACE": + ctx.fail("'track_interface' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if track_interface not in config_db.get_table(table_name_t): + ctx.fail("Router Interface '{}' not found".format(track_interface)) + + vrrp_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp6 instance {} not found on interface {}".format(vrrp_id, interface_name)) + + # track_intf_key = track_interface + "|weight|" + str(weight) + vrrp6_track_keys = config_db.get_keys("VRRP6_TRACK") + if vrrp6_track_keys: + track_key = (interface_name, str(vrrp_id)) + count = 0 + for item in vrrp6_track_keys: + subtuple1 = item[:2] + if subtuple1 == track_key: + count += 1 + + if count >= 8: + ctx.fail("The Vrrpv6 instance {} has already configured 8 track interfaces".format(vrrp_id)) + + # create a new entry + track6_entry = {} + track6_entry["priority_increment"] = priority_increment + config_db.set_entry("VRRP6_TRACK", (interface_name, str(vrrp_id), track_interface), track6_entry) + + +@vrrp6_track_interface.command('remove') +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument("track_interface", metavar="", required=True) +@click.pass_context +def remove_track_interface_v6(ctx, interface_name, vrrp_id, track_interface): + """Remove track_interface to the vrrp instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + track_interface = interface_alias_to_name(config_db, track_interface) + if interface_name is None: + ctx.fail("'interface_name' is None!") + if track_interface is None: + ctx.fail("'track_interface' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + table_name_t = get_interface_table_name(track_interface) + if table_name_t == "" or table_name_t == "LOOPBACK_INTERFACE": + ctx.fail("'track_interface' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + + vrrp_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp_entry: + ctx.fail("vrrp6 instance {} not found on interface {}".format(vrrp_id, interface_name)) + + track6_entry = config_db.get_entry("VRRP6_TRACK", (interface_name, str(vrrp_id), track_interface)) + if not track6_entry: + ctx.fail("{} is not configured on the vrrp6 instance {}!".format(track_interface, vrrp_id)) + config_db.set_entry('VRRP6_TRACK', (interface_name, str(vrrp_id), track_interface), None) + + +# +# 'vrrp6' subcommand ('config interface vrrp6 priority ...') +# +@vrrp6.command("priority") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('priority', metavar='', required=True, type=click.IntRange(1, 254), default=100) +@click.pass_context +def priority_v6(ctx, interface_name, vrrp_id, priority): + """config priority to the Vrrpv6 instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp6_entry: + ctx.fail("Vrrpv6 instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp6_entry['priority'] = priority + config_db.set_entry("VRRP6", (interface_name, str(vrrp_id)), vrrp6_entry) + + +# +# 'vrrp' subcommand ('config interface vrrp6 adv_interval ...') +# +@vrrp6.command("adv_interval") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('interval', metavar='', required=True, type=click.IntRange(1, 255), default=1000) +@click.pass_context +def adv_interval_v6(ctx, interface_name, vrrp_id, interval): + """config adv_interval to the Vrrpv6 instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp6_entry: + ctx.fail("Vrrpv6 instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp6_entry['adv_interval'] = interval + config_db.set_entry("VRRP6", (interface_name, str(vrrp_id)), vrrp6_entry) + + +# +# 'vrrp' subcommand ('config interface vrrp6 pre_empt ...') +# +@vrrp6.command("pre_empt") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.argument('mode', metavar='', required=True, type=click.Choice(["enabled", "disabled"])) +@click.pass_context +def pre_empt_v6(ctx, interface_name, vrrp_id, mode): + """Config pre_empt mode to the Vrrpv6 instance""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) + + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp6_entry: + ctx.fail("Vrrpv6 instance {} not found on interface {}".format(vrrp_id, interface_name)) + + vrrp6_entry['preempt'] = mode + config_db.set_entry("VRRP6", (interface_name, str(vrrp_id)), vrrp6_entry) + + +# +# 'vrrp6' subcommand +# +@vrrp6.command("add") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.pass_context +def add_vrrp_v6(ctx, interface_name, vrrp_id): + """Add Vrrpv6 instance to the interface""" + config_db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) if interface_name is None: ctx.fail("'interface_name' is None!") - interface_type = "" - if interface_name.startswith("Ethernet"): - interface_type = "INTERFACE" - elif interface_name.startswith("PortChannel"): - interface_type = "PORTCHANNEL_INTERFACE" - elif interface_name.startswith("Vlan"): - interface_type = "VLAN_INTERFACE" - else: + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) - if (interface_type == "INTERFACE" ) or (interface_type == "PORTCHANNEL_INTERFACE"): - if interface_name_is_valid(db, interface_name) is False: - ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) - - if (interface_type == "VLAN_INTERFACE"): - if not clicommon.is_valid_vlan_interface(db, interface_name): - ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if vrrp6_entry: + ctx.fail("{} has already configured the Vrrpv6 instance {}!".format(interface_name, vrrp_id)) + else: + vrrp6_keys = config_db.get_keys("VRRP6") + if len(vrrp6_keys) >= 254: + ctx.fail("Has already configured 254 Vrrpv6 instances!") + intf_cfg = 0 + for key in vrrp6_keys: + if key[1] == str(vrrp_id): + ctx.fail("The Vrrpv6 instance {} has already configured!".format(vrrp_id)) + if key[0] == interface_name: + intf_cfg += 1 + if intf_cfg >= 16: + ctx.fail("{} has already configured 16 Vrrpv6 instances!".format(interface_name)) + + config_db.set_entry('VRRP6', (interface_name, str(vrrp_id)), {"vid": vrrp_id}) + + +@vrrp6.command("remove") +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrrp_id', metavar='', required=True, type=click.IntRange(1, 255)) +@click.pass_context +def remove_vrrp_v6(ctx, interface_name, vrrp_id): + """Remove Vrrpv6 instance to the interface""" + config_db = ctx.obj["config_db"] - portchannel_member_table = db.get_table('PORTCHANNEL_MEMBER') + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(config_db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") - if interface_is_in_portchannel(portchannel_member_table, interface_name): - ctx.fail("{} is configured as a member of portchannel. Cannot configure the IPv6 link local mode!" - .format(interface_name)) + table_name = get_interface_table_name(interface_name) + if table_name == "" or table_name == "LOOPBACK_INTERFACE": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + if interface_name not in config_db.get_table(table_name): + ctx.fail("Router Interface '{}' not found".format(interface_name)) - vlan_member_table = db.get_table('VLAN_MEMBER') - if interface_is_in_vlan(vlan_member_table, interface_name): - ctx.fail("{} is configured as a member of vlan. Cannot configure the IPv6 link local mode!" - .format(interface_name)) + vrrp6_entry = config_db.get_entry("VRRP6", (interface_name, str(vrrp_id))) + if not vrrp6_entry: + ctx.fail("{} dose not configured the Vrrpv6 instance {}!".format(interface_name, vrrp_id)) + config_db.set_entry('VRRP6', (interface_name, str(vrrp_id)), None) - interface_dict = db.get_table(interface_type) - set_ipv6_link_local_only_on_interface(db, interface_dict, interface_type, interface_name, "disable") # # 'vrf' group ('config vrf ...') @@ -5419,8 +7177,8 @@ def add_vrf(ctx, vrf_name): config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) if not vrf_name.startswith("Vrf") and not (vrf_name == 'mgmt') and not (vrf_name == 'management'): ctx.fail("'vrf_name' must begin with 'Vrf' or named 'mgmt'/'management' in case of ManagementVRF.") - if len(vrf_name) > 15: - ctx.fail("'vrf_name' is too long!") + if not isInterfaceNameValid(vrf_name): + ctx.fail("'vrf_name' length should not exceed {} characters".format(IFACE_NAME_MAX_LEN)) if is_vrf_exists(config_db, vrf_name): ctx.fail("VRF {} already exists!".format(vrf_name)) elif (vrf_name == 'mgmt' or vrf_name == 'management'): @@ -5439,8 +7197,8 @@ def del_vrf(ctx, vrf_name): config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) if not vrf_name.startswith("Vrf") and not (vrf_name == 'mgmt') and not (vrf_name == 'management'): ctx.fail("'vrf_name' must begin with 'Vrf' or named 'mgmt'/'management' in case of ManagementVRF.") - if len(vrf_name) > 15: - ctx.fail("'vrf_name' is too long!") + if not isInterfaceNameValid(vrf_name): + ctx.fail("'vrf_name' length should not exceed {} characters".format((IFACE_NAME_MAX_LEN))) syslog_table = config_db.get_table("SYSLOG_SERVER") syslog_vrf_dev = "mgmt" if vrf_name == "management" else vrf_name for syslog_entry, syslog_data in syslog_table.items(): @@ -5522,53 +7280,60 @@ def route(ctx): ctx.obj = {} ctx.obj['config_db'] = config_db + @route.command('add', context_settings={"ignore_unknown_options": True}) -@click.argument('command_str', metavar='prefix [vrf ] nexthop <[vrf ] >|>', nargs=-1, type=click.Path()) +@click.argument('command_str', metavar='prefix [vrf ] nexthop [vrf ] \ + |>|>', nargs=-1, type=click.Path()) @click.pass_context def add_route(ctx, command_str): """Add route command""" config_db = ctx.obj['config_db'] key, route = cli_sroute_to_config(ctx, command_str) - # If defined intf name, check if it belongs to interface - if 'ifname' in route: - if (not route['ifname'] in config_db.get_keys('VLAN_INTERFACE') and - not route['ifname'] in config_db.get_keys('INTERFACE') and - not route['ifname'] in config_db.get_keys('PORTCHANNEL_INTERFACE') and - not route['ifname'] in config_db.get_keys('VLAN_SUB_INTERFACE') and - not route['ifname'] == 'null'): - ctx.fail('interface {} doesn`t exist'.format(route['ifname'])) - entry_counter = 1 if 'nexthop' in route: entry_counter = len(route['nexthop'].split(',')) + if 'ifname' in route and len(route['ifname'].split(',')) > entry_counter: + entry_counter = len(route['ifname'].split(',')) # Alignment in case the command contains several nexthop ip for i in range(entry_counter): + # Set vrf to empty string if not defined if 'nexthop-vrf' in route: - if i > 0: + if i >= len(route['nexthop-vrf'].split(',')): vrf = route['nexthop-vrf'].split(',')[0] route['nexthop-vrf'] += ',' + vrf else: route['nexthop-vrf'] = '' - if not 'nexthop' in route: + # Set nexthop to empty string if not defined + if 'nexthop' in route: + if i >= len(route['nexthop'].split(',')): + route['nexthop'] += ',' + else: route['nexthop'] = '' + # Set ifname to empty string if not defined if 'ifname' in route: - if i > 0: + if i >= len(route['ifname'].split(',')): route['ifname'] += ',' else: route['ifname'] = '' # Set default values for distance and blackhole because the command doesn't have such an option if 'distance' in route: - route['distance'] += ',0' + if i >= len(route['distance'].split(',')): + route['distance'] += ',0' else: route['distance'] = '0' if 'blackhole' in route: - route['blackhole'] += ',false' + if i >= len(route['blackhole'].split(',')): + # If the user configure with "ifname" as "null", set 'blackhole' attribute as true. + if route['ifname'].split(',')[i] == 'null': + route['blackhole'] += ',true' + else: + route['blackhole'] += ',false' else: # If the user configure with "ifname" as "null", set 'blackhole' attribute as true. if 'ifname' in route and route['ifname'] == 'null': @@ -5582,28 +7347,30 @@ def add_route(ctx, command_str): # If exist update current entry current_entry = config_db.get_entry('STATIC_ROUTE', key) - for entry in ['nexthop', 'nexthop-vrf', 'ifname', 'distance', 'blackhole']: - if not entry in current_entry: - current_entry[entry] = '' - if entry in route: - current_entry[entry] += ',' + route[entry] + for item in ['nexthop', 'nexthop-vrf', 'ifname', 'distance', 'blackhole']: + if item not in current_entry: + current_entry[item] = '' + if item in route: + current_entry[item] += ',' + route[item] else: - current_entry[entry] += ',' + current_entry[item] += ',' config_db.set_entry("STATIC_ROUTE", key, current_entry) else: config_db.set_entry("STATIC_ROUTE", key, route) + @route.command('del', context_settings={"ignore_unknown_options": True}) -@click.argument('command_str', metavar='prefix [vrf ] nexthop <[vrf ] >|>', nargs=-1, type=click.Path()) +@click.argument('command_str', metavar='prefix [vrf ] nexthop [vrf ] \ + |>|>', nargs=-1, type=click.Path()) @click.pass_context def del_route(ctx, command_str): """Del route command""" config_db = ctx.obj['config_db'] key, route = cli_sroute_to_config(ctx, command_str, strict_nh=False) keys = config_db.get_keys('STATIC_ROUTE') - prefix_tuple = tuple(key.split('|')) - if not tuple(key.split("|")) in keys and not prefix_tuple in keys: + + if not tuple(key.split("|")) in keys: ctx.fail('Route {} doesnt exist'.format(key)) else: # If not defined nexthop or intf name remove entire route @@ -5638,9 +7405,11 @@ def del_route(ctx, command_str): # Create tuple from CLI argument # config route add prefix 1.4.3.4/32 nexthop vrf Vrf-RED 20.0.0.2 # ('20.0.0.2', 'Vrf-RED', '') - for entry in ['nexthop', 'nexthop-vrf', 'ifname']: - if entry in route: - cli_tuple += (route[entry],) + for item in ['nexthop', 'nexthop-vrf', 'ifname']: + if item in route: + if ',' in route[item]: + ctx.fail('Only one nexthop can be deleted at a time') + cli_tuple += (route[item],) else: cli_tuple += ('',) @@ -5877,7 +7646,15 @@ def dropcounters(): @click.option("-g", "--group", type=str, help="Group for this counter") @click.option("-d", "--desc", type=str, help="Description for this counter") @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def install(counter_name, alias, group, counter_type, desc, reasons, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def install(counter_name, alias, group, counter_type, desc, reasons, verbose, namespace): """Install a new drop counter""" command = ['dropconfig', '-c', 'install', '-n', str(counter_name), '-t', str(counter_type), '-r', str(reasons)] if alias: @@ -5886,6 +7663,8 @@ def install(counter_name, alias, group, counter_type, desc, reasons, verbose): command += ['-g', str(group)] if desc: command += ['-d', str(desc)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -5896,9 +7675,19 @@ def install(counter_name, alias, group, counter_type, desc, reasons, verbose): @dropcounters.command() @click.argument("counter_name", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def delete(counter_name, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def delete(counter_name, verbose, namespace): """Delete an existing drop counter""" command = ['dropconfig', '-c', 'uninstall', '-n', str(counter_name)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -5909,9 +7698,19 @@ def delete(counter_name, verbose): @click.argument("counter_name", type=str, required=True) @click.argument("reasons", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def add_reasons(counter_name, reasons, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def add_reasons(counter_name, reasons, verbose, namespace): """Add reasons to an existing drop counter""" command = ['dropconfig', '-c', 'add', '-n', str(counter_name), '-r', str(reasons)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -5922,9 +7721,19 @@ def add_reasons(counter_name, reasons, verbose): @click.argument("counter_name", type=str, required=True) @click.argument("reasons", type=str, required=True) @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def remove_reasons(counter_name, reasons, verbose): +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def remove_reasons(counter_name, reasons, verbose, namespace): """Remove reasons from an existing drop counter""" command = ['dropconfig', '-c', 'remove', '-n', str(counter_name), '-r', str(reasons)] + if namespace: + command += ['-ns', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -5943,7 +7752,8 @@ def remove_reasons(counter_name, reasons, verbose): @click.option('-ydrop', metavar='', type=click.IntRange(0, 100), help="Set yellow drop probability") @click.option('-gdrop', metavar='', type=click.IntRange(0, 100), help="Set green drop probability") @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output") -def ecn(profile, rmax, rmin, ymax, ymin, gmax, gmin, rdrop, ydrop, gdrop, verbose): +@multi_asic_util.multi_asic_click_option_namespace +def ecn(profile, rmax, rmin, ymax, ymin, gmax, gmin, rdrop, ydrop, gdrop, verbose, namespace): """ECN-related configuration tasks""" log.log_info("'ecn -profile {}' executing...".format(profile)) command = ['ecnconfig', '-p', str(profile)] @@ -5957,6 +7767,8 @@ def ecn(profile, rmax, rmin, ymax, ymin, gmax, gmin, rdrop, ydrop, gdrop, verbos if ydrop is not None: command += ['-ydrop', str(ydrop)] if gdrop is not None: command += ['-gdrop', str(gdrop)] if verbose: command += ["-vv"] + if namespace is not None: + command += ['-n', str(namespace)] clicommon.run_command(command, display_cmd=verbose) @@ -5966,13 +7778,29 @@ def ecn(profile, rmax, rmin, ymax, ymin, gmax, gmin, rdrop, ydrop, gdrop, verbos @config.command() @click.option('-p', metavar='', type=str, required=True, help="Profile name") @click.option('-a', metavar='', type=click.IntRange(-8,8), help="Set alpha for profile type dynamic") -@click.option('-s', metavar='', type=int, help="Set staticth for profile type static") -def mmu(p, a, s): +@click.option('-s', metavar='', type=click.IntRange(min=0), help="Set staticth for profile type static") +@click.option('-t', type=click.Choice(["on", "off"]), help="Set packet trimming eligibility") +@click.option('--verbose', '-vv', is_flag=True, help="Enable verbose output") +@click.option('--namespace', + '-n', + 'namespace', + default=None, + type=str, + show_default=True, + help='Namespace name or all', + callback=multi_asic_util.multi_asic_namespace_validation_callback) +def mmu(p, a, s, t, namespace, verbose): """mmuconfig configuration tasks""" log.log_info("'mmuconfig -p {}' executing...".format(p)) command = ['mmuconfig', '-p', str(p)] if a is not None: command += ['-a', str(a)] if s is not None: command += ['-s', str(s)] + if t is not None: + command += ['-t', str(t)] + if namespace is not None: + command += ['-n', str(namespace)] + if verbose: + command += ['-vv'] clicommon.run_command(command) @@ -5994,8 +7822,9 @@ def pfc(ctx): @pfc.command() @click.argument('interface_name', metavar='', required=True) @click.argument('status', type=click.Choice(['on', 'off'])) +@multi_asic_util.multi_asic_click_option_namespace @click.pass_context -def asymmetric(ctx, interface_name, status): +def asymmetric(ctx, interface_name, status, namespace): """Set asymmetric PFC configuration.""" # Get the config_db connector config_db = ctx.obj['config_db'] @@ -6005,7 +7834,11 @@ def asymmetric(ctx, interface_name, status): if interface_name is None: ctx.fail("'interface_name' is None!") - clicommon.run_command(['pfc', 'config', 'asymmetric', str(status), str(interface_name)]) + cmd = ['pfc', 'config', 'asymmetric', str(status), str(interface_name)] + if namespace is not None: + cmd += ['-n', str(namespace)] + + clicommon.run_command(cmd) # # 'pfc priority' command ('config interface pfc priority ...') @@ -6015,8 +7848,9 @@ def asymmetric(ctx, interface_name, status): @click.argument('interface_name', metavar='', required=True) @click.argument('priority', type=click.Choice([str(x) for x in range(8)])) @click.argument('status', type=click.Choice(['on', 'off'])) +@multi_asic_util.multi_asic_click_option_namespace @click.pass_context -def priority(ctx, interface_name, priority, status): +def priority(ctx, interface_name, priority, status, namespace): """Set PFC priority configuration.""" # Get the config_db connector config_db = ctx.obj['config_db'] @@ -6026,7 +7860,11 @@ def priority(ctx, interface_name, priority, status): if interface_name is None: ctx.fail("'interface_name' is None!") - clicommon.run_command(['pfc', 'config', 'priority', str(status), str(interface_name), str(priority)]) + cmd = ['pfc', 'config', 'priority', str(status), str(interface_name), str(priority)] + if namespace is not None: + cmd += ['-n', str(namespace)] + + clicommon.run_command(cmd) # # 'buffer' group ('config buffer ...') @@ -6444,13 +8282,13 @@ def add_loopback(ctx, loopback_name): config_db = ValidatedConfigDBConnector(ctx.obj['db']) if ADHOC_VALIDATION: if is_loopback_name_valid(loopback_name) is False: - ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' " - .format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO)) + ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' and should not exceed {} characters" + .format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO, IFACE_NAME_MAX_LEN)) lo_intfs = [k for k, v in config_db.get_table('LOOPBACK_INTERFACE').items() if type(k) != tuple] if loopback_name in lo_intfs: ctx.fail("{} already exists".format(loopback_name)) # TODO: MISSING CONSTRAINT IN YANG VALIDATION - + try: config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, {"NULL" : "NULL"}) except ValueError: @@ -6474,7 +8312,7 @@ def del_loopback(ctx, loopback_name): ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ] for ip in ips: config_db.set_entry('LOOPBACK_INTERFACE', (loopback_name, ip), None) - + try: config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, None) except JsonPatchConflict: @@ -6522,44 +8360,55 @@ def enable(enable): @click.pass_context def ntp(ctx): """NTP server configuration tasks""" - config_db = ConfigDBConnector() - config_db.connect() - ctx.obj = {'db': config_db} + # This is checking to see if ctx.obj is a dictionary, to differentiate it + # between unit test scenario and runtime scenario. + if not isinstance(ctx.obj, dict): + config_db = ConfigDBConnector() + config_db.connect() + ctx.obj = {'db': config_db} @ntp.command('add') @click.argument('ntp_ip_address', metavar='', required=True) +@click.option('--association-type', type=click.Choice(["server", "pool"], case_sensitive=False), default="server", + help="Define the association type for this NTP server") +@click.option('--iburst', is_flag=True, help="Enable iburst for this NTP server") +@click.option('--version', type=int, help="Specify the version for this NTP server") @click.pass_context -def add_ntp_server(ctx, ntp_ip_address): +def add_ntp_server(ctx, ntp_ip_address, association_type, iburst, version): """ Add NTP server IP """ if ADHOC_VALIDATION: - if not clicommon.is_ipaddress(ntp_ip_address): + if not clicommon.is_ipaddress(ntp_ip_address) and association_type != "pool": ctx.fail('Invalid IP address') - db = ValidatedConfigDBConnector(ctx.obj['db']) + db = ValidatedConfigDBConnector(ctx.obj['db']) ntp_servers = db.get_table("NTP_SERVER") if ntp_ip_address in ntp_servers: click.echo("NTP server {} is already configured".format(ntp_ip_address)) return else: + ntp_server_options = {} + if version: + ntp_server_options['version'] = version + if association_type != "server": + ntp_server_options['association_type'] = association_type + if iburst: + ntp_server_options['iburst'] = "on" try: - db.set_entry('NTP_SERVER', ntp_ip_address, {'NULL': 'NULL'}) + db.set_entry('NTP_SERVER', ntp_ip_address, ntp_server_options) except ValueError as e: - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) click.echo("NTP server {} added to configuration".format(ntp_ip_address)) try: - click.echo("Restarting ntp-config service...") - clicommon.run_command(['systemctl', 'restart', 'ntp-config'], display_cmd=False) + click.echo("Restarting chrony service...") + clicommon.run_command(['systemctl', 'restart', 'chrony'], display_cmd=False) except SystemExit as e: - ctx.fail("Restart service ntp-config failed with error {}".format(e)) + ctx.fail("Restart service chrony failed with error {}".format(e)) @ntp.command('del') @click.argument('ntp_ip_address', metavar='', required=True) @click.pass_context def del_ntp_server(ctx, ntp_ip_address): """ Delete NTP server IP """ - if ADHOC_VALIDATION: - if not clicommon.is_ipaddress(ntp_ip_address): - ctx.fail('Invalid IP address') - db = ValidatedConfigDBConnector(ctx.obj['db']) + db = ValidatedConfigDBConnector(ctx.obj['db']) ntp_servers = db.get_table("NTP_SERVER") if ntp_ip_address in ntp_servers: try: @@ -6570,10 +8419,10 @@ def del_ntp_server(ctx, ntp_ip_address): else: ctx.fail("NTP server {} is not configured.".format(ntp_ip_address)) try: - click.echo("Restarting ntp-config service...") - clicommon.run_command(['systemctl', 'restart', 'ntp-config'], display_cmd=False) + click.echo("Restarting chrony service...") + clicommon.run_command(['systemctl', 'restart', 'chrony'], display_cmd=False) except SystemExit as e: - ctx.fail("Restart service ntp-config failed with error {}".format(e)) + ctx.fail("Restart service chrony failed with error {}".format(e)) # # 'sflow' group ('config sflow ...') @@ -6887,19 +8736,19 @@ def add(ctx, name, ipaddr, port, vrf): if not is_valid_collector_info(name, ipaddr, port, vrf): return - config_db = ValidatedConfigDBConnector(ctx.obj['db']) + config_db = ValidatedConfigDBConnector(ctx.obj['db']) collector_tbl = config_db.get_table('SFLOW_COLLECTOR') if (collector_tbl and name not in collector_tbl and len(collector_tbl) == 2): click.echo("Only 2 collectors can be configured, please delete one") return - + try: config_db.mod_entry('SFLOW_COLLECTOR', name, {"collector_ip": ipaddr, "collector_port": port, "collector_vrf": vrf}) except ValueError as e: - ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) return # @@ -7190,6 +9039,8 @@ def add_subinterface(ctx, subinterface_name, vid): if interface_alias is None: ctx.fail("{} invalid subinterface".format(interface_alias)) + if not isInterfaceNameValid(subinterface_name): + ctx.fail("Subinterface name length should not exceed {} characters".format(IFACE_NAME_MAX_LEN)) if interface_alias.startswith("Po") is True: intf_table_name = CFG_PORTCHANNEL_PREFIX @@ -7232,7 +9083,7 @@ def add_subinterface(ctx, subinterface_name, vid): if vid is not None: subintf_dict.update({"vlan" : vid}) subintf_dict.update({"admin_status" : "up"}) - + try: config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, subintf_dict) except ValueError as e: @@ -7339,5 +9190,441 @@ def date(date, time): clicommon.run_command(['timedatectl', 'set-time', date_time]) +# +# 'asic-sdk-health-event' group ('config asic-sdk-health-event ...') +# +@config.group() +def asic_sdk_health_event(): + """Configuring asic-sdk-health-event""" + pass + + +@asic_sdk_health_event.group() +def suppress(): + """Suppress ASIC/SDK health event""" + pass + + +def handle_asic_sdk_health_suppress(db, severity, category_list, max_events, namespace): + ctx = click.get_current_context() + + if multi_asic.get_num_asics() > 1: + namespace_list = multi_asic.get_namespaces_from_linux() + else: + namespace_list = [DEFAULT_NAMESPACE] + + severityCapabilities = { + "fatal": "REG_FATAL_ASIC_SDK_HEALTH_CATEGORY", + "warning": "REG_WARNING_ASIC_SDK_HEALTH_CATEGORY", + "notice": "REG_NOTICE_ASIC_SDK_HEALTH_CATEGORY" + } + + if category_list: + categories = {"software", "firmware", "cpu_hw", "asic_hw"} + + if category_list == 'none': + suppressedCategoriesList = [] + elif category_list == 'all': + suppressedCategoriesList = list(categories) + else: + suppressedCategoriesList = category_list.split(',') + + unsupportCategories = set(suppressedCategoriesList) - categories + if unsupportCategories: + ctx.fail("Invalid category(ies): {}".format(unsupportCategories)) + + for ns in namespace_list: + if namespace and namespace != ns: + continue + + config_db = db.cfgdb_clients[ns] + state_db = db.db_clients[ns] + + entry_name = "SWITCH_CAPABILITY|switch" + if "true" != state_db.get(state_db.STATE_DB, entry_name, "ASIC_SDK_HEALTH_EVENT"): + ctx.fail("ASIC/SDK health event is not supported on the platform") + + if "true" != state_db.get(state_db.STATE_DB, entry_name, severityCapabilities[severity]): + ctx.fail("Suppressing ASIC/SDK health {} event is not supported on the platform".format(severity)) + + entry = config_db.get_entry("SUPPRESS_ASIC_SDK_HEALTH_EVENT", severity) + need_remove = False + noarg = True + + if category_list: + noarg = False + if suppressedCategoriesList: + entry["categories"] = suppressedCategoriesList + elif entry.get("categories"): + entry.pop("categories") + need_remove = True + + if max_events is not None: + noarg = False + if max_events > 0: + entry["max_events"] = max_events + elif entry.get("max_events"): + entry.pop("max_events") + need_remove = True + + if noarg: + ctx.fail("At least one argument should be provided!") + + if entry: + config_db.set_entry("SUPPRESS_ASIC_SDK_HEALTH_EVENT", severity, entry) + elif need_remove: + config_db.set_entry("SUPPRESS_ASIC_SDK_HEALTH_EVENT", severity, None) + + +@suppress.command() +@click.option('--category-list', metavar='', type=str, help="Categories to be suppressed") +@click.option('--max-events', metavar='', type=click.IntRange(0), help="Maximum number of received events") +@click.option('--namespace', '-n', 'namespace', required=False, default=None, show_default=False, + help='Option needed for multi-asic only: provide namespace name', + type=click.Choice(multi_asic_util.multi_asic_ns_choices())) +@clicommon.pass_db +def fatal(db, category_list, max_events, namespace): + handle_asic_sdk_health_suppress(db, 'fatal', category_list, max_events, namespace) + + +@suppress.command() +@click.option('--category-list', metavar='', type=str, help="Categories to be suppressed") +@click.option('--max-events', metavar='', type=click.IntRange(0), help="Maximum number of received events") +@click.option('--namespace', '-n', 'namespace', required=False, default=None, show_default=False, + help='Option needed for multi-asic only: provide namespace name', + type=click.Choice(multi_asic_util.multi_asic_ns_choices())) +@clicommon.pass_db +def warning(db, category_list, max_events, namespace): + handle_asic_sdk_health_suppress(db, 'warning', category_list, max_events, namespace) + + +@suppress.command() +@click.option('--category-list', metavar='', type=str, help="Categories to be suppressed") +@click.option('--max-events', metavar='', type=click.IntRange(0), help="Maximum number of received events") +@click.option('--namespace', '-n', 'namespace', required=False, default=None, show_default=False, + help='Option needed for multi-asic only: provide namespace name', + type=click.Choice(multi_asic_util.multi_asic_ns_choices())) +@clicommon.pass_db +def notice(db, category_list, max_events, namespace): + handle_asic_sdk_health_suppress(db, 'notice', category_list, max_events, namespace) + + +# +# 'serial_console' group ('config serial_console') +# +@config.group(cls=clicommon.AbbreviationGroup, name='serial_console') +def serial_console(): + """Configuring system serial-console behavior""" + pass + + +@serial_console.command('sysrq-capabilities') +@click.argument('sysrq_capabilities', metavar='', required=True, + type=click.Choice(['enabled', 'disabled'])) +def sysrq_capabilities(sysrq_capabilities): + """Set serial console sysrq-capabilities state""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry("SERIAL_CONSOLE", 'POLICIES', + {'sysrq_capabilities': sysrq_capabilities}) + + +@serial_console.command('inactivity-timeout') +@click.argument('inactivity_timeout', metavar='', required=True, + type=click.IntRange(0, 35000)) +def inactivity_timeout_serial(inactivity_timeout): + """Set serial console inactivity timeout""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry("SERIAL_CONSOLE", 'POLICIES', + {'inactivity_timeout': inactivity_timeout}) + + +# +# 'ssh' group ('config ssh') +# +@config.group(cls=clicommon.AbbreviationGroup, name='ssh') +def ssh(): + """Configuring system ssh behavior""" + pass + + +@ssh.command('inactivity-timeout') +@click.argument('inactivity_timeout', metavar='', required=True, + type=click.IntRange(0, 35000)) +def inactivity_timeout_ssh(inactivity_timeout): + """Set ssh inactivity timeout""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry("SSH_SERVER", 'POLICIES', + {'inactivity_timeout': inactivity_timeout}) + + +@ssh.command('max-sessions') +@click.argument('max-sessions', metavar='', required=True, + type=click.IntRange(0, 100)) +def max_sessions(max_sessions): + """Set max number of concurrent logins""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry("SSH_SERVER", 'POLICIES', + {'max_sessions': max_sessions}) + + +# +# 'banner' group ('config banner ...') +# +@config.group() +def banner(): + """Configuring system banner messages""" + pass + + +@banner.command() +@click.argument('state', metavar='', required=True, type=click.Choice(['enabled', 'disabled'])) +def state(state): + """Set banner feature state""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_BANNER_MESSAGE_TABLE_NAME, 'global', + {'state': state}) + + +@banner.command() +@click.argument('message', metavar='', required=True) +def login(message): + """Set login message""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_BANNER_MESSAGE_TABLE_NAME, 'global', + {'login': message}) + + +@banner.command() +@click.argument('message', metavar='', required=True) +def logout(message): + """Set logout message""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_BANNER_MESSAGE_TABLE_NAME, 'global', + {'logout': message}) + + +@banner.command() +@click.argument('message', metavar='', required=True) +def motd(message): + """Set message of the day""" + + config_db = ConfigDBConnector() + config_db.connect() + config_db.mod_entry(swsscommon.CFG_BANNER_MESSAGE_TABLE_NAME, 'global', + {'motd': message}) + + +# +# 'vnet' group ('config vnet ...') +# + +@config.group(cls=clicommon.AbbreviationGroup, name='vnet') +@click.pass_context +def vnet(ctx): + """VNET-related configuration tasks""" + config_db = ConfigDBConnector() + config_db.connect() + ctx.obj = {} + ctx.obj['config_db'] = config_db + + +@vnet.command('add') +@click.argument('vnet_name', metavar='', type=str, required=True) +@click.argument('vni', metavar='', required=True) +@click.argument('vxlan_tunnel', metavar='', required=True) +@click.argument('peer_list', metavar='', required=False) +@click.argument('guid', metavar='', type=str, required=False) +@click.argument('scope', metavar='', required=False) +@click.argument('advertise_prefix', metavar='', type=bool, required=False) +@click.argument('overlay_dmac', metavar='', required=False) +@click.argument('src_mac', metavar='', required=False) +@click.pass_context +def add_vnet(ctx, vnet_name, vni, vxlan_tunnel, peer_list, guid, scope, advertise_prefix, overlay_dmac, src_mac): + """Add Vnet""" + config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) + + vnet_name_is_valid(ctx, vnet_name) + + if not vni.isdigit() or clicommon.vni_id_is_valid(int(vni)) is False: + ctx.fail("Invalid VNI {}. Valid range [1 to 16777215].".format(vni)) + if not clicommon.is_vxlan_tunnel_exists(config_db, vxlan_tunnel): + ctx.fail("Vxlan tunnel {} does not exist!".format(vxlan_tunnel)) + + subvnet_dict = {} + subvnet_dict["vni"] = vni + subvnet_dict["vxlan_tunnel"] = vxlan_tunnel + + if peer_list: + peer_list = peer_list.split(',') + for peer in peer_list: + vnet_name_is_valid(ctx, peer) + subvnet_dict["peer_list"] = peer_list + + if guid: + if len(guid) > GUID_MAX_LEN: + ctx.fail("'guid' length should not exceed {} characters".format(GUID_MAX_LEN)) + subvnet_dict["guid"] = guid + + if scope: + if scope != 'default': + ctx.fail("Only 'default' value is allowed for scope!") + subvnet_dict["scope"] = scope + + if advertise_prefix: + subvnet_dict["advertise_prefix"] = advertise_prefix + + if overlay_dmac: + if not clicommon.is_mac_address_valid(overlay_dmac): + ctx.fail("Invalid MAC for overlay dmac {} .".format(overlay_dmac)) + subvnet_dict["overlay_dmac"] = overlay_dmac + + if src_mac: + if not clicommon.is_mac_address_valid(src_mac): + ctx.fail("Invalid MAC for src mac {} .".format(src_mac)) + subvnet_dict["src_mac"] = src_mac + + try: + config_db.set_entry('VNET', vnet_name, subvnet_dict) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + click.echo("VNET {} is added/updated.".format(vnet_name)) + + +@vnet.command('del') +@click.argument('vnet_name', metavar='', required=True) +@click.pass_context +def del_vnet(ctx, vnet_name): + """Del Vnet""" + config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) + + vnet_name_is_valid(ctx, vnet_name) + + if not is_vnet_exists(config_db, vnet_name): + ctx.fail("VNET {} does not exist!".format(vnet_name)) + else: + del_interface_bind_to_vnet(config_db, vnet_name) + del_route_bind_to_vnet(config_db, vnet_name) + + try: + config_db.set_entry('VNET', vnet_name, None) + except JsonPatchConflict as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + click.echo("VNET {} deleted and all associated IP addresses and routes removed.".format(vnet_name)) + + +@vnet.command('add-route') +@click.argument('vnet_name', metavar='', type=str, required=True) +@click.argument('prefix', metavar='', required=True) +@click.argument('endpoint', metavar='', required=True) +@click.argument('vni', metavar='', required=False) +@click.argument('mac_address', metavar='', required=False) +@click.argument('endpoint_monitor', metavar='', required=False) +@click.argument('profile', metavar='', type=str, required=False) +@click.argument('primary', metavar='', required=False) +@click.argument('monitoring', metavar='', type=str, required=False) +@click.argument('adv_prefix', metavar='', required=False) +@click.pass_context +def add_vnet_route(ctx, vnet_name, prefix, endpoint, vni, mac_address, endpoint_monitor, + profile, primary, monitoring, adv_prefix): + """Add/Update VNET Route""" + config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) + + vnet_name_is_valid(ctx, vnet_name) + + if not is_vnet_exists(config_db, vnet_name): + ctx.fail("VNET {} doesnot exist, cannot add a route!".format(vnet_name)) + if not clicommon.is_ipprefix(prefix): + ctx.fail("Invalid prefix {}".format(prefix)) + else: + if not clicommon.is_ipaddress(endpoint): + ctx.fail("Endpoint has invalid IP address {}".format(endpoint)) + + subvnet_dict = {} + subvnet_dict["endpoint"] = endpoint + + if vni: + if not vni.isdigit() or clicommon.vni_id_is_valid(int(vni)) is False: + ctx.fail("Invalid VNI {}. Valid range [1 to 16777215].".format(vni)) + subvnet_dict["vni"] = vni + + if mac_address: + if not clicommon.is_mac_address_valid(mac_address): + ctx.fail("Invalid MAC {}".format(mac_address)) + subvnet_dict["mac_address"] = mac_address + + if endpoint_monitor: + if not clicommon.is_ipaddress(endpoint_monitor): + ctx.fail("Endpoint monitor has invalid IP address {}".format(endpoint_monitor)) + subvnet_dict["endpoint_monitor"] = endpoint_monitor + + if profile: + subvnet_dict['profile'] = profile + + if primary: + if not clicommon.is_ipaddress(primary): + ctx.fail("Primary has invalid IP address {}".format(primary)) + subvnet_dict['primary'] = primary + + if monitoring: + subvnet_dict['monitoring'] = monitoring + + if adv_prefix: + if not clicommon.is_ipprefix(adv_prefix): + ctx.fail("Invalid adv_prefix {}".format(adv_prefix)) + subvnet_dict['adv_prefix'] = adv_prefix + + try: + config_db.set_entry('VNET_ROUTE_TUNNEL', (vnet_name, prefix), subvnet_dict) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + click.echo("VNET route added/updated for the VNET {}.".format(vnet_name)) + + +@vnet.command('del-route') +@click.argument('vnet_name', metavar='', type=str, required=True) +@click.argument('prefix', metavar='', required=False) +@click.pass_context +def del_vnet_route(ctx, vnet_name, prefix): + """Del a specific VNET route or all VNET routes""" + config_db = ValidatedConfigDBConnector(ctx.obj['config_db']) + + vnet_name_is_valid(ctx, vnet_name) + + if not is_vnet_exists(config_db, vnet_name): + ctx.fail("VNET {} doesnot exist, cannot delete the route!".format(vnet_name)) + if not is_vnet_route_exists(config_db, vnet_name): + ctx.fail("Routes dont exist for the VNET {}, cant delete it!".format(vnet_name)) + if prefix: + if not clicommon.is_ipprefix(prefix): + ctx.fail("Invalid prefix {}".format(prefix)) + if not is_specific_vnet_route_exists(config_db, vnet_name, prefix): + ctx.fail("Route does not exist for the VNET {}, cant delete it!".format(vnet_name)) + else: + for key in config_db.get_table('VNET_ROUTE_TUNNEL'): + if key[0] == vnet_name and key[1] == prefix: + try: + config_db.set_entry('VNET_ROUTE_TUNNEL', (vnet_name, prefix), None) + except ValueError as e: + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + click.echo("Specific route deleted for the VNET {}.".format(vnet_name)) + else: + del_route_bind_to_vnet(config_db, vnet_name) + click.echo("All routes deleted for the VNET {}.".format(vnet_name)) + + if __name__ == '__main__': config() diff --git a/config/memory_statistics.py b/config/memory_statistics.py new file mode 100644 index 0000000000..5205b4e89e --- /dev/null +++ b/config/memory_statistics.py @@ -0,0 +1,331 @@ +# Standard library imports +import syslog + +# Third-party imports +import click + +# Type hints +from typing import Tuple, Optional + +# Local imports +from swsscommon.swsscommon import ConfigDBConnector + +# Constants +MEMORY_STATISTICS_TABLE = "MEMORY_STATISTICS" +MEMORY_STATISTICS_KEY = "memory_statistics" + +SAMPLING_INTERVAL_MIN = 3 +SAMPLING_INTERVAL_MAX = 15 +RETENTION_PERIOD_MIN = 1 +RETENTION_PERIOD_MAX = 30 + +DEFAULT_SAMPLING_INTERVAL = 5 # minutes +DEFAULT_RETENTION_PERIOD = 15 # days + +syslog.openlog("memory_statistics_config", syslog.LOG_PID | syslog.LOG_CONS, syslog.LOG_USER) + + +def log_to_syslog(message: str, level: int = syslog.LOG_INFO) -> None: + """ + Logs a message to the system log (syslog) with error handling. + + This function logs the provided message to syslog at the specified level. + It handles potential errors such as system-related issues (OSError) and + invalid parameters (ValueError) by displaying appropriate error messages. + + Args: + message (str): The message to log. + level (int, optional): The log level (default is syslog.LOG_INFO). + """ + try: + syslog.syslog(level, message) + except OSError as e: + click.echo(f"System error while logging to syslog: {e}", err=True) + except ValueError as e: + click.echo(f"Invalid syslog parameters: {e}", err=True) + + +def generate_error_message(error_type: str, error: Exception) -> str: + """ + Generates a formatted error message for logging and user feedback. + + Args: + error_type (str): A short description of the error type. + error (Exception): The actual exception object. + + Returns: + str: A formatted error message string. + """ + return f"{error_type}: {error}" + + +def validate_range(value: int, min_val: int, max_val: int) -> bool: + """ + Validates whether a given integer value falls within a specified range. + + Args: + value (int): The value to validate. + min_val (int): The minimum allowable value. + max_val (int): The maximum allowable value. + + Returns: + bool: True if the value is within range, False otherwise. + """ + return min_val <= value <= max_val + + +class MemoryStatisticsDB: + """ + Singleton class for managing the connection to the memory statistics configuration database. + """ + _instance = None + _db = None + + def __new__(cls): + """ + Creates and returns a singleton instance of MemoryStatisticsDB. + + Returns: + MemoryStatisticsDB: The singleton instance. + """ + if cls._instance is None: + cls._instance = super(MemoryStatisticsDB, cls).__new__(cls) + cls._connect_db() + return cls._instance + + @classmethod + def _connect_db(cls): + """ + Establishes a connection to the ConfigDB database. + + Logs an error if the connection fails. + """ + try: + cls._db = ConfigDBConnector() + cls._db.connect() + except RuntimeError as e: + log_to_syslog(f"ConfigDB connection failed: {e}", syslog.LOG_ERR) + cls._db = None + + @classmethod + def get_db(cls): + """ + Retrieves the database connection instance, reconnecting if necessary. + + Returns: + ConfigDBConnector: The active database connection. + + Raises: + RuntimeError: If the database connection is unavailable. + """ + if cls._db is None: + cls._connect_db() + if cls._db is None: + raise RuntimeError("Database connection unavailable") + return cls._db + + +def update_memory_statistics_status(enabled: bool) -> Tuple[bool, Optional[str]]: + """ + Updates the enable/disable status of memory statistics in the configuration database. + + This function modifies the configuration database to enable or disable + memory statistics collection based on the provided status. It also logs + the action and returns a tuple indicating whether the operation was successful. + + Args: + enabled (bool): True to enable memory statistics, False to disable. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing success status and an optional error message. + """ + try: + db = MemoryStatisticsDB.get_db() + + db.mod_entry(MEMORY_STATISTICS_TABLE, MEMORY_STATISTICS_KEY, {"enabled": str(enabled).lower()}) + msg = f"Memory statistics feature {'enabled' if enabled else 'disabled'} successfully." + click.echo(msg) + log_to_syslog(msg) + return True, None + except (KeyError, ConnectionError, RuntimeError) as e: + error_msg = generate_error_message(f"Failed to {'enable' if enabled else 'disable'} memory statistics", e) + + click.echo(error_msg, err=True) + log_to_syslog(error_msg, syslog.LOG_ERR) + return False, error_msg + + +@click.group(help="Tool to manage memory statistics configuration.") +def cli(): + """ + Memory statistics configuration tool. + + This command-line interface (CLI) allows users to configure and manage + memory statistics settings such as enabling/disabling the feature and + modifying parameters like the sampling interval and retention period. + """ + pass + + +@cli.group(help="Commands to configure system settings.") +def config(): + """ + Configuration commands for managing memory statistics. + + Example: + $ config memory-stats enable + $ config memory-stats sampling-interval 5 + """ + pass + + +@config.group(name='memory-stats', help="Manage memory statistics collection settings.") +def memory_stats(): + """Configure memory statistics collection and settings. + + This group contains commands to enable/disable memory statistics collection + and configure related parameters. + + Examples: + Enable memory statistics: + $ config memory-stats enable + + Set sampling interval to 5 minutes: + $ config memory-stats sampling-interval 5 + + Set retention period to 7 days: + $ config memory-stats retention-period 7 + + Disable memory statistics: + $ config memory-stats disable + """ + pass + + +@memory_stats.command(name='enable') +def memory_stats_enable(): + """Enable memory statistics collection. + + This command enables the collection of memory statistics on the device. + It updates the configuration and reminds the user to run 'config save' + to persist changes. + + Example: + $ config memory-stats enable + Memory statistics feature enabled successfully. + Reminder: Please run 'config save' to persist changes. + """ + success, error = update_memory_statistics_status(True) + if success: + click.echo("Reminder: Please run 'config save' to persist changes.") + log_to_syslog("Memory statistics enabled. Reminder to run 'config save'") + + +@memory_stats.command(name='disable') +def memory_stats_disable(): + """Disable memory statistics collection. + + This command disables the collection of memory statistics on the device. + It updates the configuration and reminds the user to run 'config save' + to persist changes. + + Example: + $ config memory-stats disable + Memory statistics feature disabled successfully. + Reminder: Please run 'config save' to persist changes. + """ + success, error = update_memory_statistics_status(False) + if success: + click.echo("Reminder: Please run 'config save' to persist changes.") + log_to_syslog("Memory statistics disabled. Reminder to run 'config save'") + + +@memory_stats.command(name='sampling-interval') +@click.argument("interval", type=int) +def memory_stats_sampling_interval(interval: int): + """ + Configure the sampling interval for memory statistics collection. + + This command updates the interval at which memory statistics are collected. + The interval must be between 3 and 15 minutes. + + Args: + interval (int): The desired sampling interval in minutes. + + Examples: + Set sampling interval to 5 minutes: + $ config memory-stats sampling-interval 5 + Sampling interval set to 5 minutes successfully. + Reminder: Please run 'config save' to persist changes. + + Invalid interval example: + $ config memory-stats sampling-interval 20 + Error: Sampling interval must be between 3 and 15 minutes. + """ + if not validate_range(interval, SAMPLING_INTERVAL_MIN, SAMPLING_INTERVAL_MAX): + error_msg = f"Error: Sampling interval must be between {SAMPLING_INTERVAL_MIN} and {SAMPLING_INTERVAL_MAX}." + click.echo(error_msg, err=True) + log_to_syslog(error_msg, syslog.LOG_ERR) + return + + try: + db = MemoryStatisticsDB.get_db() + db.mod_entry(MEMORY_STATISTICS_TABLE, MEMORY_STATISTICS_KEY, {"sampling_interval": str(interval)}) + success_msg = f"Sampling interval set to {interval} minutes successfully." + click.echo(success_msg) + log_to_syslog(success_msg) + click.echo("Reminder: Please run 'config save' to persist changes.") + except (KeyError, ConnectionError, ValueError, RuntimeError) as e: + error_msg = generate_error_message(f"{type(e).__name__} setting sampling interval", e) + click.echo(error_msg, err=True) + log_to_syslog(error_msg, syslog.LOG_ERR) + return + + +@memory_stats.command(name='retention-period') +@click.argument("period", type=int) +def memory_stats_retention_period(period: int): + """ + Configure the retention period for memory statistics storage. + + This command sets the number of days memory statistics should be retained. + The retention period must be between 1 and 30 days. + + Args: + period (int): The desired retention period in days. + + Examples: + Set retention period to 7 days: + $ config memory-stats retention-period 7 + Retention period set to 7 days successfully. + Reminder: Please run 'config save' to persist changes. + + Invalid period example: + $ config memory-stats retention-period 45 + Error: Retention period must be between 1 and 30 days. + """ + if not validate_range(period, RETENTION_PERIOD_MIN, RETENTION_PERIOD_MAX): + error_msg = f"Error: Retention period must be between {RETENTION_PERIOD_MIN} and {RETENTION_PERIOD_MAX}." + click.echo(error_msg, err=True) + log_to_syslog(error_msg, syslog.LOG_ERR) + return + + try: + db = MemoryStatisticsDB.get_db() + db.mod_entry(MEMORY_STATISTICS_TABLE, MEMORY_STATISTICS_KEY, {"retention_period": str(period)}) + success_msg = f"Retention period set to {period} days successfully." + click.echo(success_msg) + log_to_syslog(success_msg) + click.echo("Reminder: Please run 'config save' to persist changes.") + except (KeyError, ConnectionError, ValueError, RuntimeError) as e: + error_msg = generate_error_message(f"{type(e).__name__} setting retention period", e) + click.echo(error_msg, err=True) + log_to_syslog(error_msg, syslog.LOG_ERR) + return + + +if __name__ == "__main__": + try: + cli() + finally: + syslog.closelog() diff --git a/config/muxcable.py b/config/muxcable.py index ba80cb02af..d7d672a930 100644 --- a/config/muxcable.py +++ b/config/muxcable.py @@ -5,6 +5,7 @@ import click import re +from natsort import natsorted import utilities_common.cli as clicommon from sonic_py_common import multi_asic from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector @@ -276,14 +277,52 @@ def lookup_statedb_and_update_configdb(db, per_npu_statedb, config_db, port, sta port_status_dict[port_name] = 'OK' def update_configdb_pck_loss_data(config_db, port, val): + fvs = {} configdb_state = get_value_for_key_in_config_tbl(config_db, port, "state", "MUX_CABLE") + fvs["state"] = configdb_state ipv4_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv4", "MUX_CABLE") + fvs["server_ipv4"] = ipv4_value ipv6_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv6", "MUX_CABLE") + fvs["server_ipv6"] = ipv6_value + soc_ipv4_value = get_optional_value_for_key_in_config_tbl(config_db, port, "soc_ipv4", "MUX_CABLE") + if soc_ipv4_value is not None: + fvs["soc_ipv4"] = soc_ipv4_value + cable_type = get_optional_value_for_key_in_config_tbl(config_db, port, "cable_type", "MUX_CABLE") + if cable_type is not None: + fvs["cable_type"] = cable_type + prober_type_val = get_optional_value_for_key_in_config_tbl(config_db, port, "prober_type", "MUX_CABLE") + if prober_type_val is not None: + fvs["prober_type"] = prober_type_val + + fvs["pck_loss_data_reset"] = val + try: + config_db.set_entry("MUX_CABLE", port, fvs) + except ValueError as e: + ctx = click.get_current_context() + ctx.fail("Invalid ConfigDB. Error: {}".format(e)) + + +def update_configdb_prober_type(config_db, port, val): + fvs = {} + configdb_state = get_value_for_key_in_config_tbl(config_db, port, "state", "MUX_CABLE") + fvs["state"] = configdb_state + ipv4_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv4", "MUX_CABLE") + fvs["server_ipv4"] = ipv4_value + ipv6_value = get_value_for_key_in_config_tbl(config_db, port, "server_ipv6", "MUX_CABLE") + fvs["server_ipv6"] = ipv6_value + soc_ipv4_value = get_optional_value_for_key_in_config_tbl(config_db, port, "soc_ipv4", "MUX_CABLE") + if soc_ipv4_value is not None: + fvs["soc_ipv4"] = soc_ipv4_value + cable_type = get_optional_value_for_key_in_config_tbl(config_db, port, "cable_type", "MUX_CABLE") + if cable_type is not None: + fvs["cable_type"] = cable_type + pck_loss_data = get_optional_value_for_key_in_config_tbl(config_db, port, "pck_loss_data_reset", "MUX_CABLE") + if pck_loss_data is not None: + fvs["pck_loss_data_reset"] = pck_loss_data + fvs["prober_type"] = val try: - config_db.set_entry("MUX_CABLE", port, {"state": configdb_state, - "server_ipv4": ipv4_value, "server_ipv6": ipv6_value, - "pck_loss_data_reset": val}) + config_db.set_entry("MUX_CABLE", port, fvs) except ValueError as e: ctx = click.get_current_context() ctx.fail("Invalid ConfigDB. Error: {}".format(e)) @@ -380,6 +419,73 @@ def mode(db, state, port, json_output): sys.exit(CONFIG_SUCCESSFUL) +# 'muxcable' command ("config muxcable probertype hardware/software ") +@muxcable.command() +@click.argument('probertype', metavar='', required=True, type=click.Choice(["hardware", "software"])) +@click.argument('port', metavar='', required=True, default=None) +@clicommon.pass_db +def probertype(db, probertype, port): + """Config muxcable probertype""" + + port = platform_sfputil_helper.get_interface_name(port, db) + + port_table_keys = {} + y_cable_asic_table_keys = {} + per_npu_configdb = {} + per_npu_statedb = {} + + # Getting all front asic namespace and correspding config and state DB connector + + namespaces = multi_asic.get_front_end_namespaces() + for namespace in namespaces: + asic_id = multi_asic.get_asic_index_from_namespace(namespace) + # replace these with correct macros + per_npu_configdb[asic_id] = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + per_npu_configdb[asic_id].connect() + per_npu_statedb[asic_id] = SonicV2Connector(use_unix_socket_path=True, namespace=namespace) + per_npu_statedb[asic_id].connect(per_npu_statedb[asic_id].STATE_DB) + + port_table_keys[asic_id] = per_npu_statedb[asic_id].keys( + per_npu_statedb[asic_id].STATE_DB, 'MUX_CABLE_TABLE|*') + + if port is not None and port != "all": + + asic_index = None + if platform_sfputil is not None: + asic_index = platform_sfputil.get_asic_id_for_logical_port(port) + if asic_index is None: + # TODO this import is only for unit test purposes, and should be removed once sonic_platform_base + # is fully mocked + import sonic_platform_base.sonic_sfp.sfputilhelper + asic_index = sonic_platform_base.sonic_sfp.sfputilhelper.SfpUtilHelper().get_asic_id_for_logical_port(port) + if asic_index is None: + click.echo("Got invalid asic index for port {}, cant retreive mux status".format(port)) + sys.exit(CONFIG_FAIL) + + if per_npu_statedb[asic_index] is not None: + y_cable_asic_table_keys = port_table_keys[asic_index] + logical_key = "MUX_CABLE_TABLE|{}".format(port) + if logical_key in y_cable_asic_table_keys: + update_configdb_prober_type(per_npu_configdb[asic_index], port, probertype) + sys.exit(CONFIG_SUCCESSFUL) + else: + click.echo("this is not a valid port {} present on mux_cable".format(port)) + sys.exit(CONFIG_FAIL) + else: + click.echo("there is not a valid asic asic-{} table for this asic_index".format(asic_index)) + sys.exit(CONFIG_FAIL) + + elif port == "all" and port is not None: + + for namespace in namespaces: + asic_id = multi_asic.get_asic_index_from_namespace(namespace) + for key in port_table_keys[asic_id]: + logical_port = key.split("|")[1] + update_configdb_prober_type(per_npu_configdb[asic_id], logical_port, probertype) + + sys.exit(CONFIG_SUCCESSFUL) + + # 'muxcable' command ("config muxcable kill-radv ") @muxcable.command(short_help="Kill radv service when it is meant to be stopped, so no good-bye packet is sent for ceasing To Be an Advertising Interface") @click.argument('knob', metavar='', required=True, type=click.Choice(["enable", "disable"])) @@ -395,8 +501,7 @@ def kill_radv(db, knob): mux_lmgrd_cfg_tbl = config_db.get_table("MUX_LINKMGR") config_db.mod_entry("MUX_LINKMGR", "SERVICE_MGMT", {"kill_radv": "True" if knob == "enable" else "False"}) - - #'muxcable' command ("config muxcable packetloss reset ") +# 'muxcable' command ("config muxcable packetloss reset ") @muxcable.command() @click.argument('action', metavar='', required=True, type=click.Choice(["reset"])) @click.argument('port', metavar='', required=True, default=None) @@ -1269,3 +1374,82 @@ def telemetry(db, state): else: click.echo("ERR: Unable to set ycabled telemetry state to {}".format(state)) sys.exit(CONFIG_FAIL) + + +# 'muxcable' command ("config muxcable reset-heartbeat-suspend ") +@muxcable.command() +@click.argument('port', metavar='', required=True, default=None) +@clicommon.pass_db +def reset_heartbeat_suspend(db, port): + """Reset the mux port heartbeat suspend.""" + + if port is None: + click.echo("no port provided") + sys.exit(CONFIG_FAIL) + + port = platform_sfputil_helper.get_interface_name(port, db) + asic_index = multi_asic.get_asic_index_from_namespace(EMPTY_NAMESPACE) + config_dbs = {} + mux_linkmgrd_tables = {} + mux_config_tables = {} + + # Getting all front asic namespace and correspding config DB connector + namespaces = multi_asic.get_front_end_namespaces() + for namespace in namespaces: + asic_index = multi_asic.get_asic_index_from_namespace(namespace) + config_dbs[asic_index] = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_dbs[asic_index].connect() + mux_linkmgrd_tables[asic_index] = config_dbs[asic_index].get_table("MUX_LINKMGR") + mux_config_tables[asic_index] = config_dbs[asic_index].get_table("MUX_CABLE") + + mux_ports = [] + if port == "all": + for asic_index, mux_config_table in mux_config_tables.items(): + config_db = config_dbs[asic_index] + mux_linkmgrd_table = mux_linkmgrd_tables[asic_index] + mux_ports = [p for p, c in mux_config_table.items() + if c.get("cable_type", "active-standby") == "active-standby"] + if mux_ports: + # trigger one-shot heartbeat suspend reset + config_db.mod_entry("MUX_LINKMGR", "LINK_PROBER", {"reset_suspend_timer": ",".join(mux_ports)}) + # restore config db to the original + config_db.set_entry("MUX_LINKMGR", "LINK_PROBER", mux_linkmgrd_table.get("LINK_PROBER", None)) + else: + asic_index = None + if platform_sfputil is not None: + asic_index = platform_sfputil.get_asic_id_for_logical_port(port) + if asic_index is None: + # TODO this import is only for unit test purposes, and should be removed once sonic_platform_base + # is fully mocked + import sonic_platform_base.sonic_sfp.sfputilhelper + asic_index = sonic_platform_base.sonic_sfp.sfputilhelper.SfpUtilHelper().get_asic_id_for_logical_port(port) + if asic_index is None: + click.echo("Got invalid asic index for port {}, can't reset heartbeat suspend".format(port)) + sys.exit(CONFIG_FAIL) + if asic_index in config_dbs: + config_db = config_dbs[asic_index] + mux_linkmgrd_table = mux_linkmgrd_tables[asic_index] + mux_config_table = mux_config_tables[asic_index] + if port not in mux_config_table: + click.echo("Got invalid port {}, can't reset heartbeat suspend'".format(port)) + sys.exit(CONFIG_FAIL) + elif mux_config_table[port].get("cable_type", "active-standby") != "active-standby": + click.echo( + "Got invalid port {}, can't reset heartbeat suspend on active-active mux port".format(port) + ) + sys.exit(CONFIG_FAIL) + mux_ports.append(port) + # trigger one-shot heartbeat suspend reset + config_db.mod_entry("MUX_LINKMGR", "LINK_PROBER", {"reset_suspend_timer": port}) + # restore config db to the original + config_db.set_entry("MUX_LINKMGR", "LINK_PROBER", mux_linkmgrd_table.get("LINK_PROBER", None)) + else: + click.echo("Got invalid asic index for port {}, can't reset heartbeat suspend'".format(port)) + sys.exit(CONFIG_FAIL) + + if not mux_ports: + click.echo("No mux ports found to reset heartbeat suspend") + sys.exit(CONFIG_FAIL) + + mux_ports = natsorted(mux_ports) + click.echo("Success in resetting heartbeat suspend for mux ports: {}".format(", ".join(mux_ports))) diff --git a/config/plugins/mlnx.py b/config/plugins/mlnx.py index 75846d54e3..115b310f69 100644 --- a/config/plugins/mlnx.py +++ b/config/plugins/mlnx.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2017-2021 NVIDIA CORPORATION & AFFILIATES. +# Copyright (c) 2017-2024 NVIDIA CORPORATION & AFFILIATES. # Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,7 @@ ENV_VARIABLE_SX_SNIFFER_TARGET = 'SX_SNIFFER_TARGET' # SDK sniffer file path and name -SDK_SNIFFER_TARGET_PATH = '/var/log/mellanox/sniffer/' +SDK_SNIFFER_TARGET_PATH = '/var/log/sdk_dbg/' SDK_SNIFFER_FILENAME_PREFIX = 'sx_sdk_sniffer_' SDK_SNIFFER_FILENAME_EXT = '.pcap' @@ -164,40 +164,6 @@ def mlnx(): """ Mellanox platform configuration tasks """ pass - -# 'sniffer' group -@mlnx.group() -def sniffer(): - """ Utility for managing Mellanox SDK/PRM sniffer """ - pass - - -# 'sdk' subgroup -@sniffer.group() -def sdk(): - """SDK Sniffer - Command Line to enable/disable SDK sniffer""" - pass - - -@sdk.command() -@click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, expose_value=False, - prompt='Swss service will be restarted, continue?') -def enable(): - """Enable SDK Sniffer""" - click.echo("Enabling SDK sniffer") - sdk_sniffer_enable() - click.echo("Note: the sniffer file may exhaust the space on /var/log, please disable it when you are done with this sniffering.") - - -@sdk.command() -@click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, expose_value=False, - prompt='Swss service will be restarted, continue?') -def disable(): - """Disable SDK Sniffer""" - click.echo("Disabling SDK sniffer") - sdk_sniffer_disable() - - def sdk_sniffer_enable(): """Enable SDK Sniffer""" sdk_sniffer_filename = sniffer_filename_generate(SDK_SNIFFER_TARGET_PATH, @@ -216,7 +182,7 @@ def sdk_sniffer_enable(): env_variable_string=sdk_sniffer_env_variable_string) if not ignore: err = restart_swss() - if err is not 0: + if err != 0: return click.echo('SDK sniffer is Enabled, recording file is %s.' % sdk_sniffer_filename) else: @@ -229,7 +195,7 @@ def sdk_sniffer_disable(): ignore = sniffer_env_variable_set(enable=False, env_variable_name=ENV_VARIABLE_SX_SNIFFER) if not ignore: err = restart_swss() - if err is not 0: + if err != 0: return click.echo("SDK sniffer is Disabled.") else: diff --git a/config/plugins/sonic-fine-grained-ecmp_yang.py b/config/plugins/sonic-fine-grained-ecmp_yang.py new file mode 100644 index 0000000000..3416d2f1f7 --- /dev/null +++ b/config/plugins/sonic-fine-grained-ecmp_yang.py @@ -0,0 +1,514 @@ +""" +Autogenerated config CLI plugin. +""" + +import copy +import click +import utilities_common.cli as clicommon +import utilities_common.general as general +from config import config_mgmt + + +# Load sonic-cfggen from source since /usr/local/bin/sonic-cfggen does not have .py extension. +sonic_cfggen = general.load_module_from_source('sonic_cfggen', '/usr/local/bin/sonic-cfggen') + + +def exit_with_error(*args, **kwargs): + """ Print a message with click.secho and abort CLI. + + Args: + args: Positional arguments to pass to click.secho + kwargs: Keyword arguments to pass to click.secho + """ + + click.secho(*args, **kwargs) + raise click.Abort() + + +def validate_config_or_raise(cfg): + """ Validate config db data using ConfigMgmt. + + Args: + cfg (Dict): Config DB data to validate. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + try: + cfg = sonic_cfggen.FormatConverter.to_serialized(copy.deepcopy(cfg)) + config_mgmt.ConfigMgmt().loadData(cfg) + except Exception as err: + raise Exception('Failed to validate configuration: {}'.format(err)) + + +def add_entry_validated(db, table, key, data): + """ Add new entry in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + data (Dict): Entry data. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key in cfg[table]: + raise Exception(f"{key} already exists") + + cfg[table][key] = data + + validate_config_or_raise(cfg) + db.set_entry(table, key, data) + + +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 obect. + 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 Exception(f"No field/values to update {key}") + + if create_if_not_exists: + cfg[table].setdefault(key, {}) + + if key not in cfg[table]: + raise Exception(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 + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def del_entry_validated(db, table, key): + """ Delete entry in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + + cfg[table].pop(key) + + validate_config_or_raise(cfg) + db.set_entry(table, key, None) + + +def add_list_entry_validated(db, table, key, attr, data): + """ Add new entry into list in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add data to. + key (Union[str, Tuple]): Key name in the table. + attr (str): Attribute name which represents a list the data needs to be added to. + data (List): Data list to add to config DB. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + cfg[table][key].setdefault(attr, []) + for entry in data: + if entry in cfg[table][key][attr]: + raise Exception(f"{entry} already exists") + cfg[table][key][attr].append(entry) + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def del_list_entry_validated(db, table, key, attr, data): + """ Delete entry from list in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to remove data from. + key (Union[str, Tuple]): Key name in the table. + attr (str): Attribute name which represents a list the data needs to be removed from. + data (Dict): Data list to remove from config DB. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + cfg[table][key].setdefault(attr, []) + for entry in data: + if entry not in cfg[table][key][attr]: + raise Exception(f"{entry} does not exist") + cfg[table][key][attr].remove(entry) + if not cfg[table][key][attr]: + cfg[table][key].pop(attr) + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def clear_list_entry_validated(db, table, key, attr): + """ Clear list in object and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to remove the list attribute from. + key (Union[str, Tuple]): Key name in the table. + attr (str): Attribute name which represents a list that needs to be removed. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + update_entry_validated(db, table, key, {attr: None}) + + +@click.group(name="fg-nhg", + cls=clicommon.AliasedGroup) +def FG_NHG(): + """ FG_NHG part of config_db.json """ + + pass + + +@FG_NHG.command(name="add") +@click.argument( + "name", + nargs=1, + required=True, +) +@click.option( + "--bucket-size", + help="total hash bucket size desired, recommended value of Lowest Common Multiple of \ + 1..max-next-hops.[mandatory]", +) +@click.option( + "--match-mode", + help="The filtering method used to identify when to use Fine Grained vs regular route handling. \ + -- nexthop-based filters on nexthop IPs only. \ + -- route-based filters on both prefix and nexthop IPs. \ + -- prefix-based filters on prefix only.[mandatory]", +) +@click.option( + "--max-next-hops", + help="Applicable only for match_mode = prefix-based. Maximum number of nexthops that will be \ + received in route updates for any of the prefixes that match FG_NHG_PREFIX for \ + this FG_NHG.[mandatory]", +) +@clicommon.pass_db +def FG_NHG_add(db, name, bucket_size, match_mode, max_next_hops): + """ Add object in FG_NHG. """ + + table = "FG_NHG" + key = name + data = {} + if bucket_size is not None: + data["bucket_size"] = bucket_size + if match_mode is not None: + data["match_mode"] = match_mode + if max_next_hops is not None: + data["max_next_hops"] = max_next_hops + + try: + add_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@FG_NHG.command(name="update") +@click.argument( + "name", + nargs=1, + required=True, +) +@click.option( + "--bucket-size", + help="total hash bucket size desired, recommended value is Lowest Common Multiple of \ + 1..max-next-hops[mandatory]", +) +@click.option( + "--match-mode", + help="The filtering method used to identify when to use Fine Grained vs regular route handling. \ + -- nexthop-based filters on nexthop IPs only. \ + -- route-based filters on both prefix and nexthop IPs. \ + -- prefix-based filters on prefix only.[mandatory]", +) +@click.option( + "--max-next-hops", + help="Applicable only for match_mode = prefix-based. Maximum number of nexthops that will be \ + received in route updates for any of the prefixes that match FG_NHG_PREFIX for this FG_NHG.[mandatory]", +) +@clicommon.pass_db +def FG_NHG_update(db, name, bucket_size, match_mode, max_next_hops): + """ Add object in FG_NHG. """ + + table = "FG_NHG" + key = name + data = {} + if bucket_size is not None: + data["bucket_size"] = bucket_size + if match_mode is not None: + data["match_mode"] = match_mode + if max_next_hops is not None: + data["max_next_hops"] = max_next_hops + + try: + update_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@FG_NHG.command(name="delete") +@click.argument( + "name", + nargs=1, + required=True, +) +@clicommon.pass_db +def FG_NHG_delete(db, name): + """ Delete object in FG_NHG. """ + + table = "FG_NHG" + key = name + try: + del_entry_validated(db.cfgdb, table, key) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@click.group(name="fg-nhg-prefix", + cls=clicommon.AliasedGroup) +def FG_NHG_PREFIX(): + """ FG_NHG_PREFIX part of config_db.json """ + + pass + + +@FG_NHG_PREFIX.command(name="add") +@click.argument( + "ip-prefix", + nargs=1, + required=True, +) +@click.option( + "--fg-nhg", + help="Fine Grained next-hop group name[mandatory]", +) +@clicommon.pass_db +def FG_NHG_PREFIX_add(db, ip_prefix, fg_nhg): + """ Add object in FG_NHG_PREFIX. """ + + table = "FG_NHG_PREFIX" + key = ip_prefix + data = {} + if fg_nhg is not None: + data["FG_NHG"] = fg_nhg + + try: + add_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@FG_NHG_PREFIX.command(name="update") +@click.argument( + "ip-prefix", + nargs=1, + required=True, +) +@click.option( + "--fg-nhg", + help="Fine Grained next-hop group name[mandatory]", +) +@clicommon.pass_db +def FG_NHG_PREFIX_update(db, ip_prefix, fg_nhg): + """ Add object in FG_NHG_PREFIX. """ + + table = "FG_NHG_PREFIX" + key = ip_prefix + data = {} + if fg_nhg is not None: + data["FG_NHG"] = fg_nhg + + try: + update_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@FG_NHG_PREFIX.command(name="delete") +@click.argument( + "ip-prefix", + nargs=1, + required=True, +) +@clicommon.pass_db +def FG_NHG_PREFIX_delete(db, ip_prefix): + """ Delete object in FG_NHG_PREFIX. """ + + table = "FG_NHG_PREFIX" + key = ip_prefix + try: + del_entry_validated(db.cfgdb, table, key) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@click.group(name="fg-nhg-member", + cls=clicommon.AliasedGroup) +def FG_NHG_MEMBER(): + """ FG_NHG_MEMBER part of config_db.json """ + + pass + + +@FG_NHG_MEMBER.command(name="add") +@click.argument( + "next-hop-ip", + nargs=1, + required=True, +) +@click.option( + "--fg-nhg", + help="Fine Grained next-hop group name[mandatory]", +) +@click.option( + "--bank", + help="An index which specifies a bank/group in which the redistribution is performed[mandatory]", +) +@click.option( + "--link", + help="Link associated with next-hop-ip, if configured, enables next-hop withdrawal/addition \ + per link's operational state changes", +) +@clicommon.pass_db +def FG_NHG_MEMBER_add(db, next_hop_ip, fg_nhg, bank, link): + """ Add object in FG_NHG_MEMBER. """ + + table = "FG_NHG_MEMBER" + key = next_hop_ip + data = {} + if fg_nhg is not None: + data["FG_NHG"] = fg_nhg + if bank is not None: + data["bank"] = bank + if link is not None: + data["link"] = link + + try: + add_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@FG_NHG_MEMBER.command(name="update") +@click.argument( + "next-hop-ip", + nargs=1, + required=True, +) +@click.option( + "--fg-nhg", + help="Fine Grained next-hop group name[mandatory]", +) +@click.option( + "--bank", + help="An index which specifies a bank/group in which the redistribution is performed[mandatory]", +) +@click.option( + "--link", + help="Link associated with next-hop-ip, if configured, enables next-hop withdrawal/addition \ + per link's operational state changes", +) +@clicommon.pass_db +def FG_NHG_MEMBER_update(db, next_hop_ip, fg_nhg, bank, link): + """ Add object in FG_NHG_MEMBER. """ + + table = "FG_NHG_MEMBER" + key = next_hop_ip + data = {} + if fg_nhg is not None: + data["FG_NHG"] = fg_nhg + if bank is not None: + data["bank"] = bank + if link is not None: + data["link"] = link + + try: + update_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@FG_NHG_MEMBER.command(name="delete") +@click.argument( + "next-hop-ip", + nargs=1, + required=True, +) +@clicommon.pass_db +def FG_NHG_MEMBER_delete(db, next_hop_ip): + """ Delete object in FG_NHG_MEMBER. """ + + table = "FG_NHG_MEMBER" + key = next_hop_ip + try: + del_entry_validated(db.cfgdb, table, key) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +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_nodes = [FG_NHG, FG_NHG_PREFIX, FG_NHG_MEMBER] + for cli_node in cli_nodes: + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command(cli_node) diff --git a/config/plugins/sonic-hash.py b/config/plugins/sonic-hash.py index 1d3c65c537..75a787722d 100644 --- a/config/plugins/sonic-hash.py +++ b/config/plugins/sonic-hash.py @@ -10,11 +10,16 @@ CFG_SWITCH_HASH, STATE_SWITCH_CAPABILITY, SW_CAP_HASH_FIELD_LIST_KEY, - SW_CAP_ECMP_HASH_KEY, - SW_CAP_LAG_HASH_KEY, + SW_CAP_ECMP_HASH_ALGORITHM_KEY, + SW_CAP_LAG_HASH_ALGORITHM_KEY, + SW_CAP_ECMP_HASH_CAPABLE_KEY, + SW_CAP_LAG_HASH_CAPABLE_KEY, + SW_CAP_ECMP_HASH_ALGORITHM_CAPABLE_KEY, + SW_CAP_LAG_HASH_ALGORITHM_CAPABLE_KEY, SW_HASH_KEY, SW_CAP_KEY, HASH_FIELD_LIST, + HASH_ALGORITHM, SYSLOG_IDENTIFIER, get_param, get_param_hint, @@ -47,6 +52,22 @@ def hash_field_validator(ctx, param, value): return list(value) +def hash_algorithm_validator(ctx, param, value): + """ + Check if hash algorithm argument is valid + Args: + ctx: click context + param: click parameter context + value: value of parameter + Returns: + str: validated parameter + """ + + click.Choice(HASH_ALGORITHM).convert(value, param, ctx) + + return value + + def ecmp_hash_validator(ctx, db, ecmp_hash): """ Check if ECMP hash argument is valid @@ -66,9 +87,9 @@ def ecmp_hash_validator(ctx, db, ecmp_hash): 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') + entry.setdefault(SW_CAP_ECMP_HASH_CAPABLE_KEY, 'false') - if entry[SW_CAP_ECMP_HASH_KEY] == 'false': + if entry[SW_CAP_ECMP_HASH_CAPABLE_KEY] == 'false': raise click.UsageError("Failed to configure {}: operation is not supported".format( get_param_hint(ctx, "ecmp_hash")), ctx ) @@ -106,9 +127,9 @@ def lag_hash_validator(ctx, db, lag_hash): 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') + entry.setdefault(SW_CAP_LAG_HASH_CAPABLE_KEY, 'false') - if entry[SW_CAP_LAG_HASH_KEY] == 'false': + if entry[SW_CAP_LAG_HASH_CAPABLE_KEY] == 'false': raise click.UsageError("Failed to configure {}: operation is not supported".format( get_param_hint(ctx, "lag_hash")), ctx ) @@ -126,6 +147,72 @@ def lag_hash_validator(ctx, db, lag_hash): for hash_field in lag_hash: click.Choice(cap_list).convert(hash_field, get_param(ctx, "lag_hash"), ctx) + +def ecmp_hash_algorithm_validator(ctx, db, ecmp_hash_algorithm): + """ + Check if ECMP hash algorithm argument is valid + + Args: + ctx: click context + db: State DB connector object + ecmp_hash_algorithm: ECMP hash algorithm + """ + + entry = db.get_all(db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, SW_CAP_KEY)) + + entry.setdefault(SW_CAP_ECMP_HASH_ALGORITHM_KEY, 'N/A') + entry.setdefault(SW_CAP_ECMP_HASH_ALGORITHM_CAPABLE_KEY, 'false') + + if entry[SW_CAP_ECMP_HASH_ALGORITHM_CAPABLE_KEY] == 'false': + raise click.UsageError("Failed to configure {}: operation is not supported".format( + get_param_hint(ctx, "ecmp_hash_algorithm")), ctx + ) + + if not entry[SW_CAP_ECMP_HASH_ALGORITHM_KEY]: + raise click.UsageError("Failed to configure {}: no hash algorithm capabilities".format( + get_param_hint(ctx, "ecmp_hash_algorithm")), ctx + ) + + if entry[SW_CAP_ECMP_HASH_ALGORITHM_KEY] == 'N/A': + return + + cap_list = entry[SW_CAP_ECMP_HASH_ALGORITHM_KEY].split(',') + + click.Choice(cap_list).convert(ecmp_hash_algorithm, get_param(ctx, "ecmp_hash_algorithm"), ctx) + + +def lag_hash_algorithm_validator(ctx, db, lag_hash_algorithm): + """ + Check if LAG hash algorithm argument is valid + + Args: + ctx: click context + db: State DB connector object + lag_hash_algorithm: LAG hash algorithm + """ + + entry = db.get_all(db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, SW_CAP_KEY)) + + entry.setdefault(SW_CAP_LAG_HASH_ALGORITHM_KEY, 'N/A') + entry.setdefault(SW_CAP_LAG_HASH_ALGORITHM_CAPABLE_KEY, 'false') + + if entry[SW_CAP_LAG_HASH_ALGORITHM_CAPABLE_KEY] == 'false': + raise click.UsageError("Failed to configure {}: operation is not supported".format( + get_param_hint(ctx, "lag_hash_algorithm")), ctx + ) + + if not entry[SW_CAP_LAG_HASH_ALGORITHM_KEY]: + raise click.UsageError("Failed to configure {}: no hash algorithm capabilities".format( + get_param_hint(ctx, "lag_hash_algorithm")), ctx + ) + + if entry[SW_CAP_LAG_HASH_ALGORITHM_KEY] == 'N/A': + return + + cap_list = entry[SW_CAP_LAG_HASH_ALGORITHM_KEY].split(',') + + click.Choice(cap_list).convert(lag_hash_algorithm, get_param(ctx, "lag_hash_algorithm"), ctx) + # # Hash DB interface --------------------------------------------------------------------------------------------------- # @@ -258,6 +345,66 @@ def SWITCH_HASH_GLOBAL_lag_hash(ctx, db, lag_hash): ctx.fail(str(err)) +@SWITCH_HASH_GLOBAL.command( + name="ecmp-hash-algorithm" +) +@click.argument( + "ecmp-hash-algorithm", + nargs=1, + required=True, + callback=hash_algorithm_validator, +) +@clicommon.pass_db +@click.pass_context +def SWITCH_HASH_GLOBAL_ecmp_hash_algorithm(ctx, db, ecmp_hash_algorithm): + """ Hash algorithm for hashing packets going through ECMP """ + + ecmp_hash_algorithm_validator(ctx, db.db, ecmp_hash_algorithm) + + table = CFG_SWITCH_HASH + key = SW_HASH_KEY + data = { + "ecmp_hash_algorithm": ecmp_hash_algorithm, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured switch global ECMP hash algorithm: {}".format(ecmp_hash_algorithm)) + except Exception as e: + log.log_error("Failed to configure switch global ECMP hash algorithm: {}".format(str(e))) + ctx.fail(str(e)) + + +@SWITCH_HASH_GLOBAL.command( + name="lag-hash-algorithm" +) +@click.argument( + "lag-hash-algorithm", + nargs=1, + required=True, + callback=hash_algorithm_validator, +) +@clicommon.pass_db +@click.pass_context +def SWITCH_HASH_GLOBAL_lag_hash_algorithm(ctx, db, lag_hash_algorithm): + """ Hash algorithm for hashing packets going through LAG """ + + lag_hash_algorithm_validator(ctx, db.db, lag_hash_algorithm) + + table = CFG_SWITCH_HASH + key = SW_HASH_KEY + data = { + "lag_hash_algorithm": lag_hash_algorithm, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured switch global LAG hash algorithm: {}".format(lag_hash_algorithm)) + except Exception as e: + log.log_error("Failed to configure switch global LAG hash algorithm: {}".format(str(e))) + ctx.fail(str(e)) + + def register(cli): """ Register new CLI nodes in root CLI. diff --git a/config/plugins/sonic-system-ldap_yang.py b/config/plugins/sonic-system-ldap_yang.py new file mode 100644 index 0000000000..cc211cdb90 --- /dev/null +++ b/config/plugins/sonic-system-ldap_yang.py @@ -0,0 +1,393 @@ +""" +Autogenerated config CLI plugin. + + +""" + +import copy +import click +import utilities_common.cli as clicommon +import utilities_common.general as general +from config import config_mgmt + +# Load sonic-cfggen from source since /usr/local/bin/sonic-cfggen does not have .py extension. +sonic_cfggen = general.load_module_from_source('sonic_cfggen', '/usr/local/bin/sonic-cfggen') + + +def exit_with_error(*args, **kwargs): + """ Print a message with click.secho and abort CLI. + + Args: + args: Positional arguments to pass to click.secho + kwargs: Keyword arguments to pass to click.secho + """ + + click.secho(*args, **kwargs) + raise click.Abort() + + +def validate_config_or_raise(cfg): + """ Validate config db data using ConfigMgmt. + + Args: + cfg (Dict): Config DB data to validate. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + try: + cfg = sonic_cfggen.FormatConverter.to_serialized(copy.deepcopy(cfg)) + config_mgmt.ConfigMgmt().loadData(cfg) + except Exception as err: + raise Exception('Failed to validate configuration: {}'.format(err)) + + +def add_entry_validated(db, table, key, data): + """ Add new entry in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + data (Dict): Entry data. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key in cfg[table]: + raise Exception(f"{key} already exists") + + cfg[table][key] = data + + validate_config_or_raise(cfg) + db.set_entry(table, key, data) + + +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 obect. + 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 Exception(f"No field/values to update {key}") + + if create_if_not_exists: + cfg[table].setdefault(key, {}) + + if key not in cfg[table]: + raise Exception(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 + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def del_entry_validated(db, table, key): + """ Delete entry in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + + cfg[table].pop(key) + + validate_config_or_raise(cfg) + db.set_entry(table, key, None) + + +@click.group(name="ldap-server", + cls=clicommon.AliasedGroup) +def LDAP_SERVER(): + """ """ + + pass + + +@LDAP_SERVER.command(name="add") +@click.argument( + "hostname", + nargs=1, + required=True, +) +@click.option( + "--priority", + help="Server priority", +) +@clicommon.pass_db +def LDAP_SERVER_add(db, hostname, priority): + """ Add object in LDAP_SERVER. """ + + table = "LDAP_SERVER" + key = hostname + data = {} + if priority is not None: + data["priority"] = priority + + try: + add_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_SERVER.command(name="update") +@click.argument( + "hostname", + nargs=1, + required=True, +) +@click.option( + "--priority", + help="Server priority", +) +@clicommon.pass_db +def LDAP_SERVER_update(db, hostname, priority): + """ Add object in LDAP_SERVER. """ + + table = "LDAP_SERVER" + key = hostname + data = {} + if priority is not None: + data["priority"] = priority + + try: + update_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_SERVER.command(name="delete") +@click.argument( + "hostname", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_SERVER_delete(db, hostname): + """ Delete object in LDAP_SERVER. """ + + table = "LDAP_SERVER" + key = hostname + try: + del_entry_validated(db.cfgdb, table, key) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@click.group(name="ldap", + cls=clicommon.AliasedGroup) +def LDAP(): + """ """ + + pass + + +@LDAP.group(name="global", cls=clicommon.AliasedGroup) +@clicommon.pass_db +def LDAP_global(db): + """ """ + + pass + + +@LDAP_global.command(name="bind-dn") +@click.argument( + "bind-dn", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_bind_dn(db, bind_dn): + """ LDAP global bind dn """ + + table = "LDAP" + key = "global" + data = { + "bind_dn": bind_dn, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_global.command(name="bind-password") +@click.argument( + "bind-password", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_bind_password(db, bind_password): + """ Shared secret used for encrypting the communication """ + + table = "LDAP" + key = "global" + data = { + "bind_password": bind_password, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_global.command(name="bind-timeout") +@click.argument( + "bind-timeout", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_bind_timeout(db, bind_timeout): + """ Ldap bind timeout """ + + table = "LDAP" + key = "global" + data = { + "bind_timeout": bind_timeout, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_global.command(name="version") +@click.argument( + "version", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_version(db, version): + """ Ldap version """ + + table = "LDAP" + key = "global" + data = { + "version": version, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_global.command(name="base-dn") +@click.argument( + "base-dn", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_base_dn(db, base_dn): + """ Ldap user base dn """ + + table = "LDAP" + key = "global" + data = { + "base_dn": base_dn, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_global.command(name="port") +@click.argument( + "port", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_port(db, port): + """ TCP port to communicate with LDAP server """ + + table = "LDAP" + key = "global" + data = { + "port": port, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +@LDAP_global.command(name="timeout") +@click.argument( + "timeout", + nargs=1, + required=True, +) +@clicommon.pass_db +def LDAP_global_timeout(db, timeout): + """ Ldap timeout duration in sec """ + + table = "LDAP" + key = "global" + data = { + "timeout": timeout, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +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 = LDAP_SERVER + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command(LDAP_SERVER) + cli_node = LDAP + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command(LDAP) diff --git a/config/plugins/sonic-trimming.py b/config/plugins/sonic-trimming.py new file mode 100644 index 0000000000..91e029653e --- /dev/null +++ b/config/plugins/sonic-trimming.py @@ -0,0 +1,228 @@ +""" +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_trimming import ( + CFG_SWITCH_TRIMMING, + STATE_SWITCH_CAPABILITY, + STATE_CAP_TRIMMING_CAPABLE_KEY, + STATE_CAP_QUEUE_MODE_KEY, + STATE_CAP_QUEUE_MODE_DYNAMIC, + STATE_CAP_QUEUE_MODE_STATIC, + CFG_TRIM_QUEUE_INDEX_DYNAMIC, + CFG_TRIM_KEY, + STATE_CAP_KEY, + UINT32_MAX, + UINT8_MAX, + SYSLOG_IDENTIFIER, + get_db, + to_str, +) + + +log = logger.Logger(SYSLOG_IDENTIFIER) +log.set_min_log_priority_info() + + +# +# Validators ---------------------------------------------------------------------------------------------------------- +# + + +class SizeTypeValidator(click.ParamType): + """ Size option validator """ + name = "integer" + + def convert(self, value, param, ctx): + click.IntRange(0, UINT32_MAX).convert(value, param, ctx) + return value + + +class DscpTypeValidator(click.ParamType): + """ Dscp option validator """ + name = "integer" + + def convert(self, value, param, ctx): + click.IntRange(0, UINT8_MAX).convert(value, param, ctx) + return value + + +class QueueTypeValidator(click.ParamType): + """ Queue index option validator """ + name = "text" + + def get_metavar(self, param): + db = get_db(click.get_current_context()) + + entry = db.get_all(db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, STATE_CAP_KEY)) + entry.setdefault(STATE_CAP_QUEUE_MODE_KEY, "N/A") + + cap_list = entry[STATE_CAP_QUEUE_MODE_KEY].split(',') + + if cap_list.count(STATE_CAP_QUEUE_MODE_DYNAMIC) == len(cap_list): + return "dynamic" + elif cap_list.count(STATE_CAP_QUEUE_MODE_STATIC) == len(cap_list): + return "INTEGER" + + return "[INTEGER|dynamic]" + + def convert(self, value, param, ctx): + db = get_db(ctx) + + entry = db.get_all(db.STATE_DB, "{}|{}".format(STATE_SWITCH_CAPABILITY, STATE_CAP_KEY)) + + entry.setdefault(STATE_CAP_TRIMMING_CAPABLE_KEY, "false") + entry.setdefault(STATE_CAP_QUEUE_MODE_KEY, "N/A") + + if entry[STATE_CAP_TRIMMING_CAPABLE_KEY] == "false": + raise click.UsageError("Failed to configure {}: operation is not supported".format( + param.get_error_hint(ctx)), ctx + ) + + if not entry[STATE_CAP_QUEUE_MODE_KEY]: + raise click.UsageError("Failed to configure {}: no queue resolution mode capabilities".format( + param.get_error_hint(ctx)), ctx + ) + + verify_cap = True + + if entry[STATE_CAP_QUEUE_MODE_KEY] == "N/A": + verify_cap = False + + cap_list = entry[STATE_CAP_QUEUE_MODE_KEY].split(',') + + if value == CFG_TRIM_QUEUE_INDEX_DYNAMIC: + if verify_cap and (STATE_CAP_QUEUE_MODE_DYNAMIC not in cap_list): + self.fail("dynamic queue resolution mode is not supported", param, ctx) + else: + if verify_cap and (STATE_CAP_QUEUE_MODE_STATIC not in cap_list): + self.fail("static queue resolution mode is not supported", param, ctx) + + click.IntRange(0, UINT8_MAX).convert(value, param, ctx) + + return value + + +# +# 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 + if value is not None: + cfg[table][key][attr] = value + entry_changed = True + + if not entry_changed: + return + + db.set_entry(table, key, cfg[table][key]) + + +# +# CLI ----------------------------------------------------------------------------------------------------------------- +# + + +@click.group( + name="switch-trimming", + cls=clicommon.AliasedGroup +) +def SWITCH_TRIMMING(): + """ Configure switch trimming feature """ + + pass + + +@SWITCH_TRIMMING.command( + name="global" +) +@click.option( + "-s", "--size", "size", + help="Configures size (in bytes) to trim eligible packet", + type=SizeTypeValidator(), +) +@click.option( + "-d", "--dscp", "dscp", + help="Configures DSCP value assigned to a packet after trimming", + type=DscpTypeValidator(), +) +@click.option( + "-q", "--queue", "queue", + help="Configures queue index to use for transmission of a packet after trimming", + type=QueueTypeValidator(), +) +@clicommon.pass_db +@click.pass_context +def SWITCH_TRIMMING_GLOBAL(ctx, db, size, dscp, queue): + """ Configure switch trimming global """ + + if not (size or dscp or queue): + raise click.UsageError("Failed to configure switch trimming global: no options are provided", ctx) + + table = CFG_SWITCH_TRIMMING + key = CFG_TRIM_KEY + + data = { + "size": size, + "dscp_value": dscp, + "queue_index": queue, + } + + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + log.log_notice("Configured switch trimming global: {}".format(to_str(data))) + except Exception as e: + log.log_error("Failed to configure switch trimming global: {}".format(str(e))) + ctx.fail(str(e)) + + +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_TRIMMING + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command(SWITCH_TRIMMING) diff --git a/config/stp.py b/config/stp.py new file mode 100644 index 0000000000..85d7041847 --- /dev/null +++ b/config/stp.py @@ -0,0 +1,917 @@ + +# +# 'spanning-tree' group ('config spanning-tree ...') +# + +import click +import utilities_common.cli as clicommon +from natsort import natsorted +import logging + +STP_MIN_ROOT_GUARD_TIMEOUT = 5 +STP_MAX_ROOT_GUARD_TIMEOUT = 600 +STP_DEFAULT_ROOT_GUARD_TIMEOUT = 30 + +STP_MIN_FORWARD_DELAY = 4 +STP_MAX_FORWARD_DELAY = 30 +STP_DEFAULT_FORWARD_DELAY = 15 + +STP_MIN_HELLO_INTERVAL = 1 +STP_MAX_HELLO_INTERVAL = 10 +STP_DEFAULT_HELLO_INTERVAL = 2 + +STP_MIN_MAX_AGE = 6 +STP_MAX_MAX_AGE = 40 +STP_DEFAULT_MAX_AGE = 20 + +STP_MIN_BRIDGE_PRIORITY = 0 +STP_MAX_BRIDGE_PRIORITY = 61440 +STP_DEFAULT_BRIDGE_PRIORITY = 32768 + +PVST_MAX_INSTANCES = 255 + + +def get_intf_list_in_vlan_member_table(config_db): + """ + Get info from REDIS ConfigDB and create interface to vlan mapping + """ + get_int_vlan_configdb_info = config_db.get_table('VLAN_MEMBER') + int_list = [] + for key in get_int_vlan_configdb_info: + interface = key[1] + if interface not in int_list: + int_list.append(interface) + return int_list + +################################## +# STP parameter validations +################################## + + +def is_valid_root_guard_timeout(ctx, root_guard_timeout): + if root_guard_timeout not in range(STP_MIN_ROOT_GUARD_TIMEOUT, STP_MAX_ROOT_GUARD_TIMEOUT + 1): + ctx.fail("STP root guard timeout must be in range 5-600") + + +def is_valid_forward_delay(ctx, forward_delay): + if forward_delay not in range(STP_MIN_FORWARD_DELAY, STP_MAX_FORWARD_DELAY + 1): + ctx.fail("STP forward delay value must be in range 4-30") + + +def is_valid_hello_interval(ctx, hello_interval): + if hello_interval not in range(STP_MIN_HELLO_INTERVAL, STP_MAX_HELLO_INTERVAL + 1): + ctx.fail("STP hello timer must be in range 1-10") + + +def is_valid_max_age(ctx, max_age): + if max_age not in range(STP_MIN_MAX_AGE, STP_MAX_MAX_AGE + 1): + ctx.fail("STP max age value must be in range 6-40") + + +def is_valid_bridge_priority(ctx, priority): + if priority % 4096 != 0: + ctx.fail("STP bridge priority must be multiple of 4096") + if priority not in range(STP_MIN_BRIDGE_PRIORITY, STP_MAX_BRIDGE_PRIORITY + 1): + ctx.fail("STP bridge priority must be in range 0-61440") + + +def validate_params(forward_delay, max_age, hello_time): + if (2 * (int(forward_delay) - 1)) >= int(max_age) >= (2 * (int(hello_time) + 1)): + return True + else: + return False + + +def is_valid_stp_vlan_parameters(ctx, db, vlan_name, param_type, new_value): + stp_vlan_entry = db.get_entry('STP_VLAN', vlan_name) + cfg_vlan_forward_delay = stp_vlan_entry.get("forward_delay") + cfg_vlan_max_age = stp_vlan_entry.get("max_age") + cfg_vlan_hello_time = stp_vlan_entry.get("hello_time") + ret_val = False + if param_type == "forward_delay": + ret_val = validate_params(new_value, cfg_vlan_max_age, cfg_vlan_hello_time) + elif param_type == "max_age": + ret_val = validate_params(cfg_vlan_forward_delay, new_value, cfg_vlan_hello_time) + elif param_type == "hello_time": + ret_val = validate_params(cfg_vlan_forward_delay, cfg_vlan_max_age, new_value) + + if ret_val is not True: + ctx.fail("2*(forward_delay-1) >= max_age >= 2*(hello_time +1 ) not met for VLAN") + + +def is_valid_stp_global_parameters(ctx, db, param_type, new_value): + stp_global_entry = db.get_entry('STP', "GLOBAL") + cfg_forward_delay = stp_global_entry.get("forward_delay") + cfg_max_age = stp_global_entry.get("max_age") + cfg_hello_time = stp_global_entry.get("hello_time") + ret_val = False + if param_type == "forward_delay": + ret_val = validate_params(new_value, cfg_max_age, cfg_hello_time) + elif param_type == "max_age": + ret_val = validate_params(cfg_forward_delay, new_value, cfg_hello_time) + elif param_type == "hello_time": + ret_val = validate_params(cfg_forward_delay, cfg_max_age, new_value) + + if ret_val is not True: + ctx.fail("2*(forward_delay-1) >= max_age >= 2*(hello_time +1 ) not met") + + +def get_max_stp_instances(): + return PVST_MAX_INSTANCES + + +def update_stp_vlan_parameter(ctx, db, param_type, new_value): + stp_global_entry = db.get_entry('STP', "GLOBAL") + + allowed_params = {"priority", "max_age", "hello_time", "forward_delay"} + if param_type not in allowed_params: + ctx.fail("Invalid parameter") + + current_global_value = stp_global_entry.get("forward_delay") + + vlan_dict = db.get_table('STP_VLAN') + for vlan in vlan_dict.keys(): + vlan_entry = db.get_entry('STP_VLAN', vlan) + current_vlan_value = vlan_entry.get(param_type) + if current_global_value == current_vlan_value: + db.mod_entry('STP_VLAN', vlan, {param_type: new_value}) + + +def check_if_vlan_exist_in_db(db, ctx, vid): + vlan_name = 'Vlan{}'.format(vid) + vlan = db.get_entry('VLAN', vlan_name) + if len(vlan) == 0: + ctx.fail("{} doesn't exist".format(vlan_name)) + + +def enable_stp_for_vlans(db): + vlan_count = 0 + fvs = {'enabled': 'true', + 'forward_delay': get_global_stp_forward_delay(db), + 'hello_time': get_global_stp_hello_time(db), + 'max_age': get_global_stp_max_age(db), + 'priority': get_global_stp_priority(db) + } + vlan_dict = natsorted(db.get_table('VLAN')) + max_stp_instances = get_max_stp_instances() + for vlan_key in vlan_dict: + if vlan_count >= max_stp_instances: + logging.warning("Exceeded maximum STP configurable VLAN instances for {}".format(vlan_key)) + break + db.set_entry('STP_VLAN', vlan_key, fvs) + vlan_count += 1 + + +def get_stp_enabled_vlan_count(db): + count = 0 + stp_vlan_keys = db.get_table('STP_VLAN').keys() + for key in stp_vlan_keys: + if db.get_entry('STP_VLAN', key).get('enabled') == 'true': + count += 1 + return count + + +def vlan_enable_stp(db, vlan_name): + fvs = {'enabled': 'true', + 'forward_delay': get_global_stp_forward_delay(db), + 'hello_time': get_global_stp_hello_time(db), + 'max_age': get_global_stp_max_age(db), + 'priority': get_global_stp_priority(db) + } + if is_global_stp_enabled(db): + if get_stp_enabled_vlan_count(db) < get_max_stp_instances(): + db.set_entry('STP_VLAN', vlan_name, fvs) + else: + logging.warning("Exceeded maximum STP configurable VLAN instances for {}".format(vlan_name)) + + +def interface_enable_stp(db, interface_name): + fvs = {'enabled': 'true', + 'root_guard': 'false', + 'bpdu_guard': 'false', + 'bpdu_guard_do_disable': 'false', + 'portfast': 'false', + 'uplink_fast': 'false' + } + if is_global_stp_enabled(db): + db.set_entry('STP_PORT', interface_name, fvs) + + +def is_vlan_configured_interface(db, interface_name): + intf_to_vlan_list = get_vlan_list_for_interface(db, interface_name) + if intf_to_vlan_list: # if empty + return True + else: + return False + + +def is_interface_vlan_member(db, vlan_name, interface_name): + ctx = click.get_current_context() + key = vlan_name + '|' + interface_name + entry = db.get_entry('VLAN_MEMBER', key) + if len(entry) == 0: # if empty + ctx.fail("{} is not member of {}".format(interface_name, vlan_name)) + + +def get_vlan_list_for_interface(db, interface_name): + vlan_intf_info = db.get_table('VLAN_MEMBER') + vlan_list = [] + for line in vlan_intf_info: + if interface_name == line[1]: + vlan_name = line[0] + vlan_list.append(vlan_name) + return vlan_list + + +def get_pc_member_port_list(db): + pc_member_info = db.get_table('PORTCHANNEL_MEMBER') + pc_member_port_list = [] + for line in pc_member_info: + intf_name = line[1] + pc_member_port_list.append(intf_name) + return pc_member_port_list + + +def get_vlan_list_from_stp_vlan_intf_table(db, intf_name): + stp_vlan_intf_info = db.get_table('STP_VLAN_PORT') + vlan_list = [] + for line in stp_vlan_intf_info: + if line[1] == intf_name: + vlan_list.append(line[0]) + return vlan_list + + +def get_intf_list_from_stp_vlan_intf_table(db, vlan_name): + stp_vlan_intf_info = db.get_table('STP_VLAN_PORT') + intf_list = [] + for line in stp_vlan_intf_info: + if line[0] == vlan_name: + intf_list.append(line[1]) + return intf_list + + +def is_portchannel_member_port(db, interface_name): + return interface_name in get_pc_member_port_list(db) + + +def enable_stp_for_interfaces(db): + fvs = {'enabled': 'true', + 'root_guard': 'false', + 'bpdu_guard': 'false', + 'bpdu_guard_do_disable': 'false', + 'portfast': 'false', + 'uplink_fast': 'false' + } + port_dict = natsorted(db.get_table('PORT')) + intf_list_in_vlan_member_table = get_intf_list_in_vlan_member_table(db) + + for port_key in port_dict: + if port_key in intf_list_in_vlan_member_table: + db.set_entry('STP_PORT', port_key, fvs) + + po_ch_dict = natsorted(db.get_table('PORTCHANNEL')) + for po_ch_key in po_ch_dict: + if po_ch_key in intf_list_in_vlan_member_table: + db.set_entry('STP_PORT', po_ch_key, fvs) + + +def is_global_stp_enabled(db): + stp_entry = db.get_entry('STP', "GLOBAL") + mode = stp_entry.get("mode") + if mode: + return True + else: + return False + + +def check_if_global_stp_enabled(db, ctx): + if not is_global_stp_enabled(db): + ctx.fail("Global STP is not enabled - first configure STP mode") + + +def get_global_stp_mode(db): + stp_entry = db.get_entry('STP', "GLOBAL") + mode = stp_entry.get("mode") + return mode + + +def get_global_stp_forward_delay(db): + stp_entry = db.get_entry('STP', "GLOBAL") + forward_delay = stp_entry.get("forward_delay") + return forward_delay + + +def get_global_stp_hello_time(db): + stp_entry = db.get_entry('STP', "GLOBAL") + hello_time = stp_entry.get("hello_time") + return hello_time + + +def get_global_stp_max_age(db): + stp_entry = db.get_entry('STP', "GLOBAL") + max_age = stp_entry.get("max_age") + return max_age + + +def get_global_stp_priority(db): + stp_entry = db.get_entry('STP', "GLOBAL") + priority = stp_entry.get("priority") + return priority + + +@click.group() +@clicommon.pass_db +def spanning_tree(_db): + """STP command line""" + pass + + +############################################### +# STP Global commands implementation +############################################### + +# cmd: STP enable +@spanning_tree.command('enable') +@click.argument('mode', metavar='', required=True, type=click.Choice(["pvst"])) +@clicommon.pass_db +def spanning_tree_enable(_db, mode): + """enable STP """ + ctx = click.get_current_context() + db = _db.cfgdb + if mode == "pvst" and get_global_stp_mode(db) == "pvst": + ctx.fail("PVST is already configured") + fvs = {'mode': mode, + 'rootguard_timeout': STP_DEFAULT_ROOT_GUARD_TIMEOUT, + 'forward_delay': STP_DEFAULT_FORWARD_DELAY, + 'hello_time': STP_DEFAULT_HELLO_INTERVAL, + 'max_age': STP_DEFAULT_MAX_AGE, + 'priority': STP_DEFAULT_BRIDGE_PRIORITY + } + db.set_entry('STP', "GLOBAL", fvs) + # Enable STP for VLAN by default + enable_stp_for_interfaces(db) + enable_stp_for_vlans(db) + + +# cmd: STP disable +@spanning_tree.command('disable') +@click.argument('mode', metavar='', required=True, type=click.Choice(["pvst"])) +@clicommon.pass_db +def stp_disable(_db, mode): + """disable STP """ + db = _db.cfgdb + db.set_entry('STP', "GLOBAL", None) + # Disable STP for all VLANs and interfaces + db.delete_table('STP_VLAN') + db.delete_table('STP_PORT') + db.delete_table('STP_VLAN_PORT') + if get_global_stp_mode(db) == "pvst": + print("Error PVST disable failed") + + +# cmd: STP global root guard timeout +@spanning_tree.command('root_guard_timeout') +@click.argument('root_guard_timeout', metavar='<5-600 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_global_root_guard_timeout(_db, root_guard_timeout): + """Configure STP global root guard timeout value""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + is_valid_root_guard_timeout(ctx, root_guard_timeout) + db.mod_entry('STP', "GLOBAL", {'rootguard_timeout': root_guard_timeout}) + + +# cmd: STP global forward delay +@spanning_tree.command('forward_delay') +@click.argument('forward_delay', metavar='<4-30 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_global_forward_delay(_db, forward_delay): + """Configure STP global forward delay""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + is_valid_forward_delay(ctx, forward_delay) + is_valid_stp_global_parameters(ctx, db, "forward_delay", forward_delay) + update_stp_vlan_parameter(ctx, db, "forward_delay", forward_delay) + db.mod_entry('STP', "GLOBAL", {'forward_delay': forward_delay}) + + +# cmd: STP global hello interval +@spanning_tree.command('hello') +@click.argument('hello_interval', metavar='<1-10 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_global_hello_interval(_db, hello_interval): + """Configure STP global hello interval""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + is_valid_hello_interval(ctx, hello_interval) + is_valid_stp_global_parameters(ctx, db, "hello_time", hello_interval) + update_stp_vlan_parameter(ctx, db, "hello_time", hello_interval) + db.mod_entry('STP', "GLOBAL", {'hello_time': hello_interval}) + + +# cmd: STP global max age +@spanning_tree.command('max_age') +@click.argument('max_age', metavar='<6-40 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_global_max_age(_db, max_age): + """Configure STP global max_age""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + is_valid_max_age(ctx, max_age) + is_valid_stp_global_parameters(ctx, db, "max_age", max_age) + update_stp_vlan_parameter(ctx, db, "max_age", max_age) + db.mod_entry('STP', "GLOBAL", {'max_age': max_age}) + + +# cmd: STP global bridge priority +@spanning_tree.command('priority') +@click.argument('priority', metavar='<0-61440>', required=True, type=int) +@clicommon.pass_db +def stp_global_priority(_db, priority): + """Configure STP global bridge priority""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + is_valid_bridge_priority(ctx, priority) + update_stp_vlan_parameter(ctx, db, "priority", priority) + db.mod_entry('STP', "GLOBAL", {'priority': priority}) + + +############################################### +# STP VLAN commands implementation +############################################### +@spanning_tree.group('vlan') +@clicommon.pass_db +def spanning_tree_vlan(_db): + """Configure STP for a VLAN""" + pass + + +def is_stp_enabled_for_vlan(db, vlan_name): + stp_entry = db.get_entry('STP_VLAN', vlan_name) + stp_enabled = stp_entry.get("enabled") + if stp_enabled == "true": + return True + else: + return False + + +def check_if_stp_enabled_for_vlan(ctx, db, vlan_name): + if not is_stp_enabled_for_vlan(db, vlan_name): + ctx.fail("STP is not enabled for VLAN") + + +@spanning_tree_vlan.command('enable') +@click.argument('vid', metavar='', required=True, type=int) +@clicommon.pass_db +def stp_vlan_enable(_db, vid): + """Enable STP for a VLAN""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + if is_stp_enabled_for_vlan(db, vlan_name): + ctx.fail("STP is already enabled for " + vlan_name) + if get_stp_enabled_vlan_count(db) >= get_max_stp_instances(): + ctx.fail("Exceeded maximum STP configurable VLAN instances") + check_if_global_stp_enabled(db, ctx) + # when enabled for first time, create VLAN entry with + # global values - else update only VLAN STP state + stp_vlan_entry = db.get_entry('STP_VLAN', vlan_name) + if len(stp_vlan_entry) == 0: + fvs = {'enabled': 'true', + 'forward_delay': get_global_stp_forward_delay(db), + 'hello_time': get_global_stp_hello_time(db), + 'max_age': get_global_stp_max_age(db), + 'priority': get_global_stp_priority(db) + } + db.set_entry('STP_VLAN', vlan_name, fvs) + else: + db.mod_entry('STP_VLAN', vlan_name, {'enabled': 'true'}) + # Refresh stp_vlan_intf entry for vlan + for vlan, intf in db.get_table('STP_VLAN_PORT'): + if vlan == vlan_name: + vlan_intf_key = "{}|{}".format(vlan_name, intf) + vlan_intf_entry = db.get_entry('STP_VLAN_PORT', vlan_intf_key) + db.mod_entry('STP_VLAN_PORT', vlan_intf_key, vlan_intf_entry) + + +@spanning_tree_vlan.command('disable') +@click.argument('vid', metavar='', required=True, type=int) +@clicommon.pass_db +def stp_vlan_disable(_db, vid): + """Disable STP for a VLAN""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + db.mod_entry('STP_VLAN', vlan_name, {'enabled': 'false'}) + + +@spanning_tree_vlan.command('forward_delay') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('forward_delay', metavar='<4-30 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_vlan_forward_delay(_db, vid, forward_delay): + """Configure STP forward delay for VLAN""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_forward_delay(ctx, forward_delay) + is_valid_stp_vlan_parameters(ctx, db, vlan_name, "forward_delay", forward_delay) + db.mod_entry('STP_VLAN', vlan_name, {'forward_delay': forward_delay}) + + +@spanning_tree_vlan.command('hello') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('hello_interval', metavar='<1-10 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_vlan_hello_interval(_db, vid, hello_interval): + """Configure STP hello interval for VLAN""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_hello_interval(ctx, hello_interval) + is_valid_stp_vlan_parameters(ctx, db, vlan_name, "hello_time", hello_interval) + db.mod_entry('STP_VLAN', vlan_name, {'hello_time': hello_interval}) + + +@spanning_tree_vlan.command('max_age') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('max_age', metavar='<6-40 seconds>', required=True, type=int) +@clicommon.pass_db +def stp_vlan_max_age(_db, vid, max_age): + """Configure STP max age for VLAN""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_max_age(ctx, max_age) + is_valid_stp_vlan_parameters(ctx, db, vlan_name, "max_age", max_age) + db.mod_entry('STP_VLAN', vlan_name, {'max_age': max_age}) + + +@spanning_tree_vlan.command('priority') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('priority', metavar='<0-61440>', required=True, type=int) +@clicommon.pass_db +def stp_vlan_priority(_db, vid, priority): + """Configure STP bridge priority for VLAN""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_vlan_exist_in_db(db, ctx, vid) + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + is_valid_bridge_priority(ctx, priority) + db.mod_entry('STP_VLAN', vlan_name, {'priority': priority}) + + +############################################### +# STP interface commands implementation +############################################### + + +def is_stp_enabled_for_interface(db, intf_name): + stp_entry = db.get_entry('STP_PORT', intf_name) + stp_enabled = stp_entry.get("enabled") + if stp_enabled == "true": + return True + else: + return False + + +def check_if_stp_enabled_for_interface(ctx, db, intf_name): + if not is_stp_enabled_for_interface(db, intf_name): + ctx.fail("STP is not enabled for interface {}".format(intf_name)) + + +def check_if_interface_is_valid(ctx, db, interface_name): + from config.main import interface_name_is_valid + if interface_name_is_valid(db, interface_name) is False: + ctx.fail("Interface name is invalid. Please enter a valid interface name!!") + for key in db.get_table('INTERFACE'): + if type(key) != tuple: + continue + if key[0] == interface_name: + ctx.fail(" {} has ip address {} configured - It's not a L2 interface".format(interface_name, key[1])) + if is_portchannel_member_port(db, interface_name): + ctx.fail(" {} is a portchannel member port - STP can't be configured".format(interface_name)) + if not is_vlan_configured_interface(db, interface_name): + ctx.fail(" {} has no VLAN configured - It's not a L2 interface".format(interface_name)) + + +@spanning_tree.group('interface') +@clicommon.pass_db +def spanning_tree_interface(_db): + """Configure STP for interface""" + pass + + +@spanning_tree_interface.command('enable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_enable(_db, interface_name): + """Enable STP for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + if is_stp_enabled_for_interface(db, interface_name): + ctx.fail("STP is already enabled for " + interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + stp_intf_entry = db.get_entry('STP_PORT', interface_name) + if len(stp_intf_entry) == 0: + fvs = {'enabled': 'true', + 'root_guard': 'false', + 'bpdu_guard': 'false', + 'bpdu_guard_do_disable': 'false', + 'portfast': 'false', + 'uplink_fast': 'false'} + db.set_entry('STP_PORT', interface_name, fvs) + else: + db.mod_entry('STP_PORT', interface_name, {'enabled': 'true'}) + + +@spanning_tree_interface.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_disable(_db, interface_name): + """Disable STP for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_global_stp_enabled(db, ctx) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'enabled': 'false'}) + + +# STP interface port priority +STP_INTERFACE_MIN_PRIORITY = 0 +STP_INTERFACE_MAX_PRIORITY = 240 +STP_INTERFACE_DEFAULT_PRIORITY = 128 + + +def is_valid_interface_priority(ctx, intf_priority): + if intf_priority not in range(STP_INTERFACE_MIN_PRIORITY, STP_INTERFACE_MAX_PRIORITY + 1): + ctx.fail("STP interface priority must be in range 0-240") + + +@spanning_tree_interface.command('priority') +@click.argument('interface_name', metavar='', required=True) +@click.argument('priority', metavar='<0-240>', required=True, type=int) +@clicommon.pass_db +def stp_interface_priority(_db, interface_name, priority): + """Configure STP port priority for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + is_valid_interface_priority(ctx, priority) + curr_intf_proirty = db.get_entry('STP_PORT', interface_name).get('priority') + db.mod_entry('STP_PORT', interface_name, {'priority': priority}) + # update interface priority in all stp_vlan_intf entries if entry exists + for vlan, intf in db.get_table('STP_VLAN_PORT'): + if intf == interface_name: + vlan_intf_key = "{}|{}".format(vlan, interface_name) + vlan_intf_entry = db.get_entry('STP_VLAN_PORT', vlan_intf_key) + if len(vlan_intf_entry) != 0: + vlan_intf_priority = vlan_intf_entry.get('priority') + if curr_intf_proirty == vlan_intf_priority: + db.mod_entry('STP_VLAN_PORT', vlan_intf_key, {'priority': priority}) + # end + + +# STP interface port path cost +STP_INTERFACE_MIN_PATH_COST = 1 +STP_INTERFACE_MAX_PATH_COST = 200000000 + + +def is_valid_interface_path_cost(ctx, intf_path_cost): + if intf_path_cost < STP_INTERFACE_MIN_PATH_COST or intf_path_cost > STP_INTERFACE_MAX_PATH_COST: + ctx.fail("STP interface path cost must be in range 1-200000000") + + +@spanning_tree_interface.command('cost') +@click.argument('interface_name', metavar='', required=True) +@click.argument('cost', metavar='<1-200000000>', required=True, type=int) +@clicommon.pass_db +def stp_interface_path_cost(_db, interface_name, cost): + """Configure STP path cost for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + is_valid_interface_path_cost(ctx, cost) + curr_intf_cost = db.get_entry('STP_PORT', interface_name).get('path_cost') + db.mod_entry('STP_PORT', interface_name, {'path_cost': cost}) + # update interface path_cost in all stp_vlan_intf entries if entry exists + for vlan, intf in db.get_table('STP_VLAN_PORT'): + if intf == interface_name: + vlan_intf_key = "{}|{}".format(vlan, interface_name) + vlan_intf_entry = db.get_entry('STP_VLAN_PORT', vlan_intf_key) + if len(vlan_intf_entry) != 0: + vlan_intf_cost = vlan_intf_entry.get('path_cost') + if curr_intf_cost == vlan_intf_cost: + db.mod_entry('STP_VLAN_PORT', vlan_intf_key, {'path_cost': cost}) + # end + + +# STP interface root guard +@spanning_tree_interface.group('root_guard') +@clicommon.pass_db +def spanning_tree_interface_root_guard(_db): + """Configure STP root guard for interface""" + pass + + +@spanning_tree_interface_root_guard.command('enable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_root_guard_enable(_db, interface_name): + """Enable STP root guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'root_guard': 'true'}) + + +@spanning_tree_interface_root_guard.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_root_guard_disable(_db, interface_name): + """Disable STP root guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'root_guard': 'false'}) + + +# STP interface bpdu guard +@spanning_tree_interface.group('bpdu_guard') +@clicommon.pass_db +def spanning_tree_interface_bpdu_guard(_db): + """Configure STP bpdu guard for interface""" + pass + + +@spanning_tree_interface_bpdu_guard.command('enable') +@click.argument('interface_name', metavar='', required=True) +@click.option('-s', '--shutdown', is_flag=True) +@clicommon.pass_db +def stp_interface_bpdu_guard_enable(_db, interface_name, shutdown): + """Enable STP bpdu guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + if shutdown is True: + bpdu_guard_do_disable = 'true' + else: + bpdu_guard_do_disable = 'false' + fvs = {'bpdu_guard': 'true', + 'bpdu_guard_do_disable': bpdu_guard_do_disable} + db.mod_entry('STP_PORT', interface_name, fvs) + + +@spanning_tree_interface_bpdu_guard.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_bpdu_guard_disable(_db, interface_name): + """Disable STP bpdu guard for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'bpdu_guard': 'false'}) + + +# STP interface portfast +@spanning_tree_interface.group('portfast') +@clicommon.pass_db +def spanning_tree_interface_portfast(_db): + """Configure STP portfast for interface""" + pass + + +@spanning_tree_interface_portfast.command('enable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_portfast_enable(_db, interface_name): + """Enable STP portfast for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'portfast': 'true'}) + + +@spanning_tree_interface_portfast.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_portfast_disable(_db, interface_name): + """Disable STP portfast for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'portfast': 'false'}) + + +# STP interface root uplink_fast +@spanning_tree_interface.group('uplink_fast') +@clicommon.pass_db +def spanning_tree_interface_uplink_fast(_db): + """Configure STP uplink fast for interface""" + pass + + +@spanning_tree_interface_uplink_fast.command('enable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_uplink_fast_enable(_db, interface_name): + """Enable STP uplink fast for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'uplink_fast': 'true'}) + + +@spanning_tree_interface_uplink_fast.command('disable') +@click.argument('interface_name', metavar='', required=True) +@clicommon.pass_db +def stp_interface_uplink_fast_disable(_db, interface_name): + """Disable STP uplink fast for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_interface_is_valid(ctx, db, interface_name) + db.mod_entry('STP_PORT', interface_name, {'uplink_fast': 'false'}) + + +############################################### +# STP interface per VLAN commands implementation +############################################### +@spanning_tree_vlan.group('interface') +@clicommon.pass_db +def spanning_tree_vlan_interface(_db): + """Configure STP parameters for interface per VLAN""" + pass + + +# STP interface per vlan port priority +def is_valid_vlan_interface_priority(ctx, priority): + if priority not in range(STP_INTERFACE_MIN_PRIORITY, STP_INTERFACE_MAX_PRIORITY + 1): + ctx.fail("STP per vlan port priority must be in range 0-240") + + +@spanning_tree_vlan_interface.command('priority') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('interface_name', metavar='', required=True) +@click.argument('priority', metavar='<0-240>', required=True, type=int) +@clicommon.pass_db +def stp_vlan_interface_priority(_db, vid, interface_name, priority): + """Configure STP per vlan port priority for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_vlan_exist_in_db(db, ctx, vid) + is_interface_vlan_member(db, vlan_name, interface_name) + is_valid_vlan_interface_priority(ctx, priority) + vlan_interface = str(vlan_name) + "|" + interface_name + db.mod_entry('STP_VLAN_PORT', vlan_interface, {'priority': priority}) + + +@spanning_tree_vlan_interface.command('cost') +@click.argument('vid', metavar='', required=True, type=int) +@click.argument('interface_name', metavar='', required=True) +@click.argument('cost', metavar='<1-200000000>', required=True, type=int) +@clicommon.pass_db +def stp_vlan_interface_cost(_db, vid, interface_name, cost): + """Configure STP per vlan path cost for interface""" + ctx = click.get_current_context() + db = _db.cfgdb + vlan_name = 'Vlan{}'.format(vid) + check_if_stp_enabled_for_vlan(ctx, db, vlan_name) + check_if_stp_enabled_for_interface(ctx, db, interface_name) + check_if_vlan_exist_in_db(db, ctx, vid) + is_interface_vlan_member(db, vlan_name, interface_name) + is_valid_interface_path_cost(ctx, cost) + vlan_interface = str(vlan_name) + "|" + interface_name + db.mod_entry('STP_VLAN_PORT', vlan_interface, {'path_cost': cost}) + + +# Invoke main() +# if __name__ == '__main__': +# spanning_tree() diff --git a/config/switchport.py b/config/switchport.py new file mode 100644 index 0000000000..ca94a08444 --- /dev/null +++ b/config/switchport.py @@ -0,0 +1,135 @@ +import click +from .utils import log +import utilities_common.cli as clicommon + +# +# 'switchport' mode ('config switchport ...') +# + + +@click.group(cls=clicommon.AbbreviationGroup, name='switchport', invoke_without_command=False) +def switchport(): + """Switchport mode configuration tasks""" + pass + + +@switchport.command("mode") +@click.argument("type", metavar="", required=True, type=click.Choice(["access", "trunk", "routed"])) +@click.argument("port", metavar="port", required=True) +@clicommon.pass_db +def switchport_mode(db, type, port): + """switchport mode help commands.Mode_type can be access or trunk or routed""" + + ctx = click.get_current_context() + + log.log_info("'switchport mode {} {}' executing...".format(type, port)) + mode_exists_status = True + + # checking if port name with alias exists + if clicommon.get_interface_naming_mode() == "alias": + alias = port + iface_alias_converter = clicommon.InterfaceAliasConverter(db) + port = iface_alias_converter.alias_to_name(alias) + + if clicommon.is_port_mirror_dst_port(db.cfgdb, port): + ctx.fail("{} is configured as mirror destination port".format(port)) + + if clicommon.is_valid_port(db.cfgdb, port): + is_port = True + elif clicommon.is_valid_portchannel(db.cfgdb, port): + is_port = False + else: + ctx.fail("{} does not exist".format(port)) + + portchannel_member_table = db.cfgdb.get_table('PORTCHANNEL_MEMBER') + + if (is_port and clicommon.interface_is_in_portchannel(portchannel_member_table, port)): + ctx.fail("{} is part of portchannel!".format(port)) + + if is_port: + port_data = db.cfgdb.get_entry('PORT', port) + else: + port_data = db.cfgdb.get_entry('PORTCHANNEL', port) + + # mode type is either access or trunk + if type != "routed": + + if "mode" in port_data: + existing_mode = port_data["mode"] + else: + mode_exists_status = False + + if (is_port and clicommon.is_port_router_interface(db.cfgdb, port)) or \ + (not is_port and clicommon.is_pc_router_interface(db.cfgdb, port)): + ctx.fail("Remove IP from {} to change mode!".format(port)) + + if not mode_exists_status: + port_data["mode"] = type + if is_port: + db.cfgdb.set_entry("PORT", port, port_data) + # if not port then is a port channel + elif not is_port: + db.cfgdb.set_entry("PORTCHANNEL", port, port_data) + + if mode_exists_status: + if existing_mode == "routed": + # if the port in an interface + if is_port: + db.cfgdb.mod_entry("PORT", port, {"mode": "{}".format(type)}) + # if not port then is a port channel + elif not is_port: + db.cfgdb.mod_entry("PORTCHANNEL", port, {"mode": "{}".format(type)}) + + if existing_mode == type: + ctx.fail("{} is already in {} mode".format(port, type)) + else: + if existing_mode == "access" and type == "trunk": + pass + if existing_mode == "trunk" and type == "access": + if clicommon.interface_is_tagged_member(db.cfgdb, port): + ctx.fail( + "{} is in {} mode and have tagged member(s).\nRemove " + "tagged member(s) from {} to switch to {} mode".format(port, existing_mode, port, type)) + if is_port: + db.cfgdb.mod_entry("PORT", port, {"mode": "{}".format(type)}) + # if not port then is a port channel + elif not is_port: + db.cfgdb.mod_entry("PORTCHANNEL", port, {"mode": "{}".format(type)}) + + click.echo("{} switched to {} mode".format(port, type)) + + # if mode type is routed + else: + + if clicommon.interface_is_tagged_member(db.cfgdb, port): + ctx.fail("{} has tagged member(s). \nRemove them to change mode to {}".format(port, type)) + + if clicommon.interface_is_untagged_member(db.cfgdb, port): + ctx.fail("{} has untagged member. \nRemove it to change mode to {}".format(port, type)) + + if "mode" in port_data: + existing_mode = port_data["mode"] + else: + mode_exists_status = False + + if not mode_exists_status: + port_data["mode"] = type + if is_port: + db.cfgdb.set_entry("PORT", port, port_data) + + # if not port then is a port channel + elif not is_port: + db.cfgdb.set_entry("PORTCHANNEL", port, port_data) + pass + + if mode_exists_status: + if existing_mode == type: + ctx.fail("{} is already in {} mode".format(port, type)) + else: + if is_port: + db.cfgdb.mod_entry("PORT", port, {"mode": "{}".format(type)}) + # if not port then is a port channel + elif not is_port: + db.cfgdb.mod_entry("PORTCHANNEL", port, {"mode": "{}".format(type)}) + + click.echo("{} switched to {} mode".format(port, type)) diff --git a/config/syslog.py b/config/syslog.py index 90fc52ec9d..7228e365c8 100644 --- a/config/syslog.py +++ b/config/syslog.py @@ -5,7 +5,9 @@ import subprocess import utilities_common.cli as clicommon +import utilities_common.multi_asic as multi_asic_util from sonic_py_common import logger +from sonic_py_common import multi_asic from syslog_util import common as syslog_common @@ -457,17 +459,240 @@ def delete(db, server_ip_address): def rate_limit_host(db, interval, burst): """ Configure syslog rate limit for host """ syslog_common.rate_limit_validator(interval, burst) - syslog_common.save_rate_limit_to_db(db, None, interval, burst, log) + syslog_common.save_rate_limit_to_db(db.cfgdb, None, interval, burst, log) @syslog.command("rate-limit-container") @click.argument("service_name", required=True) @click.option("-i", "--interval", help="Configures syslog rate limit interval in seconds for specified containers", type=click.IntRange(0, 2147483647)) @click.option("-b", "--burst", help="Configures syslog rate limit burst in number of messages for specified containers", type=click.IntRange(0, 2147483647)) +@click.option('--namespace', '-n', 'namespace', default=None, + type=click.Choice(multi_asic_util.multi_asic_ns_choices() + ['default']), + show_default=True, help='Namespace name or all') @clicommon.pass_db -def rate_limit_container(db, service_name, interval, burst): +def rate_limit_container(db, service_name, interval, burst, namespace): """ Configure syslog rate limit for containers """ syslog_common.rate_limit_validator(interval, burst) - feature_data = db.cfgdb.get_table(syslog_common.FEATURE_TABLE) + features = db.cfgdb.get_table(syslog_common.FEATURE_TABLE) + syslog_common.service_validator(features, service_name) + + global_feature_data, per_ns_feature_data = syslog_common.extract_feature_data(features) + if not namespace: + # for all namespaces + for namespace, cfg_db in db.cfgdb_clients.items(): + if namespace == multi_asic.DEFAULT_NAMESPACE: + feature_data = global_feature_data + else: + feature_data = per_ns_feature_data + if service_name and service_name not in feature_data: + continue + syslog_common.service_validator(feature_data, service_name) + syslog_common.save_rate_limit_to_db(cfg_db, service_name, interval, burst, log) + return + elif namespace == 'default': + # for default/global namespace only + namespace = multi_asic.DEFAULT_NAMESPACE + feature_data = global_feature_data + else: + # for a specific namespace + feature_data = per_ns_feature_data + syslog_common.service_validator(feature_data, service_name) - syslog_common.save_rate_limit_to_db(db, service_name, interval, burst, log) + syslog_common.save_rate_limit_to_db(db.cfgdb_clients[namespace], service_name, interval, burst, log) + + +@syslog.group( + name="rate-limit-feature", + cls=clicommon.AliasedGroup +) +def rate_limit_feature(): + """ Configure syslog rate limit feature """ + pass + + +def get_feature_names_to_proceed(db, service_name, namespace): + """Get feature name list to be proceed by "config syslog rate-limit-feature enable" and + "config syslog rate-limit-feature disable" CLIs + + Args: + db (object): Db object + service_name (str): Nullable service name to be enable/disable + namespace (str): Namespace provided by user + + Returns: + list: A list of feature name + """ + features = db.cfgdb.get_table(syslog_common.FEATURE_TABLE) + if service_name: + syslog_common.service_validator(features, service_name) + + global_feature_data, per_ns_feature_data = syslog_common.extract_feature_data(features) + if not namespace: + if not service_name: + feature_list = [feature_name for feature_name in global_feature_data.keys()] + if multi_asic.is_multi_asic(): + asic_count = multi_asic.get_num_asics() + for i in range(asic_count): + feature_list.extend([f'{feature_name}{i}' for feature_name in per_ns_feature_data.keys()]) + else: + feature_config = features[service_name] + feature_list = [] + if feature_config[syslog_common.FEATURE_HAS_GLOBAL_SCOPE].lower() == 'true': + feature_list.append(service_name) + + if multi_asic.is_multi_asic(): + if feature_config[syslog_common.FEATURE_HAS_PER_ASIC_SCOPE].lower() == 'true': + asic_count = multi_asic.get_num_asics() + for i in range(asic_count): + feature_list.append(multi_asic.get_container_name_from_asic_id(service_name, i)) + elif namespace == 'default': + if not service_name: + feature_list = [feature_name for feature_name in global_feature_data.keys()] + else: + syslog_common.service_validator(global_feature_data, service_name) + feature_list = [service_name] + else: + asic_num = multi_asic.get_asic_id_from_name(namespace) + if not service_name: + feature_list = [multi_asic.get_container_name_from_asic_id(feature_name, asic_num) for feature_name in per_ns_feature_data.keys()] + else: + syslog_common.service_validator(per_ns_feature_data, service_name) + feature_list = [multi_asic.get_container_name_from_asic_id(service_name, asic_num)] + return feature_list + + +@rate_limit_feature.command("enable") +@click.argument("service_name", required=False) +@click.option('--namespace', '-n', 'namespace', default=None, + type=click.Choice(multi_asic_util.multi_asic_ns_choices() + ['default']), + show_default=True, help='Namespace name or all') +@clicommon.pass_db +def enable_rate_limit_feature(db, service_name, namespace): + """ Enable syslog rate limit feature """ + feature_list = get_feature_names_to_proceed(db, service_name, namespace) + for feature_name in feature_list: + click.echo(f'Enabling syslog rate limit feature for {feature_name}') + shell_cmd = f'docker ps -f status=running --format "{{{{.Names}}}}" | grep -E "^{feature_name}$"' + output, _ = clicommon.run_command(shell_cmd, return_cmd=True, shell=True) + if not output: + click.echo(f'{feature_name} is not running, ignoring...') + continue + + output, _ = clicommon.run_command(['docker', 'exec', '-i', feature_name, 'supervisorctl', 'status', 'containercfgd'], + ignore_error=True, return_cmd=True) + if 'no such process' not in output: + click.echo(f'Syslog rate limit feature is already enabled in {feature_name}, ignoring...') + continue + + commands = [ + ['docker', 'cp', '/usr/share/sonic/templates/containercfgd.conf', f'{feature_name}:/etc/supervisor/conf.d/'], + ['docker', 'exec', '-i', feature_name, 'supervisorctl', 'reread'], + ['docker', 'exec', '-i', feature_name, 'supervisorctl', 'update'], + ['docker', 'exec', '-i', feature_name, 'supervisorctl', 'start', 'containercfgd'] + ] + + failed = False + for command in commands: + output, ret = clicommon.run_command(command, return_cmd=True) + if ret != 0: + failed = True + click.echo(f'Enable syslog rate limit feature for {feature_name} failed - {output}') + break + + if not failed: + click.echo(f'Enabled syslog rate limit feature for {feature_name}') + + +@rate_limit_feature.command("disable") +@click.argument("service_name", required=False) +@click.option('--namespace', '-n', 'namespace', default=None, + type=click.Choice(multi_asic_util.multi_asic_ns_choices() + ['default']), + show_default=True, help='Namespace name or all') +@clicommon.pass_db +def disable_rate_limit_feature(db, service_name, namespace): + """ Disable syslog rate limit feature """ + feature_list = get_feature_names_to_proceed(db, service_name, namespace) + for feature_name in feature_list: + click.echo(f'Disabling syslog rate limit feature for {feature_name}') + shell_cmd = f'docker ps -f status=running --format "{{{{.Names}}}}" | grep -E "^{feature_name}$"' + output, _ = clicommon.run_command(shell_cmd, return_cmd=True, shell=True) + if not output: + click.echo(f'{feature_name} is not running, ignoring...') + continue + + output, _ = clicommon.run_command(['docker', 'exec', '-i', feature_name, 'supervisorctl', 'status', 'containercfgd'], + ignore_error=True, return_cmd=True) + if 'no such process' in output: + click.echo(f'Syslog rate limit feature is already disabled in {feature_name}, ignoring...') + continue + + commands = [ + ['docker', 'exec', '-i', feature_name, 'supervisorctl', 'stop', 'containercfgd'], + ['docker', 'exec', '-i', feature_name, 'rm', '-f', '/etc/supervisor/conf.d/containercfgd.conf'], + ['docker', 'exec', '-i', feature_name, 'supervisorctl', 'reread'], + ['docker', 'exec', '-i', feature_name, 'supervisorctl', 'update'] + ] + failed = False + for command in commands: + output, ret = clicommon.run_command(command, return_cmd=True) + if ret != 0: + failed = True + click.echo(f'Disable syslog rate limit feature for {feature_name} failed - {output}') + break + + if not failed: + click.echo(f'Disabled syslog rate limit feature for {feature_name}') + + +@syslog.command('level') +@click.option("-i", "--identifier", + required=True, + help="Log identifier in DB for which loglevel is applied (provided with -l)") +@click.option("-l", "--level", + required=True, + help="Loglevel value", + type=click.Choice(['DEBUG', 'INFO', 'NOTICE', 'WARN', 'ERROR'])) +@click.option("--container", + help="Container name to which the SIGHUP is sent (provided with --pid or --program)") +@click.option("--program", + help="Program name to which the SIGHUP is sent (provided with --container)") +@click.option("--pid", + help="Process ID to which the SIGHUP is sent (provided with --container if PID is from container)") +@click.option('--namespace', '-n', 'namespace', default=None, + type=click.Choice(multi_asic_util.multi_asic_ns_choices()), + show_default=True, help='Namespace name') +@clicommon.pass_db +def level(db, identifier, level, container, program, pid, namespace): + """ Configure log level """ + if program and not container: + raise click.UsageError('--program must be specified with --container') + + if container and not program and not pid: + raise click.UsageError('--container must be specified with --pid or --program') + + if not namespace: + cfg_db = db.cfgdb + else: + asic_id = multi_asic.get_asic_id_from_name(namespace) + container = f'{container}{asic_id}' + cfg_db = db.cfgdb_clients[namespace] + + cfg_db.mod_entry('LOGGER', identifier, {'LOGLEVEL': level}) + if not container and not program and not pid: + return + + log_config = cfg_db.get_entry('LOGGER', identifier) + require_manual_refresh = log_config.get('require_manual_refresh') + if not require_manual_refresh: + return + + if container: + if program: + command = ['docker', 'exec', '-i', container, 'supervisorctl', 'signal', 'HUP', program] + else: + command = ['docker', 'exec', '-i', container, 'kill', '-s', 'SIGHUP', pid] + else: + command = ['kill', '-s', 'SIGHUP', pid] + output, ret = clicommon.run_command(command, return_cmd=True) + if ret != 0: + raise click.ClickException(f'Failed: {output}') diff --git a/config/vlan.py b/config/vlan.py index 6cc3193ec0..eae51eb312 100644 --- a/config/vlan.py +++ b/config/vlan.py @@ -1,12 +1,12 @@ import click import utilities_common.cli as clicommon import utilities_common.dhcp_relay_util as dhcp_relay_util -from swsscommon.swsscommon import SonicV2Connector from jsonpatch import JsonPatchConflict from time import sleep from .utils import log from .validated_config_db_connector import ValidatedConfigDBConnector +from . import stp ADHOC_VALIDATION = True DHCP_RELAY_TABLE = "DHCP_RELAY" @@ -15,6 +15,8 @@ # # 'vlan' group ('config vlan ...') # + + @click.group(cls=clicommon.AbbreviationGroup, name='vlan') def vlan(): """VLAN-related configuration tasks""" @@ -32,28 +34,54 @@ def is_dhcp_relay_running(): @vlan.command('add') -@click.argument('vid', metavar='', required=True, type=int) +@click.argument('vid', metavar='', required=True) +@click.option('-m', '--multiple', is_flag=True, help="Add Multiple Vlan(s) in Range or in Comma separated list") @clicommon.pass_db -def add_vlan(db, vid): +def add_vlan(db, vid, multiple): """Add VLAN""" ctx = click.get_current_context() - vlan = 'Vlan{}'.format(vid) config_db = ValidatedConfigDBConnector(db.cfgdb) + + vid_list = [] + + # parser will parse the vid input if there are syntax errors it will throw error + if multiple: + vid_list = clicommon.multiple_vlan_parser(ctx, vid) + else: + if not vid.isdigit(): + ctx.fail("{} is not integer".format(vid)) + vid_list.append(int(vid)) + if ADHOC_VALIDATION: - if not clicommon.is_vlanid_in_range(vid): - ctx.fail("Invalid VLAN ID {} (1-4094)".format(vid)) - if vid == 1: - ctx.fail("{} is default VLAN".format(vlan)) # TODO: MISSING CONSTRAINT IN YANG MODEL + # loop will execute till an exception occurs + for vid in vid_list: + + if not clicommon.is_vlanid_in_range(vid): + ctx.fail("Invalid VLAN ID {} (2-4094)".format(vid)) + + # Multiple VLANs need to be referenced + vlan = 'Vlan{}'.format(vid) + + # Defualt VLAN checker + if vid == 1: + ctx.fail("{} is default VLAN".format(vlan)) # TODO: MISSING CONSTRAINT IN YANG MODEL + + log.log_info("'vlan add {}' executing...".format(vid)) + + if clicommon.check_if_vlanid_exist(db.cfgdb, vlan): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} already exists.".format(vlan)) + + if clicommon.check_if_vlanid_exist(db.cfgdb, vlan, "DHCP_RELAY"): + ctx.fail("DHCPv6 relay config for {} already exists".format(vlan)) - if clicommon.check_if_vlanid_exist(db.cfgdb, vlan): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} already exists".format(vlan)) - if clicommon.check_if_vlanid_exist(db.cfgdb, vlan, "DHCP_RELAY"): - ctx.fail("DHCPv6 relay config for {} already exists".format(vlan)) - # set dhcpv4_relay table - set_dhcp_relay_table('VLAN', config_db, vlan, {'vlanid': str(vid)}) + # Enable STP on VLAN if PVST is enabled globally + stp.vlan_enable_stp(db.cfgdb, vlan) + + # set dhcpv4_relay table + set_dhcp_relay_table('VLAN', config_db, vlan, {'vlanid': str(vid)}) def is_dhcpv6_relay_config_exist(db, vlan_name): @@ -67,76 +95,125 @@ def is_dhcpv6_relay_config_exist(db, vlan_name): return True -def delete_state_db_entry(entry_name): - state_db = SonicV2Connector() - state_db.connect(state_db.STATE_DB) - exists = state_db.exists(state_db.STATE_DB, 'DHCPv6_COUNTER_TABLE|{}'.format(entry_name)) +def delete_db_entry(entry_name, db_connector, db_name): + exists = db_connector.exists(db_name, entry_name) if exists: - state_db.delete(state_db.STATE_DB, 'DHCPv6_COUNTER_TABLE|{}'.format(entry_name)) + db_connector.delete(db_name, entry_name) + + +def enable_stp_on_port(db, port): + if stp.is_global_stp_enabled(db) is True: + vlan_list_for_intf = stp.get_vlan_list_for_interface(db, port) + if len(vlan_list_for_intf) == 0: + stp.interface_enable_stp(db, port) + +def disable_stp_on_vlan_port(db, vlan, port): + if stp.is_global_stp_enabled(db) is True: + vlan_interface = str(vlan) + "|" + port + db.set_entry('STP_VLAN_PORT', vlan_interface, None) + vlan_list_for_intf = stp.get_vlan_list_for_interface(db, port) + if len(vlan_list_for_intf) == 0: + db.set_entry('STP_PORT', port, None) + + +def disable_stp_on_vlan(db, vlan_interface): + db.set_entry('STP_VLAN', vlan_interface, None) + stp_intf_list = stp.get_intf_list_from_stp_vlan_intf_table(db, vlan_interface) + for intf_name in stp_intf_list: + key = vlan_interface + "|" + intf_name + db.set_entry('STP_VLAN_PORT', key, None) @vlan.command('del') -@click.argument('vid', metavar='', required=True, type=int) +@click.argument('vid', metavar='', required=True) +@click.option('-m', '--multiple', is_flag=True, help="Add Multiple Vlan(s) in Range or in Comma separated list") @click.option('--no_restart_dhcp_relay', is_flag=True, type=click.BOOL, required=False, default=False, help="If no_restart_dhcp_relay is True, do not restart dhcp_relay while del vlan and \ require dhcpv6 relay of this is empty") @clicommon.pass_db -def del_vlan(db, vid, no_restart_dhcp_relay): +def del_vlan(db, vid, multiple, no_restart_dhcp_relay): """Delete VLAN""" - log.log_info("'vlan del {}' executing...".format(vid)) - ctx = click.get_current_context() - vlan = 'Vlan{}'.format(vid) - if no_restart_dhcp_relay: - if is_dhcpv6_relay_config_exist(db, vlan): - ctx.fail("Can't delete {} because related DHCPv6 Relay config is exist".format(vlan)) + + vid_list = [] + # parser will parse the vid input if there are syntax errors it will throw error + if multiple: + vid_list = clicommon.multiple_vlan_parser(ctx, vid) + else: + if not vid.isdigit(): + ctx.fail("{} is not integer".format(vid)) + vid_list.append(int(vid)) config_db = ValidatedConfigDBConnector(db.cfgdb) + if ADHOC_VALIDATION: - if not clicommon.is_vlanid_in_range(vid): - ctx.fail("Invalid VLAN ID {} (1-4094)".format(vid)) + for vid in vid_list: + log.log_info("'vlan del {}' executing...".format(vid)) + + if not clicommon.is_vlanid_in_range(vid): + ctx.fail("Invalid VLAN ID {} (2-4094)".format(vid)) + + # Multiple VLANs needs to be referenced + vlan = 'Vlan{}'.format(vid) - if clicommon.check_if_vlanid_exist(db.cfgdb, vlan) == False: - ctx.fail("{} does not exist".format(vlan)) + # Multiple VLANs needs to be checked + if no_restart_dhcp_relay: + if is_dhcpv6_relay_config_exist(db, vlan): + ctx.fail("Can't delete {} because related DHCPv6 Relay config is exist".format(vlan)) - intf_table = db.cfgdb.get_table('VLAN_INTERFACE') - for intf_key in intf_table: - if ((type(intf_key) is str and intf_key == 'Vlan{}'.format(vid)) or # TODO: MISSING CONSTRAINT IN YANG MODEL - (type(intf_key) is tuple and intf_key[0] == 'Vlan{}'.format(vid))): - ctx.fail("{} can not be removed. First remove IP addresses assigned to this VLAN".format(vlan)) + if clicommon.check_if_vlanid_exist(db.cfgdb, vlan) is False: + ctx.fail("{} does not exist".format(vlan)) - keys = [ (k, v) for k, v in db.cfgdb.get_table('VLAN_MEMBER') if k == 'Vlan{}'.format(vid) ] + intf_table = db.cfgdb.get_table('VLAN_INTERFACE') + for intf_key in intf_table: + if ((type( + intf_key) is str and intf_key == 'Vlan{}'.format(vid)) or # TODO: MISSING CONSTRAINT IN YANG MODEL + (type(intf_key) is tuple and intf_key[0] == 'Vlan{}'.format(vid))): + ctx.fail("{} can not be removed. First remove IP addresses assigned to this VLAN".format(vlan)) - if keys: # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("VLAN ID {} can not be removed. First remove all members assigned to this VLAN.".format(vid)) + keys = [(k, v) for k, v in db.cfgdb.get_table('VLAN_MEMBER') if k == 'Vlan{}'.format(vid)] - vxlan_table = db.cfgdb.get_table('VXLAN_TUNNEL_MAP') - for vxmap_key, vxmap_data in vxlan_table.items(): - if vxmap_data['vlan'] == 'Vlan{}'.format(vid): - ctx.fail("vlan: {} can not be removed. First remove vxlan mapping '{}' assigned to VLAN".format(vid, '|'.join(vxmap_key)) ) + if keys: # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("VLAN ID {} can not be removed. First remove all members assigned to this VLAN.".format(vid)) - # set dhcpv4_relay table - set_dhcp_relay_table('VLAN', config_db, vlan, None) + vxlan_table = db.cfgdb.get_table('VXLAN_TUNNEL_MAP') + for vxmap_key, vxmap_data in vxlan_table.items(): + if vxmap_data['vlan'] == 'Vlan{}'.format(vid): + ctx.fail("vlan: {} can not be removed. " + "First remove vxlan mapping '{}' assigned to VLAN".format( + vid, '|'.join(vxmap_key))) - if not no_restart_dhcp_relay and is_dhcpv6_relay_config_exist(db, vlan): - # set dhcpv6_relay table - set_dhcp_relay_table('DHCP_RELAY', config_db, vlan, None) - # We need to restart dhcp_relay service after dhcpv6_relay config change - if is_dhcp_relay_running(): - dhcp_relay_util.handle_restart_dhcp_relay_service() - delete_state_db_entry(vlan) + # set dhcpv4_relay table + set_dhcp_relay_table('VLAN', config_db, vlan, None) + + if not no_restart_dhcp_relay and is_dhcpv6_relay_config_exist(db, vlan): + # set dhcpv6_relay table + set_dhcp_relay_table('DHCP_RELAY', config_db, vlan, None) + # We need to restart dhcp_relay service after dhcpv6_relay config change + if is_dhcp_relay_running(): + dhcp_relay_util.handle_restart_dhcp_relay_service() + + delete_db_entry("DHCPv6_COUNTER_TABLE|{}".format(vlan), db.db, db.db.STATE_DB) + delete_db_entry("DHCP_COUNTER_TABLE|{}".format(vlan), db.db, db.db.STATE_DB) + + # Delete STP_VLAN & STP_VLAN_PORT entries when VLAN is deleted. + disable_stp_on_vlan(db.cfgdb, 'Vlan{}'.format(vid)) vlans = db.cfgdb.get_keys('VLAN') if not vlans: docker_exec_cmd = ['docker', 'exec', '-i', 'swss'] - _, rc = clicommon.run_command(docker_exec_cmd + ['supervisorctl', 'status', 'ndppd'], ignore_error=True, return_cmd=True) + _, rc = clicommon.run_command( + docker_exec_cmd + ['supervisorctl', 'status', 'ndppd'], ignore_error=True, return_cmd=True) if rc == 0: click.echo("No VLANs remaining, stopping ndppd service") - clicommon.run_command(docker_exec_cmd + ['supervisorctl', 'stop', 'ndppd'], ignore_error=True, return_cmd=True) - clicommon.run_command(docker_exec_cmd + ['rm', '-f', '/etc/supervisor/conf.d/ndppd.conf'], ignore_error=True, return_cmd=True) + clicommon.run_command( + docker_exec_cmd + ['supervisorctl', 'stop', 'ndppd'], ignore_error=True, return_cmd=True) + clicommon.run_command( + docker_exec_cmd + ['rm', '-f', '/etc/supervisor/conf.d/ndppd.conf'], ignore_error=True, return_cmd=True) clicommon.run_command(docker_exec_cmd + ['supervisorctl', 'update'], return_cmd=True) + def restart_ndppd(): verify_swss_running_cmd = ['docker', 'container', 'inspect', '-f', '{{.State.Status}}', 'swss'] docker_exec_cmd = ['docker', 'exec', '-i', 'swss'] @@ -149,7 +226,8 @@ def restart_ndppd(): output, _ = clicommon.run_command(verify_swss_running_cmd, return_cmd=True) if output and output.strip() != "running": - click.echo(click.style('SWSS container is not running, changes will take effect the next time the SWSS container starts', fg='red'),) + click.echo(click.style('SWSS container is not running, ' + 'changes will take effect the next time the SWSS container starts', fg='red'),) return _, rc = clicommon.run_command(docker_exec_cmd + ndppd_status_cmd, ignore_error=True, return_cmd=True) @@ -163,6 +241,7 @@ def restart_ndppd(): sleep(3) clicommon.run_command(docker_exec_cmd + ndppd_restart_cmd, return_cmd=True) + @vlan.command('proxy_arp') @click.argument('vid', metavar='', required=True, type=int) @click.argument('mode', metavar='', required=True, type=click.Choice(["enabled", "disabled"])) @@ -185,100 +264,139 @@ def config_proxy_arp(db, vid, mode): # # 'member' group ('config vlan member ...') # + + @vlan.group(cls=clicommon.AbbreviationGroup, name='member') def vlan_member(): pass + @vlan_member.command('add') -@click.argument('vid', metavar='', required=True, type=int) +@click.argument('vid', metavar='', required=True) @click.argument('port', metavar='port', required=True) -@click.option('-u', '--untagged', is_flag=True) +@click.option('-u', '--untagged', is_flag=True, help="Untagged status") +@click.option('-m', '--multiple', is_flag=True, help="Add Multiple Vlan(s) in Range or in Comma separated list") +@click.option('-e', '--except_flag', is_flag=True, help="Skips the given vlans and adds all other existing vlans") @clicommon.pass_db -def add_vlan_member(db, vid, port, untagged): +def add_vlan_member(db, vid, port, untagged, multiple, except_flag): """Add VLAN member""" ctx = click.get_current_context() - log.log_info("'vlan member add {} {}' executing...".format(vid, port)) - - vlan = 'Vlan{}'.format(vid) - + # parser will parse the vid input if there are syntax errors it will throw error + vid_list = clicommon.vlan_member_input_parser(ctx, "add", db, except_flag, multiple, vid, port) + # multiple vlan command cannot be used to add multiple untagged vlan members + if untagged and (multiple or except_flag or vid == "all"): + ctx.fail("{} cannot have more than one untagged Vlan.".format(port)) config_db = ValidatedConfigDBConnector(db.cfgdb) if ADHOC_VALIDATION: - if not clicommon.is_vlanid_in_range(vid): - ctx.fail("Invalid VLAN ID {} (1-4094)".format(vid)) - - if clicommon.check_if_vlanid_exist(db.cfgdb, vlan) == False: - ctx.fail("{} does not exist".format(vlan)) - - if clicommon.get_interface_naming_mode() == "alias": # TODO: MISSING CONSTRAINT IN YANG MODEL - alias = port - iface_alias_converter = clicommon.InterfaceAliasConverter(db) - port = iface_alias_converter.alias_to_name(alias) - if port is None: - ctx.fail("cannot find port name for alias {}".format(alias)) + for vid in vid_list: + vlan = 'Vlan{}'.format(vid) + # default vlan checker + if vid == 1: + ctx.fail("{} is default VLAN".format(vlan)) + log.log_info("'vlan member add {} {}' executing...".format(vid, port)) + if not clicommon.is_vlanid_in_range(vid): + ctx.fail("Invalid VLAN ID {} (2-4094)".format(vid)) + if clicommon.check_if_vlanid_exist(db.cfgdb, vlan) is False: + ctx.fail("{} does not exist".format(vlan)) + if clicommon.get_interface_naming_mode() == "alias": # TODO: MISSING CONSTRAINT IN YANG MODEL + alias = port + iface_alias_converter = clicommon.InterfaceAliasConverter(db) + port = iface_alias_converter.alias_to_name(alias) + if port is None: + ctx.fail("cannot find port name for alias {}".format(alias)) + if clicommon.is_port_mirror_dst_port(db.cfgdb, port): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is configured as mirror destination port".format(port)) + if clicommon.is_port_vlan_member(db.cfgdb, port, vlan): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is already a member of {}".format(port, vlan)) + if clicommon.is_valid_port(db.cfgdb, port): + is_port = True + elif clicommon.is_valid_portchannel(db.cfgdb, port): + is_port = False + else: + ctx.fail("{} does not exist".format(port)) + if (is_port and clicommon.is_port_router_interface(db.cfgdb, port)) or \ + (not is_port and clicommon.is_pc_router_interface( + db.cfgdb, port)): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is a router interface!".format(port)) + portchannel_member_table = db.cfgdb.get_table('PORTCHANNEL_MEMBER') + if (is_port and clicommon.interface_is_in_portchannel( + portchannel_member_table, port)): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is part of portchannel!".format(port)) + if (clicommon.interface_is_untagged_member( + db.cfgdb, port) and untagged): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is already untagged member!".format(port)) + # checking mode status of port if its access, trunk or routed + if is_port: + port_data = config_db.get_entry('PORT', port) + # if not port then is a port channel + elif not is_port: + port_data = config_db.get_entry('PORTCHANNEL', port) + existing_mode = None + if "mode" in port_data: + existing_mode = port_data["mode"] + if existing_mode == "routed": + ctx.fail("{} is in routed mode!\nUse switchport mode command to change port mode".format(port)) + mode_type = "access" if untagged else "trunk" + if existing_mode == "access" and mode_type == "trunk": # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is in access mode! Tagged Members cannot be added".format(port)) + elif existing_mode == mode_type or (existing_mode == "trunk" and mode_type == "access"): + pass + + # If port is being made L2 port, enable STP + enable_stp_on_port(db.cfgdb, port) + + try: + config_db.set_entry('VLAN_MEMBER', (vlan, port), {'tagging_mode': "untagged" if untagged else "tagged"}) + except ValueError: + ctx.fail("{} invalid or does not exist, or {} invalid or does not exist".format(vlan, port)) - if clicommon.is_port_mirror_dst_port(db.cfgdb, port): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} is configured as mirror destination port".format(port)) - - if clicommon.is_port_vlan_member(db.cfgdb, port, vlan): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} is already a member of {}".format(port, vlan)) - - if clicommon.is_valid_port(db.cfgdb, port): - is_port = True - elif clicommon.is_valid_portchannel(db.cfgdb, port): - is_port = False - else: - ctx.fail("{} does not exist".format(port)) - - if (is_port and clicommon.is_port_router_interface(db.cfgdb, port)) or \ - (not is_port and clicommon.is_pc_router_interface(db.cfgdb, port)): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} is a router interface!".format(port)) - - portchannel_member_table = db.cfgdb.get_table('PORTCHANNEL_MEMBER') - - if (is_port and clicommon.interface_is_in_portchannel(portchannel_member_table, port)): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} is part of portchannel!".format(port)) - - if (clicommon.interface_is_untagged_member(db.cfgdb, port) and untagged): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} is already untagged member!".format(port)) - - try: - config_db.set_entry('VLAN_MEMBER', (vlan, port), {'tagging_mode': "untagged" if untagged else "tagged" }) - except ValueError: - ctx.fail("{} invalid or does not exist, or {} invalid or does not exist".format(vlan, port)) @vlan_member.command('del') -@click.argument('vid', metavar='', required=True, type=int) +@click.argument('vid', metavar='', required=True) @click.argument('port', metavar='', required=True) +@click.option('-m', '--multiple', is_flag=True, help="Add Multiple Vlan(s) in Range or in Comma separated list") +@click.option('-e', '--except_flag', is_flag=True, help="Skips the given vlans and adds all other existing vlans") @clicommon.pass_db -def del_vlan_member(db, vid, port): +def del_vlan_member(db, vid, port, multiple, except_flag): """Delete VLAN member""" ctx = click.get_current_context() - log.log_info("'vlan member del {} {}' executing...".format(vid, port)) - vlan = 'Vlan{}'.format(vid) - + + # parser will parse the vid input if there are syntax errors it will throw error + + vid_list = clicommon.vlan_member_input_parser(ctx, "del", db, except_flag, multiple, vid, port) + config_db = ValidatedConfigDBConnector(db.cfgdb) if ADHOC_VALIDATION: - if not clicommon.is_vlanid_in_range(vid): - ctx.fail("Invalid VLAN ID {} (1-4094)".format(vid)) + for vid in vid_list: + log.log_info("'vlan member del {} {}' executing...".format(vid, port)) + + if not clicommon.is_vlanid_in_range(vid): + ctx.fail("Invalid VLAN ID {} (2-4094)".format(vid)) + + vlan = 'Vlan{}'.format(vid) - if clicommon.check_if_vlanid_exist(db.cfgdb, vlan) == False: - ctx.fail("{} does not exist".format(vlan)) + if clicommon.check_if_vlanid_exist(db.cfgdb, vlan) is False: + ctx.fail("{} does not exist".format(vlan)) - if clicommon.get_interface_naming_mode() == "alias": # TODO: MISSING CONSTRAINT IN YANG MODEL - alias = port - iface_alias_converter = clicommon.InterfaceAliasConverter(db) - port = iface_alias_converter.alias_to_name(alias) - if port is None: - ctx.fail("cannot find port name for alias {}".format(alias)) + if clicommon.get_interface_naming_mode() == "alias": # TODO: MISSING CONSTRAINT IN YANG MODEL + alias = port + iface_alias_converter = clicommon.InterfaceAliasConverter(db) + port = iface_alias_converter.alias_to_name(alias) + if port is None: + ctx.fail("cannot find port name for alias {}".format(alias)) - if not clicommon.is_port_vlan_member(db.cfgdb, port, vlan): # TODO: MISSING CONSTRAINT IN YANG MODEL - ctx.fail("{} is not a member of {}".format(port, vlan)) + if not clicommon.is_port_vlan_member(db.cfgdb, port, vlan): # TODO: MISSING CONSTRAINT IN YANG MODEL + ctx.fail("{} is not a member of {}".format(port, vlan)) - try: - config_db.set_entry('VLAN_MEMBER', (vlan, port), None) - except JsonPatchConflict: - ctx.fail("{} invalid or does not exist, or {} is not a member of {}".format(vlan, port, vlan)) + # If port is being made non-L2 port, disable STP + disable_stp_on_vlan_port(db.cfgdb, vlan, port) + try: + config_db.set_entry('VLAN_MEMBER', (vlan, port), None) + delete_db_entry("DHCPv6_COUNTER_TABLE|{}".format(port), db.db, db.db.STATE_DB) + delete_db_entry("DHCP_COUNTER_TABLE|{}".format(port), db.db, db.db.STATE_DB) + except JsonPatchConflict: + ctx.fail("{} invalid or does not exist, or {} is not a member of {}".format(vlan, port, vlan)) diff --git a/config/vxlan.py b/config/vxlan.py index 71377d5609..ea49c4a34d 100644 --- a/config/vxlan.py +++ b/config/vxlan.py @@ -3,6 +3,7 @@ from jsonpatch import JsonPatchConflict from .validated_config_db_connector import ValidatedConfigDBConnector +from swsscommon.swsscommon import isInterfaceNameValid, IFACE_NAME_MAX_LEN ADHOC_VALIDATION = True # @@ -24,6 +25,8 @@ def add_vxlan(db, vxlan_name, src_ip): if ADHOC_VALIDATION: if not clicommon.is_ipaddress(src_ip): ctx.fail("{} invalid src ip address".format(src_ip)) + if not isInterfaceNameValid(vxlan_name): + ctx.fail("'vxlan_name' length should not exceed {} characters".format(IFACE_NAME_MAX_LEN)) vxlan_keys = db.cfgdb.get_keys('VXLAN_TUNNEL') if not vxlan_keys: @@ -317,4 +320,3 @@ def del_vxlan_map_range(db, vxlan_name, vlan_start, vlan_end, vni_start): config_db.set_entry('VXLAN_TUNNEL_MAP', mapname, None) except JsonPatchConflict as e: ctx.fail("Invalid ConfigDB. Error: {}".format(e)) - diff --git a/consutil/lib.py b/consutil/lib.py index 1d7f967bd3..e597e3b643 100644 --- a/consutil/lib.py +++ b/consutil/lib.py @@ -277,7 +277,7 @@ def init_device_prefix(): @staticmethod def list_console_ttys(): """Lists all console tty devices""" - cmd = ["ls", SysInfoProvider.DEVICE_PREFIX + "*"] + cmd = ["bash", "-c", "ls " + SysInfoProvider.DEVICE_PREFIX + "*"] output, _ = SysInfoProvider.run_command(cmd, abort=False) ttys = output.split('\n') ttys = list([dev for dev in ttys if re.match(SysInfoProvider.DEVICE_PREFIX + r"\d+", dev) != None]) diff --git a/counterpoll/main.py b/counterpoll/main.py index ad15c8c248..f0e08bde67 100644 --- a/counterpoll/main.py +++ b/counterpoll/main.py @@ -3,17 +3,29 @@ from flow_counter_util.route import exit_if_route_flow_counter_not_support from swsscommon.swsscommon import ConfigDBConnector from tabulate import tabulate +from sonic_py_common import device_info BUFFER_POOL_WATERMARK = "BUFFER_POOL_WATERMARK" PORT_BUFFER_DROP = "PORT_BUFFER_DROP" PG_DROP = "PG_DROP" ACL = "ACL" +ENI = "ENI" DISABLE = "disable" ENABLE = "enable" DEFLT_60_SEC= "default (60000)" DEFLT_10_SEC= "default (10000)" DEFLT_1_SEC = "default (1000)" + +def is_dpu(db): + """ Check if the device is DPU """ + platform_info = device_info.get_platform_info(db) + if platform_info.get('switch_type') == 'dpu': + return True + else: + return False + + @click.group() def cli(): """ SONiC Static Counter Poll configurations """ @@ -126,6 +138,7 @@ def disable(): port_info['FLEX_COUNTER_STATUS'] = DISABLE configdb.mod_entry("FLEX_COUNTER_TABLE", PORT_BUFFER_DROP, port_info) + # Ingress PG drop packet stat @cli.group() @click.pass_context @@ -382,6 +395,151 @@ def disable(ctx): fc_info['FLEX_COUNTER_STATUS'] = 'disable' ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_ROUTE", fc_info) +# ENI counter commands +@click.group() +@click.pass_context +def eni(ctx): + """ ENI counter commands """ + ctx.obj = ConfigDBConnector() + ctx.obj.connect() + + +@eni.command(name='interval') +@click.argument('poll_interval', type=click.IntRange(1000, 30000)) +@click.pass_context +def eni_interval(ctx, poll_interval): + """ Set eni counter query interval """ + eni_info = {} + eni_info['POLL_INTERVAL'] = poll_interval + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", ENI, eni_info) + + +@eni.command(name='enable') +@click.pass_context +def eni_enable(ctx): + """ Enable eni counter query """ + eni_info = {} + eni_info['FLEX_COUNTER_STATUS'] = 'enable' + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", ENI, eni_info) + + +@eni.command(name='disable') +@click.pass_context +def eni_disable(ctx): + """ Disable eni counter query """ + eni_info = {} + eni_info['FLEX_COUNTER_STATUS'] = 'disable' + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", ENI, eni_info) + + +# WRED queue counter commands +@cli.group() +@click.pass_context +def wredqueue(ctx): + """ WRED queue counter commands """ + ctx.obj = ConfigDBConnector() + ctx.obj.connect() + + +@wredqueue.command(name='interval') +@click.argument('poll_interval', type=click.IntRange(100, 30000)) +@click.pass_context +def wredqueue_interval(ctx, poll_interval): + """ Set wred queue counter query interval """ + wred_queue_info = {} + wred_queue_info['POLL_INTERVAL'] = poll_interval + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "WRED_ECN_QUEUE", wred_queue_info) + + +@wredqueue.command(name='enable') +@click.pass_context +def wredqueue_enable(ctx): + """ Enable wred queue counter query """ + wred_queue_info = {} + wred_queue_info['FLEX_COUNTER_STATUS'] = 'enable' + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "WRED_ECN_QUEUE", wred_queue_info) + + +@wredqueue.command(name='disable') +@click.pass_context +def wredqueue_disable(ctx): + """ Disable wred queue counter query """ + wred_queue_info = {} + wred_queue_info['FLEX_COUNTER_STATUS'] = 'disable' + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "WRED_ECN_QUEUE", wred_queue_info) + + +# WRED port counter commands +@cli.group() +@click.pass_context +def wredport(ctx): + """ WRED port counter commands """ + ctx.obj = ConfigDBConnector() + ctx.obj.connect() + + +@wredport.command(name='interval') +@click.argument('poll_interval', type=click.IntRange(100, 30000)) +@click.pass_context +def wredport_interval(ctx, poll_interval): + """ Set wred port counter query interval """ + wred_port_info = {} + wred_port_info['POLL_INTERVAL'] = poll_interval + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "WRED_ECN_PORT", wred_port_info) + + +@wredport.command(name='enable') +@click.pass_context +def wredport_enable(ctx): + """ Enable wred port counter query """ + wred_port_info = {} + wred_port_info['FLEX_COUNTER_STATUS'] = 'enable' + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "WRED_ECN_PORT", wred_port_info) + + +@wredport.command(name='disable') +@click.pass_context +def wredport_disable(ctx): + """ Disable wred port counter query """ + wred_port_info = {} + wred_port_info['FLEX_COUNTER_STATUS'] = 'disable' + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "WRED_ECN_PORT", wred_port_info) + + +# SRv6 counter commands +@cli.group() +@click.pass_context +def srv6(ctx): + """ SRv6 counter commands """ + ctx.obj = ConfigDBConnector() + ctx.obj.connect() + + +@srv6.command() +@click.pass_context +@click.argument('poll_interval', type=click.IntRange(1000, 30000)) +def interval(ctx, poll_interval): # noqa: F811 + """ Set SRv6 counter query interval """ + srv6_info = {'POLL_INTERVAL': poll_interval} + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "SRV6", srv6_info) + + +@srv6.command() +@click.pass_context +def enable(ctx): # noqa: F811 + """ Enable SRv6 counter query """ + srv6_info = {'FLEX_COUNTER_STATUS': ENABLE} + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "SRV6", srv6_info) + + +@srv6.command() +@click.pass_context +def disable(ctx): # noqa: F811 + """ Disable SRv6 counter query """ + srv6_info = {'FLEX_COUNTER_STATUS': DISABLE} + ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "SRV6", srv6_info) + + @cli.command() def show(): """ Show the counter configuration """ @@ -399,6 +557,10 @@ def show(): tunnel_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'TUNNEL') trap_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_TRAP') route_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_ROUTE') + eni_info = configdb.get_entry('FLEX_COUNTER_TABLE', ENI) + wred_queue_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'WRED_ECN_QUEUE') + wred_port_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'WRED_ECN_PORT') + srv6_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'SRV6') header = ("Type", "Interval (in ms)", "Status") data = [] @@ -427,51 +589,41 @@ def show(): if route_info: data.append(["FLOW_CNT_ROUTE_STAT", route_info.get("POLL_INTERVAL", DEFLT_10_SEC), route_info.get("FLEX_COUNTER_STATUS", DISABLE)]) + if wred_queue_info: + data.append(["WRED_ECN_QUEUE_STAT", wred_queue_info.get("POLL_INTERVAL", DEFLT_10_SEC), + wred_queue_info.get("FLEX_COUNTER_STATUS", DISABLE)]) + if wred_port_info: + data.append(["WRED_ECN_PORT_STAT", wred_port_info.get("POLL_INTERVAL", DEFLT_1_SEC), + wred_port_info.get("FLEX_COUNTER_STATUS", DISABLE)]) + if srv6_info: + data.append(["SRV6_STAT", srv6_info.get("POLL_INTERVAL", DEFLT_10_SEC), + srv6_info.get("FLEX_COUNTER_STATUS", DISABLE)]) + + if is_dpu(configdb) and eni_info: + data.append(["ENI_STAT", eni_info.get("POLL_INTERVAL", DEFLT_10_SEC), + eni_info.get("FLEX_COUNTER_STATUS", DISABLE)]) click.echo(tabulate(data, headers=header, tablefmt="simple", missingval="")) -def _update_config_db_flex_counter_table(status, filename): - """ Update counter configuration in config_db file """ - with open(filename) as config_db_file: - config_db = json.load(config_db_file) - - write_config_db = False - if "FLEX_COUNTER_TABLE" in config_db: - if status != "delay": - for counter, counter_config in config_db["FLEX_COUNTER_TABLE"].items(): - if "FLEX_COUNTER_STATUS" in counter_config and \ - counter_config["FLEX_COUNTER_STATUS"] is not status: - counter_config["FLEX_COUNTER_STATUS"] = status - write_config_db = True - - elif status == "delay": - write_config_db = True - for key in config_db["FLEX_COUNTER_TABLE"].keys(): - config_db["FLEX_COUNTER_TABLE"][key].update({"FLEX_COUNTER_DELAY_STATUS":"true"}) - - if write_config_db: - with open(filename, 'w') as config_db_file: - json.dump(config_db, config_db_file, indent=4) - -# Working on Config DB -@cli.group() -def config_db(): - """ Config DB counter commands """ - -@config_db.command() -@click.argument("filename", default="/etc/sonic/config_db.json", type=click.Path(exists=True)) -def enable(filename): - """ Enable counter configuration in config_db file """ - _update_config_db_flex_counter_table("enable", filename) - -@config_db.command() -@click.argument("filename", default="/etc/sonic/config_db.json", type=click.Path(exists=True)) -def disable(filename): - """ Disable counter configuration in config_db file """ - _update_config_db_flex_counter_table("disable", filename) - -@config_db.command() -@click.argument("filename", default="/etc/sonic/config_db.json", type=click.Path(exists=True)) -def delay(filename): - """ Delay counters in config_db file """ - _update_config_db_flex_counter_table("delay", filename) +""" +The list of dynamic commands that are added on a specific condition. +Format: + (click group/command, callback function) +""" +dynamic_commands = [ + (eni, is_dpu) +] + + +def register_dynamic_commands(cmds): + """ + Dynamically register commands based on condition callback. + """ + db = ConfigDBConnector() + db.connect() + for cmd, cb in cmds: + if cb(db): + cli.add_command(cmd) + + +register_dynamic_commands(dynamic_commands) diff --git a/debug/main.py b/debug/main.py index 069159fc75..1c12dffe85 100755 --- a/debug/main.py +++ b/debug/main.py @@ -4,6 +4,7 @@ import subprocess from shlex import join + def run_command(command, pager=False): command_str = join(command) click.echo(click.style("Command: ", fg='cyan') + click.style(command_str, fg='green')) @@ -25,6 +26,7 @@ def cli(): """SONiC command line - 'debug' command""" pass + prefix_pattern = '^[A-Za-z0-9.:/]*$' p = subprocess.check_output(['sudo', 'vtysh', '-c', 'show version'], text=True) if 'FRRouting' in p: diff --git a/debug/stp.py b/debug/stp.py new file mode 100644 index 0000000000..c154537e2a --- /dev/null +++ b/debug/stp.py @@ -0,0 +1,92 @@ +import click +import utilities_common.cli as clicommon + + +# +# This group houses Spanning_tree commands and subgroups +# +@click.group(cls=clicommon.AliasedGroup, default_if_no_args=False, invoke_without_command=True) +@click.pass_context +def spanning_tree(ctx): + '''debug spanning_tree commands''' + if ctx.invoked_subcommand is None: + command = 'sudo stpctl dbg enable' + clicommon.run_command(command) + + +@spanning_tree.group('dump', cls=clicommon.AliasedGroup, default_if_no_args=False, invoke_without_command=True) +def stp_debug_dump(): + pass + + +@stp_debug_dump.command('global') +def stp_debug_dump_global(): + command = 'sudo stpctl global' + clicommon.run_command(command) + + +@stp_debug_dump.command('vlan') +@click.argument('vlan_id', metavar='', required=True) +def stp_debug_dump_vlan(vlan_id): + command = 'sudo stpctl vlan ' + vlan_id + clicommon.run_command(command) + + +@stp_debug_dump.command('interface') +@click.argument('vlan_id', metavar='', required=True) +@click.argument('interface_name', metavar='', required=True) +def stp_debug_dump_vlan_intf(vlan_id, interface_name): + command = 'sudo stpctl port ' + vlan_id + " " + interface_name + clicommon.run_command(command) + + +@spanning_tree.command('show') +def stp_debug_show(): + command = 'sudo stpctl dbg show' + clicommon.run_command(command) + + +@spanning_tree.command('reset') +def stp_debug_reset(): + command = 'sudo stpctl dbg disable' + clicommon.run_command(command) + + +@spanning_tree.command('bpdu') +@click.argument('mode', metavar='{rx|tx}', required=False) +@click.option('-d', '--disable', is_flag=True) +def stp_debug_bpdu(mode, disable): + command = 'sudo stpctl dbg bpdu {}{}'.format( + ('rx-' if mode == 'rx' else 'tx-' if mode == 'tx' else ''), + ('off' if disable else 'on')) + clicommon.run_command(command) + + +@spanning_tree.command('verbose') +@click.option('-d', '--disable', is_flag=True) +def stp_debug_verbose(disable): + command = 'sudo stpctl dbg verbose {}'.format("off" if disable else "on") + clicommon.run_command(command) + + +@spanning_tree.command('event') +@click.option('-d', '--disable', is_flag=True) +def stp_debug_event(disable): + command = 'sudo stpctl dbg event {}'.format("off" if disable else "on") + clicommon.run_command(command) + + +@spanning_tree.command('vlan') +@click.argument('vlan_id', metavar='', required=True) +@click.option('-d', '--disable', is_flag=True) +def stp_debug_vlan(vlan_id, disable): + command = 'sudo stpctl dbg vlan {} {}'.format(vlan_id, "off" if disable else "on") + clicommon.run_command(command) + + +@spanning_tree.command('interface') +@click.argument('interface_name', metavar='', required=True) +@click.option('-d', '--disable', is_flag=True) +def stp_debug_intf(interface_name, disable): + command = 'sudo stpctl dbg port {} {}'.format(interface_name, "off" if disable else "on") + clicommon.run_command(command) diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 6784b8faf6..34ed0bccf4 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -29,6 +29,10 @@ * [ARP & NDP](#arp--ndp) * [ARP show commands](#arp-show-commands) * [NDP show commands](#ndp-show-commands) +* [ASIC SDK health event](#asic-sdk-health-event) + * [ASIC SDK health event config commands](#asic-sdk-health-event-config-commands) + * [ASIC SDK health event show commands](#asic-sdk-health-event-show-commands) + * [ASIC SDK health event clear commands](#asic-sdk-health-event-clear-commands) * [BFD](#bfd) * [BFD show commands](#bfd-show-commands) * [BGP](#bgp) @@ -39,10 +43,15 @@ * [Console config commands](#console-config-commands) * [Console connect commands](#console-connect-commands) * [Console clear commands](#console-clear-commands) + * [DPU serial console utility](#dpu-serial-console-utility) * [CMIS firmware upgrade](#cmis-firmware-upgrade) * [CMIS firmware version show commands](#cmis-firmware-version-show-commands) * [CMIS firmware upgrade commands](#cmis-firmware-upgrade-commands) * [CMIS firmware target mode commands](#cmis-firmware-target-mode-commands) +* [CMIS debug](#cmis-debug) +* [CMIS debug loopback](#cmis-debug-loopback) +* [CMIS debug rx-output](#cmis-debug-rx-output) +* [CMIS debug tx-output](#cmis-debug-tx-output) * [DHCP Relay](#dhcp-relay) * [DHCP Relay show commands](#dhcp-relay-show-commands) * [DHCP Relay clear commands](#dhcp-relay-clear-commands) @@ -92,6 +101,11 @@ * [Linux Kernel Dump](#linux-kernel-dump) * [Linux Kernel Dump show commands](#Linux-Kernel-Dump-show-commands) * [Linux Kernel Dump config commands](#Linux-Kernel-Dump-config-commands) +* [LDAP](#LDAP) + * [show LDAP global commands](#LDAP-global-show-commands) + * [LDAP global config commands](#LDAP-global-config-commands) + * [show LDAP server commands](#LDAP-server-show-commands) + * [LDAP server config commands](#LDAP-server-config-commands) * [LLDP](#lldp) * [LLDP show commands](#lldp-show-commands) * [Loading, Reloading And Saving Configuration](#loading-reloading-and-saving-configuration) @@ -115,6 +129,9 @@ * [Muxcable](#muxcable) * [Muxcable Show commands](#muxcable-show-commands) * [Muxcable Config commands](#muxcable-config-commands) +* [MMU](#mmu) + * [MMU Show commands](#mmu-show-commands) + * [NAT Config commands](#mmu-config-commands) * [NAT](#nat) * [NAT Show commands](#nat-show-commands) * [NAT Config commands](#nat-config-commands) @@ -140,6 +157,9 @@ * [PortChannels](#portchannels) * [PortChannel Show commands](#portchannel-show-commands) * [PortChannel Config commands](#portchannel-config-commands) +* [Packet Trimming](#packet-trimming) + * [Packet Trimming Show commands](#packet-trimming-show-commands) + * [Packet Trimming Config commands](#packet-trimming-config-commands) * [QoS](#qos) * [QoS Show commands](#qos-show-commands) * [PFC](#pfc) @@ -162,6 +182,8 @@ * [Subinterfaces](#subinterfaces) * [Subinterfaces Show Commands](#subinterfaces-show-commands) * [Subinterfaces Config Commands](#subinterfaces-config-commands) + * [Switchport Modes](#switchport-modes) + * [Switchport Modes Config Commands](#switchportmodes-config-commands) * [Syslog](#syslog) * [Syslog show commands](#syslog-show-commands) * [Syslog config commands](#syslog-config-commands) @@ -180,6 +202,7 @@ * [VxLAN show commands](#vxlan-show-commands) * [Vnet](#vnet) * [Vnet show commands](#vnet-show-commands) + * [Vnet config commands](#vnet-config-commands) * [Warm Reboot](#warm-reboot) * [Warm Restart](#warm-restart) * [Warm Restart show commands](#warm-restart-show-commands) @@ -202,14 +225,39 @@ * [MACsec config command](#macsec-config-command) * [MACsec show command](#macsec-show-command) * [MACsec clear command](#macsec-clear-command) +* [SFP Utilities Commands](#sfp-utilities-commands) + * [SFP Utilities show commands](#sfp-utilities-show-commands) + * [SFP Utilities read command](#sfp-utilities-read-command) + * [SFP Utilities write command](#sfp-utilities-write-command) * [Static DNS Commands](#static-dns-commands) * [Static DNS config command](#static-dns-config-command) * [Static DNS show command](#static-dns-show-command) - +* [Wake-on-LAN Commands](#wake-on-lan-commands) + * [Send Wake-on-LAN Magic Packet command](#send-wake-on-lan-magic-packet-command) +* [Banner Commands](#banner-commands) + * [Banner config commands](#banner-config-commands) + * [Banner show command](#banner-show-command) +* [Memory Statistics Commands](#memory-statistics-commands) + * [Overview](#overview) + * [Memory Statistics Config Commands](#memory-statistics-config-commands) + * [Enable/Disable Memory Statistics Monitoring](#enabledisable-memory-statistics-monitoring) + * [Set the Frequency of Memory Data Collection](#set-the-frequency-of-memory-data-collection) + * [Adjust the Data Retention Period](#adjust-the-data-retention-period) + * [Memory Statistics Show Commands](#memory-statistics-show-commands) + * [Default Historical Memory Statistics](#default-historical-memory-statistics) + * [Historical Memory Statistics for Last 10 Days](#historical-memory-statistics-for-last-10-days) + * [Historical Memory Statistics for Last 100 Minutes](#historical-memory-statistics-for-last-100-minutes) + * [Historical Memory Statistics for Last 3 Hours](#historical-memory-statistics-for-last-3-hours) + * [Historical Memory Statistics for Specific Metric (Used Memory)](#historical-memory-statistics-for-specific-metric-used-memory) + * [View Memory Statistics Configuration](#view-memory-statistics-configuration) +* [CoPP Commands](#copp-commands) + * [Overview](#overview) + * [CoPP show commands](#copp-show-commands) ## Document History | Version | Modification Date | Details | | --- | --- | --- | +| v9 | Sep-19-2024 | Add DPU serial console utility | | v8 | Oct-09-2023 | Add CMIS firmware upgrade commands | | v7 | Jun-22-2023 | Add static DNS show and config commands | | v6 | May-06-2021 | Add SNMP show and config commands | @@ -689,11 +737,52 @@ This command displays the cause of the previous reboot ``` - Example: + ### Shown below is the output of the CLI when executed on the NPU ``` admin@sonic:~$ show reboot-cause User issued reboot command [User: admin, Time: Mon Mar 25 01:02:03 UTC 2019] ``` + ### Shown below is the output of the CLI when executed on the DPU + ``` + admin@sonic:~$ show reboot-cause + reboot + ``` +``` +Note: The CLI extensions shown in this block are applicable only to smartswitch platforms. When these extensions are used on a regular switch the extension will be ignored and the output will be the same irrespective of the options. + +CLI Extensions Applicable to Smartswtich + - show reboot-cause all + - show reboot-cause history all + - show reboot-cause history DPUx +``` +**show reboot-cause all** + +This command displays the cause of the previous reboot for the Switch and the DPUs for which the midplane interfaces are up. + +- Usage: + ``` + show reboot-cause all + ``` + +- Example: + ### Shown below is the output of the CLI when executed on the NPU + ``` + root@MtFuji:/home/cisco# show reboot-cause all + Device Name Cause Time User + -------- ------------------- ------------ ------------------------------- ------ + NPU 2025_01_21_09_01_11 Power Loss N/A N/A + DPU1 2025_01_21_09_03_43 Non-Hardware Tue Jan 21 09:03:43 AM UTC 2025 + DPU0 2025_01_21_09_03_37 Non-Hardware Tue Jan 21 09:03:37 AM UTC 2025 + + ``` + ### Shown below is the output of the CLI when executed on the DPU + ``` + root@sonic:/home/admin# show reboot-cause all + Usage: show reboot-cause [OPTIONS] COMMAND [ARGS]... + Try "show reboot-cause -h" for help. + Error: No such command "all". + ``` **show reboot-cause history** This command displays the history of the previous reboots up to 10 entry @@ -704,15 +793,74 @@ This command displays the history of the previous reboots up to 10 entry ``` - Example: + ### Shown below is the output of the CLI when executed on the NPU ``` - admin@sonic:~$ show reboot-cause history - Name Cause Time User Comment - ------------------- ----------- ---------------------------- ------ --------- - 2020_10_09_02_33_06 reboot Fri Oct 9 02:29:44 UTC 2020 admin + root@MtFuji:/home/cisco# show reboot-cause history + Name Cause Time User Comment + ------------------- ---------- ------ ------ ---------------------------------------------------------------------------------- + 2020_10_09_02_40_11 Power Loss Fri Oct 9 02:40:11 UTC 2020 N/A Unknown (First boot of SONiC version azure_cisco_master.308-dirty-20250120.220704) 2020_10_09_01_56_59 reboot Fri Oct 9 01:53:49 UTC 2020 admin - 2020_10_09_02_00_53 fast-reboot Fri Oct 9 01:58:04 UTC 2020 admin - 2020_10_09_04_53_58 warm-reboot Fri Oct 9 04:51:47 UTC 2020 admin ``` + ### Shown below is the output of the CLI when executed on the DPU + ``` + root@sonic:/home/admin# show reboot-cause history + Name Cause Time User Comment + ------------------- ------- ------------------------------- ------ --------- + 2025_01_21_16_49_20 Unknown N/A N/A N/A + 2025_01_17_11_25_58 reboot Fri Jan 17 11:23:24 AM UTC 2025 admin N/A + ``` +**show reboot-cause history all** + +This command displays the history of the previous reboots up to 10 entry of the Switch and the DPUs for which the midplane interfaces are up. + +- Usage: + ``` + show reboot-cause history all + ``` + +- Example: + ### Shown below is the output of the CLI when executed on the NPU + ``` + root@MtFuji:~# show reboot-cause history all + Device Name Cause Time User Comment + -------- ------------------- ----------------------------------------- ------------------------------- ------ ------- + NPU 2024_07_23_23_06_57 Kernel Panic Tue Jul 23 11:02:27 PM UTC 2024 N/A N/A + NPU 2024_07_23_11_21_32 Power Loss N/A N/A Unknown + ``` + ### Shown below is the output of the CLI when executed on the DPU + ``` + root@sonic:/home/admin# show reboot-cause history all + Usage: show reboot-cause history [OPTIONS] + Try "show reboot-cause history -h" for help. + + Error: Got unexpected extra argument (all) + ``` +**show reboot-cause history DPU1** + +This command displays the history of the previous reboots up to 10 entry of DPU1. If DPU1 is powered down then there won't be any data in the DB and the "show reboot-cause history DPU1" output will be blank. + +- Usage: + ``` + show reboot-cause history DPU1 + ``` + +- Example: + ### Shown below is the output of the CLI when executed on the NPU + ``` + root@MtFuji:~# show reboot-cause history DPU1 + Device Name Cause Time User Comment + -------- ------ ----------------------------------------- ------ ------ --------- + DPU1 DPU1 Software causes (Hardware watchdog reset) N/A N/A N/A + ``` + ### Shown below is the output of the CLI when executed on the DPU + ``` + root@sonic:/home/admin# show reboot-cause history DPU1 + Usage: show reboot-cause history [OPTIONS] + Try "show reboot-cause history -h" for help. + + Error: Got unexpected extra argument (DPU1) + ``` + **show uptime** @@ -1922,6 +2070,158 @@ This command is used to display: ACL rules, tables and their priority, ACL packe If the `PACKETS COUNT` and `BYTES COUNT` fields have some numeric value it means that it is a SONiC ACL's and those counters are created in SONiC `COUNTERS_DB`. +## ASIC SDK health event + +### ASIC SDK health event config commands + +**config asic-sdk-health-event suppress ** + +This command is for a customer to configure the categories that he/she wants to suppress for a certain severity. + +- Usage: + ``` + config config asic-sdk-health-event suppress [--category-list ||] [--max-events ] + ``` + + - Parameters: + - severity: Specify the severity whose ASIC/SDK health events to be suppressed. It can be one of `fatal`, `warning`, and `notice`. + - category-list: Specify the categories from which the ASIC/SDK health events to be suppressed. It is a list whose element is one of `software`, `firmware`, `cpu_hw`, `asic_hw` separated by a comma. + If the category-list is `none`, none category is suppressed and all the categories will be notified for `severity`. In this case, it will not be stored in the CONFIG_DB. + If the category-list is `all`, all the categories are suppressed and none category will be notified for `severity`. + - max-events: Specify the maximum number of events of the severity to be stored in the STATE_DB. + There is no limitation if the max-events is 0. In this case, it will not be stored in the CONFIG_DB. + +- Examples: + ``` + admin@sonic:~$ sudo config asic-sdk-health-event suppress fatal --category-list cpu_hw,software --max-events 10240 + ``` + + This command will suppress ASIC/SDK health events whose severity is fatal and cagetory is cpu_hw or software. Maximum number of such events in the STATE_DB is 10240. + +### ASIC SDK health event show commands + +**show asic-sdk-health-event received** + +This command displays the received ASIC/SDK health events. + +- Usage: + ``` + show asic-sdk-health-event received [-n ] + ``` + +- Details: + - show asic-sdk-health-event received: Display the ASIC/SDK health events received on all ASICs + - show asic-sdk-health-event received -n asic0: Display all the ASIC/SDK health events received on asic0 + + +- Example: + ``` + admin@sonic:~$ show asic-sdk-health-event received + Time Severity Category Description + ------------------- ----------- --------- ----------------- + 2023-10-20 05:07:34 fatal firmware Command timeout + 2023-10-20 03:06:25 fatal software SDK daemon keep alive failed + 2023-10-20 05:07:34 fatal asic_hw Uncorrectable ECC error + 2023-10-20 01:58:43 notice asic_hw Correctable ECC error + ``` + +- Example on a multi ASIC system: + ``` + admin@sonic:~$ show asic-sdk-health-event received + asic0: + Time Severity Category Description + ------------------- ----------- --------- ----------------- + 2023-10-20 05:07:34 fatal firmware Command timeout + 2023-10-20 03:06:25 fatal software SDK daemon keep alive failed + asic1: + Time Severity Category Description + ------------------- ----------- --------- ----------------- + 2023-10-20 05:07:34 fatal asic_hw Uncorrectable ECC error + 2023-10-20 01:58:43 notice asic_hw Correctable ECC error + ``` + +Optionally, you can specify the asic name in order to display the ASIC/SDK health events received on that particular ASIC on a multi ASIC system + +- Example: + ``` + admin@sonic:~$ show asic-sdk-health-event received -n asic1 + asic1: + Time Severity Category Description + ------------------- ----------- --------- ----------------- + 2023-10-20 05:07:34 fatal firmware Command timeout + ``` + +**show asic-sdk-health-event suppress-configuration** + +This command displays the suppressed category list and maximum number of events of ASIC/SDK health events. + +- Usage: + ``` + show asic-sdk-health-event suppressed-category-list [-n ] + ``` + +- Details: + - show asic-sdk-health-event suppress-configuration: Display the ASIC/SDK health event suppress category list and maximum number of events on all ASICs + - show asic-sdk-health-event suppress-configuration -n asic0: Display all the ASIC/SDK health event suppress category list and maximum number of events on asic0 + + +- Example: + ``` + admin@sonic:~$ show asic-sdk-health-event suppress-configuration + Severity Suppressed category-list Max events + ---------- -------------------------- ------------ + fatal software unlimited + notice none 1024 + warning firmware,asic_hw 10240 + ``` + +- Example on a multi ASIC system: + ``` + admin@sonic:~$ show asic-sdk-health-event suppress-configuration + asic0: + Severity Suppressed category-list Max events + ---------- -------------------------- ------------ + notice none 1024 + warning firmware,asic_hw 10240 + asic1: + Severity Suppressed category-list Max events + ---------- -------------------------- ------------ + fatal software unlimited + ``` + +Optionally, you can specify the asic name in order to display the ASIC/SDK health event suppress category list on that particular ASIC on a multi ASIC system + +- Example: + ``` + admin@sonic:~$ show asic-sdk-health-event suppress-configuration -n asic1 + asic1: + Severity Suppressed category-list Max events + ---------- -------------------------- ------------ + fatal software unlimited + ``` + +### ASIC SDK health event clear commands + +**sonic-clear asic-sdk-health-event** + +This command clears all the received ASIC/SDK health events. + +- Usage: + ``` + sonic-clear asic-sdk-health-event [-n ] + ``` + +- Details: + - sonic-clear asic-sdk-health-event: Clear the ASIC/SDK health events received on all ASICs + - sonic-clear asic-sdk-health-event -n asic0: Display all the ASIC/SDK health events received on asic0 + + +- Example: + ``` + admin@sonic:~$ sonic-clear asic-sdk-health-event + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#asic-sdk-health-event) ## ARP & NDP @@ -2040,7 +2340,7 @@ This command displays the state and key parameters of all BFD sessions. - Usage: ``` - show bfd summary + show bfd summary [-n ] ``` - Example: ``` @@ -2059,7 +2359,7 @@ This command displays the state and key parameters of all BFD sessions that matc - Usage: ``` - show bfd peer + show bfd peer [-n ] ``` - Example: ``` @@ -2252,8 +2552,7 @@ Optionally, you can specify an IP address in order to display only that particul Click [here](#Quagga-BGP-Show-Commands) to see the example for "show ip bgp neighbors" for Quagga. - -**show ip bgp network [[|] [(bestpath | multipath | longer-prefixes | json)]] +**show ip bgp network [[|] [(bestpath | multipath | longer-prefixes | json)]]** This command displays all the details of IPv4 Border Gateway Protocol (BGP) prefixes. @@ -2461,6 +2760,26 @@ When enabled, BGP will not advertise routes which aren't yet offloaded. Disabled ``` +**show bgp device-global** + +This command displays BGP device global configuration. + +- Usage: + ```bash + show bgp device-global + ``` + +- Options: + - _-j,--json_: display in JSON format + +- Example: + ```bash + admin@sonic:~$ show bgp device-global + TSA W-ECMP + ------- ------- + enabled enabled + ``` + Go Back To [Beginning of the document](#) or [Beginning of this section](#bgp) ### BGP config commands @@ -2571,6 +2890,26 @@ Once enabled, BGP will not advertise routes which aren't yet offloaded. admin@sonic:~$ sudo config suppress-fib-pending disabled ``` +**config bgp device-global tsa/w-ecmp** + +This command is used to manage BGP device global configuration. + +Feature list: +1. TSA - Traffic-Shift-Away +2. W-ECMP - Weighted-Cost Multi-Path + +- Usage: + ```bash + config bgp device-global tsa + config bgp device-global w-ecmp + ``` + +- Examples: + ```bash + admin@sonic:~$ config bgp device-global tsa enabled + admin@sonic:~$ config bgp device-global w-ecmp enabled + ``` + Go Back To [Beginning of the document](#) or [Beginning of this section](#bgp) ## Console @@ -2614,7 +2953,7 @@ Optionally, you can display configured console ports only by specifying the `-b` 1 9600 Enabled - - switch1 ``` -## Console config commands +### Console config commands This sub-section explains the list of configuration options available for console management module. @@ -2790,6 +3129,88 @@ Optionally, you can clear with a remote device name by specifying the `-d` or `- Go Back To [Beginning of the document](#) or [Beginning of this section](#console) +### DPU serial console utility + +**dpu-tty.py** + +This command allows user to connect to a DPU serial console via TTY device with +interactive CLI program: picocom. The configuration is from platform.json. The +utility works only on smart switch that provides DPU UART connections through +/dev/ttyS* devices. + +- Usage: + ``` + dpu-tty.py (-n|--name) [(-b|-baud) ] [(-t|-tty) ] + ``` + +- Example: + ``` + root@MtFuji:/home/cisco# dpu-tty.py -n dpu0 + picocom v3.1 + + port is : /dev/ttyS4 + flowcontrol : none + baudrate is : 115200 + parity is : none + databits are : 8 + stopbits are : 1 + escape is : C-a + local echo is : no + noinit is : no + noreset is : no + hangup is : no + nolock is : no + send_cmd is : sz -vv + receive_cmd is : rz -vv -E + imap is : + omap is : + emap is : crcrlf,delbs, + logfile is : none + initstring : none + exit_after is : not set + exit is : no + + Type [C-a] [C-h] to see available commands + Terminal ready + + sonic login: admin + Password: + Linux sonic 6.1.0-11-2-arm64 #1 SMP Debian 6.1.38-4 (2023-08-08) aarch64 + You are on + ____ ___ _ _ _ ____ + / ___| / _ \| \ | (_)/ ___| + \___ \| | | | \| | | | + ___) | |_| | |\ | | |___ + |____/ \___/|_| \_|_|\____| + + -- Software for Open Networking in the Cloud -- + + Unauthorized access and/or use are prohibited. + All access and/or use are subject to monitoring. + + Help: https://sonic-net.github.io/SONiC/ + + Last login: Mon Sep 9 21:39:44 UTC 2024 on ttyS0 + admin@sonic:~$ + Terminating... + Thanks for using picocom + root@MtFuji:/home/cisco# + ``` + +Optionally, user may overwrite baud rate for experiment. + +- Example: + ``` + root@MtFuji:/home/cisco# dpu-tty.py -n dpu1 -b 9600 + ``` + +Optionally, user may overwrite TTY device for experiment. + +- Example: + ``` + root@MtFuji:/home/cisco# dpu-tty.py -n dpu2 -t ttyS4 + ``` + ## CMIS firmware upgrade ### CMIS firmware version show commands @@ -2923,6 +3344,66 @@ Example of the module supporting target mode Target Mode set to 1 ``` +## CMIS debug + +### CMIS debug loopback + +This command is the standard CMIS diagnostic control used for troubleshooting link and performance issues between the host switch and transceiver module. + +**sfputil debug loopback** + +- Usage: + ``` + sfputil debug loopback PORT_NAME LOOPBACK_MODE + + Valid values for loopback mode + host-side-input: host side input loopback mode + host-side-output: host side output loopback mode + media-side-input: media side input loopback mode + media-side-output: media side output loopback mode + ``` + +- Example: + ``` + admin@sonic:~$ sfputil debug loopback Ethernet88 host-side-input enable + admin@sonic:~$ sfputil debug loopback Ethernet88 media-side-output disable + ``` + +### CMIS debug rx-output + +The command disables RX input by muting the optical receiver on the module, preventing it from detecting incoming signals. + +**sfputil debug rx-ouput** + +- Usage: + ``` + sfputil debug rx-output PORT_NAME + + ``` + +- Example: + ``` + admin@sonic:~$ sfputil debug rx-output Ethernet88 enable + admin@sonic:~$ sfputil debug rx-output Ethernet88 disable + ``` + +### CMIS debug tx-output + +The command disables TX output by turning off the laser on the module, effectively blocking the optical signal. + +**sfputil debug tx-ouput** + +- Usage: + ``` + sfputil debug tx-output PORT_NAME + + ``` + +- Example: + ``` + admin@sonic:~$ sfputil debug tx-output Ethernet88 enable + ``` + ## DHCP Relay ### DHCP Relay show commands @@ -3838,6 +4319,21 @@ This command sets the number of consecutive polls in which no error is detected admin@sonic:~$ config fabric port monitor poll threshold recovery 5 -n asic0 ``` +**config fabric port monitor state ** + +This command sets the monitor state in CONFIG_DB to enable/disable the fabric monitor feature. + +- Usage: + ``` + config fabric port monitor state [OPTIONS] + ``` + +- Example: + ``` + admin@sonic:~$ config fabric port monitor state enable + admin@sonic:~$ config fabric port monitor state disable + ``` + ## Feature SONiC includes a capability in which Feature state can be enabled/disabled @@ -4246,34 +4742,134 @@ This command displays switch hash global configuration. show switch-hash global ``` +- Options: + - _-j,--json_: display in JSON format + - 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 | Configuration | + +========+=====================================+ + | ECMP | +-------------------+-------------+ | + | | | Hash Field | Algorithm | | + | | |-------------------+-------------| | + | | | DST_MAC | CRC | | + | | | 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 | | | + | | | IPV6_FLOW_LABEL | | | + | | +-------------------+-------------+ | + +--------+-------------------------------------+ + | LAG | +-------------------+-------------+ | + | | | Hash Field | Algorithm | | + | | |-------------------+-------------| | + | | | DST_MAC | CRC | | + | | | 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 | | | + | | | IPV6_FLOW_LABEL | | | + | | +-------------------+-------------+ | + +--------+-------------------------------------+ + ``` + +**show switch-hash capabilities** + +This command displays switch hash capabilities. + +- Usage: + ```bash + show switch-hash capabilities + ``` + +- Options: + - _-j,--json_: display in JSON format + +- Example: + ```bash + admin@sonic:~$ show switch-hash capabilities + +--------+-------------------------------------+ + | Hash | Capabilities | + +========+=====================================+ + | ECMP | +-------------------+-------------+ | + | | | Hash Field | Algorithm | | + | | |-------------------+-------------| | + | | | IN_PORT | CRC | | + | | | DST_MAC | XOR | | + | | | SRC_MAC | RANDOM | | + | | | ETHERTYPE | CRC_32LO | | + | | | VLAN_ID | CRC_32HI | | + | | | IP_PROTOCOL | CRC_CCITT | | + | | | DST_IP | CRC_XOR | | + | | | 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 | | | + | | | IPV6_FLOW_LABEL | | | + | | +-------------------+-------------+ | + +--------+-------------------------------------+ + | LAG | +-------------------+-------------+ | + | | | Hash Field | Algorithm | | + | | |-------------------+-------------| | + | | | IN_PORT | CRC | | + | | | DST_MAC | XOR | | + | | | SRC_MAC | RANDOM | | + | | | ETHERTYPE | CRC_32LO | | + | | | VLAN_ID | CRC_32HI | | + | | | IP_PROTOCOL | CRC_CCITT | | + | | | DST_IP | CRC_XOR | | + | | | 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 | | | + | | | IPV6_FLOW_LABEL | | | + | | +-------------------+-------------+ | + +--------+-------------------------------------+ ``` ### Hash Config Commands This subsection explains how to configure switch hash. -**config switch-hash global** +**config switch-hash global ecmp/lag hash** This command is used to manage switch hash global configuration. @@ -4304,7 +4900,8 @@ This command is used to manage switch hash global configuration. 'INNER_DST_IP' \ 'INNER_SRC_IP' \ 'INNER_L4_DST_PORT' \ - 'INNER_L4_SRC_PORT' + 'INNER_L4_SRC_PORT' \ + 'IPV6_FLOW_LABEL' admin@sonic:~$ config switch-hash global lag-hash \ 'DST_MAC' \ 'SRC_MAC' \ @@ -4321,7 +4918,27 @@ This command is used to manage switch hash global configuration. 'INNER_DST_IP' \ 'INNER_SRC_IP' \ 'INNER_L4_DST_PORT' \ - 'INNER_L4_SRC_PORT' + 'INNER_L4_SRC_PORT' \ + 'IPV6_FLOW_LABEL' + ``` + +**config switch-hash global ecmp/lag hash algorithm** + +This command is used to manage switch hash algorithm global configuration. + +- Usage: + ```bash + config switch-hash global ecmp-hash-algorithm + config switch-hash global lag-hash-algorithm + ``` + +- Parameters: + - _hash_algorithm_: hash algorithm for hashing packets going through ECMP/LAG + +- Examples: + ```bash + admin@sonic:~$ config switch-hash global ecmp-hash-algorithm 'CRC' + admin@sonic:~$ config switch-hash global lag-hash-algorithm 'CRC' ``` ## Interfaces @@ -4350,6 +4967,7 @@ Subsequent pages explain each of these commands in detail. neighbor Show neighbor related information portchannel Show PortChannel information status Show Interface status information + switchport Show Interface switchport information tpid Show Interface tpid information transceiver Show SFP Transceiver information ``` @@ -4430,8 +5048,8 @@ The "current-mode" subcommand is used to display current breakout mode for all i **show interfaces counters** -This show command displays packet counters for all interfaces since the last time the counters were cleared. To display l3 counters "rif" subcommand can be used. There is no facility to display counters for one specific l2 interface. For l3 interfaces a single interface output mode is present. Optional argument "-a" provides two additional columns - RX-PPS and TX_PPS. -Optional argument "-p" specify a period (in seconds) with which to gather counters over. +This show command displays packet counters for all interfaces(except the "show interface detailed" command) since the last time the counters were cleared. To display l3 counters "rif" subcommand can be used. There is no facility to display counters for one specific l2 interface. For l3 interfaces a single interface output mode is present. Optional argument "-a" provides two additional columns - RX-PPS and TX_PPS. +Optional argument "-p" specify a period (in seconds) with which to gather counters over. To display the detailed per-interface counters "detailed " subcommand can be used. - Usage: ``` @@ -4439,6 +5057,10 @@ Optional argument "-p" specify a period (in seconds) with which to gather counte show interfaces counters errors show interfaces counters rates show interfaces counters rif [-p|--period ] [-i ] + show interfaces counters fec-histogram [-i ] + show interfaces counters fec-stats + show interfaces counters detailed + show interfaces counters trim [interface_name] [-p|--period ] [-j|--json] ``` - Example: @@ -4544,6 +5166,56 @@ Optionally, you can specify a period (in seconds) with which to gather counters Ethernet24 U 173 16.09 KB/s 0.00% 0 0 0 169 11.39 KB/s 0.00% 0 0 0 ``` +The "detailed" subcommand is used to display more detailed interface counters. Along with tx/rx counters, it also displays the WRED drop counters that are supported on the platform. + +- Example: + ``` + admin@sonic:~$ show interfaces counters detailed Ethernet8 + Packets Received 64 Octets..................... 0 + Packets Received 65-127 Octets................. 0 + Packets Received 128-255 Octets................ 0 + Packets Received 256-511 Octets................ 0 + Packets Received 512-1023 Octets............... 0 + Packets Received 1024-1518 Octets.............. 0 + Packets Received 1519-2047 Octets.............. 0 + Packets Received 2048-4095 Octets.............. 0 + Packets Received 4096-9216 Octets.............. 0 + Packets Received 9217-16383 Octets............. 0 + + Total Packets Received Without Errors.......... 0 + Unicast Packets Received....................... 0 + Multicast Packets Received..................... 0 + Broadcast Packets Received..................... 0 + + Jabbers Received............................... N/A + Fragments Received............................. N/A + Undersize Received............................. 0 + Overruns Received.............................. 0 + + Packets Transmitted 64 Octets.................. 0 + Packets Transmitted 65-127 Octets.............. 0 + Packets Transmitted 128-255 Octets............. 0 + Packets Transmitted 256-511 Octets............. 0 + Packets Transmitted 512-1023 Octets............ 0 + Packets Transmitted 1024-1518 Octets........... 0 + Packets Transmitted 1519-2047 Octets........... 0 + Packets Transmitted 2048-4095 Octets........... 0 + Packets Transmitted 4096-9216 Octets........... 0 + Packets Transmitted 9217-16383 Octets.......... 0 + + Total Packets Transmitted Successfully......... 0 + Unicast Packets Transmitted.................... 0 + Multicast Packets Transmitted.................. 0 + Broadcast Packets Transmitted.................. 0 + + WRED Green Dropped Packets..................... 0 + WRED Yellow Dropped Packets.................... 0 + WRED Red Dropped Packets....................... 0 + WRED Total Dropped Packets..................... 0 + + Time Since Counters Last Cleared............... None + ``` + - NOTE: Interface counters can be cleared by the user with the following command: ``` @@ -4556,11 +5228,63 @@ Optionally, you can specify a period (in seconds) with which to gather counters admin@sonic:~$ sonic-clear rifcounters ``` -**show interfaces description** +The "fec-histogram" subcommand is used to display the fec histogram for the port. -This command displays the key fields of the interfaces such as Operational Status, Administrative Status, Alias and Description. +When data is transmitted, it's broken down into units called codewords. FEC algorithms add extra data to each codeword that can be used to detect and correct errors in transmission. +In a FEC histogram, "bins" represent ranges of errors or specific categories of errors. For instance, Bin0 might represent codewords with no errors, while Bin1 could represent codewords with a single bit error, and so on. The histogram shows how many codewords fell into each bin. A high number in the higher bins might indicate a problem with the transmission link, such as signal degradation. -- Usage: +- Example: + ``` + admin@str-s6000-acs-11:/usr/bin$ show interface counters fec-histogram -i + Symbol Errors Per Codeword Codewords + -------------------------- --------- + BIN0: 1000000 + BIN1: 900000 + BIN2: 800000 + BIN3: 700000 + BIN4: 600000 + BIN5: 500000 + BIN6: 400000 + BIN7: 300000 + BIN8: 0 + BIN9: 0 + BIN10: 0 + BIN11: 0 + BIN12: 0 + BIN13: 0 + BIN14: 0 + BIN15: 0 + ``` + +The "fec-stats" subcommand is used to disply the interface fec related statistic. + +- Example: + ``` + admin@ctd615:~$ show interfaces counters fec-stats + IFACE STATE FEC_CORR FEC_UNCORR FEC_SYMBOL_ERR FEC_PRE_BER FEC_POST_BER + ----------- ------- ---------- ------------ ---------------- ------------- -------------- + Ethernet0 U 0 0 0 1.48e-20 0.00e+00 + Ethernet8 U 0 0 0 1.98e-19 0.00e+00 + Ethernet16 U 0 0 0 1.77e-20 0.00e+00 + ``` + +The "trim" subcommand is used to display the interface packet trimming related statistic. + +- Example: + ``` + admin@sonic:~$ show interfaces counters trim + IFACE STATE TRIM_PKTS + ---------- ------- ----------- + Ethernet0 U 0 + Ethernet8 U 100 + Ethernet16 U 200 + ``` + +**show interfaces description** + +This command displays the key fields of the interfaces such as Operational Status, Administrative Status, Alias and Description. + +- Usage: ``` show interfaces description [] ``` @@ -4637,6 +5361,54 @@ This command is to display the link-training status of the selected interfaces. Ethernet8 trained on up up ``` +**show interfaces flap** + +The show interfaces flap command provides detailed insights into interface events, including the timestamp of the last link down event and the total flap count (number of times the link has gone up and down). This helps in diagnosing stability and connectivity issues. + +- Usage: + ``` + show interfaces flap + ``` +- Example: + ``` + admin@sonic:~$ show interfaces flap + Interface Flap Count Admin Oper Link Down TimeStamp (UTC) Link Up TimeStamp (UTC) + ----------- ------------ ------- ------ -------------------------------------------------------- ----------------------------------------------------------- + Ethernet0 5 Up Up Last flapped : 2024-10-01 10:00:00 (0 days 00:01:23 ago) Last Link up: 2024-09-30 10:01:03 UTC (1 days 02:30:15 ago) + Ethernet4 Never Up Up Never Last Link up: 2024-09-30 10:01:03 UTC (1 days 02:30:15 ago) + Ethernet8 1 Up Up Last flapped : 2024-10-01 10:01:00 (0 days 00:00:23 ago) Last Link up: 2024-10-02 10:01:03 UTC (5 days 02:30:15 ago) + ``` +**show interfaces errors** + +The show interface errors command provides detailed statistics and error counters for MAC-level operations on an interface. It displays the status of various operational parameters, error counts, and timestamps for when these errors occurred. + +- Usage: + ``` + show interfaces errors [] + ``` + +- Example: + ``` + admin@sonic:~$ show interfaces errors Ethernet4 + Port Errors Count Last timestamp(UTC) + ---------------------------------- ----- ------------------- + oper_error_status 5442 2024-11-02 04:00:05 + mac_local_fault 2 2024-11-02 04:00:05 + fec_sync_loss 2 2024-11-02 04:00:05 + fec_alignment_loss 2 2024-11-02 04:00:05 + high_ser_error 2 2024-11-02 04:00:05 + high ber_error 2 2024-11-02 04:00:05 + data_unit_crc_error 2 2024-11-02 04:00:05 + data_unit_misalignment_error 2 2024-11-02 04:00:05 + signal_local_error 2 2024-11-02 04:00:05 + mac_remote_fault 2 2024-11-02 04:00:50 + crc_rate 2 2024-11-02 04:00:50 + data_unit_size 2 2024-11-02 04:00:50 + code_group_error 0 Never + no_rx_reachability 0 Never + ``` + + **show interfaces mpls** This command is used to display the configured MPLS state for the list of configured interfaces. @@ -4739,7 +5511,7 @@ This command is used to display the list of expected neighbors for all interface - Usage: ``` - show interfaces neighbor expected [] + show interfaces neighbor expected [] -n [] ``` - Example: @@ -4815,6 +5587,53 @@ This command displays some more fields such as Lanes, Speed, MTU, Type, Asymmetr Ethernet180 105,106,107,108 100G 9100 hundredGigE46 down down N/A N/A ``` + +**show interface switchport status** + +This command displays switchport modes status of the interfaces + +- Usage: + ``` + show interfaces switchport status + ``` + +- Example (show interface switchport status of all interfaces): + ``` + admin@sonic:~$ show interfaces switchport status + Interface Mode + ----------- -------- + Ethernet0 access + Ethernet4 trunk + Ethernet8 routed + + ``` + +**show interface switchport config** + +This command displays switchport modes configuration of the interfaces + +- Usage: + ``` + show interfaces switchport config + ``` + +- Example (show interface switchport config of all interfaces): + ``` + admin@sonic:~$ show interfaces switchport config + Interface Mode Untagged Tagged + ----------- -------- -------- ------- + Ethernet0 access 2 + Ethernet4 trunk 3 4,5,6 + Ethernet8 routed + + ``` + + +For details please refer [Switchport Mode HLD](https://github.com/sonic-net/SONiC/pull/912/files#diff-03597c34684d527192f76a6e975792fcfc83f54e20dde63f159399232d148397) to know more about th +is command. + + + **show interfaces transceiver** This command is already explained [here](#Transceivers) @@ -5160,6 +5979,22 @@ This command is used to reset an SFP transceiver Resetting port Ethernet0... OK ``` +**config interface transceiver dom** + +This command is used to configure the Digital Optical Monitoring (DOM) for an interface. + +- Usage: + ``` + config interface transceiver dom (enable | disable) + ``` + +- Examples: + ``` + user@sonic~$ sudo config interface transceiver dom Ethernet0 enable + + user@sonic~$ sudo config interface transceiver dom Ethernet0 disable + ``` + **config interface mtu (Versions >= 201904)** This command is used to configure the mtu for the Physical interface. Use the value 1500 for setting max transfer unit size to 1500 bytes. @@ -5515,8 +6350,8 @@ Go Back To [Beginning of the document](#) or [Beginning of this section](#interf **config interface vrf bind** -This command is used to bind a interface to a vrf. -By default, all L3 interfaces will be in default vrf. Above vrf bind command will let users bind interface to a vrf. +This command is used to bind a interface to a vrf as well as vnet. +By default, all L3 interfaces will be in default vrf. Above vrf bind command will let users bind interface to a vrf/vnet. - Usage: ``` @@ -5525,8 +6360,8 @@ By default, all L3 interfaces will be in default vrf. Above vrf bind command wil **config interface vrf unbind** -This command is used to ubind a interface from a vrf. -This will move the interface to default vrf. +This command is used to ubind a interface from a vrf/vnet. +This will move the interface to default vrf/vnet. - Usage: ``` @@ -5940,6 +6775,86 @@ This command displays the kubernetes server status. ``` Go Back To [Beginning of the document](#) or [Beginning of this section](#Kubernetes) +## LDAP + +### show LDAP global commands + +This command displays the global LDAP configuration that includes the following parameters: base_dn, bind_password, bind_timeout, version, port, timeout. + +- Usage: + ``` + show ldap global + ``` +- Example: + + ``` + admin@sonic:~$ show ldap global + base-dn Ldap user base dn + bind-dn LDAP global bind dn + bind-password Shared secret used for encrypting the communication + bind-timeout Ldap bind timeout <0-120> + port TCP port to communicate with LDAP server <1-65535> + timeout Ldap timeout duration in sec <1-60> + version Ldap version <1-3> + + ``` + +### LDAP global config commands + +These commands are used to configure the LDAP global parameters + + - Usage: + ``` + config ldap global + ``` +- Example: + ``` + admin@sonic:~$ config ldap global + + host
--prio <1 - 8> + base-dn Ldap user base dn + bind-dn LDAP global bind dn + bind-password Shared secret used for encrypting the communication + bind-timeout Ldap bind timeout <0-120> + port TCP port to communicate with LDAP server <1-65535> + timeout Ldap timeout duration in sec <1-60> + version Ldap version <1-3> + ``` + +### show LDAP server commands + +This command displays the global LDAP configuration that includes the following parameters: base_dn, bind_password, bind_timeout, version, port, timeout. + +- Usage: + ``` + show ldap-server + ``` +- Example: + + ``` + admin@sonic:~$ show ldap-server + hostname Ldap hostname or IP of the configured LDAP server + priority priority for the relevant LDAP server <1-8> + ``` + +### LDAP server config commands + +These commands are used to manage the LDAP servers in the system, they are created in correspondance to the global config parameters mentioned earlier. + + - Usage: + ``` + config ldap-server + ``` +- Example: + ``` + admin@sonic:~$ config ldap-server + + add Add a new LDAP server --priority <1-8> + delete Delete an existing LDAP server from the list --priority <1-8> + update Update and existing LDAP server + +Go Back To [Beginning of the document](#) or [Beginning of this section](#LDAP) + ## Linux Kernel Dump This section demonstrates the show commands and configuration commands of Linux kernel dump mechanism in SONiC. @@ -7194,6 +8109,140 @@ While adding a new SPAN session, users need to configure the following fields th Go Back To [Beginning of the document](#) or [Beginning of this section](#mirroring) +## MMU + +### MMU Show commands + +This subsection explains how to display switch Memory Management Unit (MMU) configuration. + +**show mmu** + +This command displays MMU configuration. + +- Usage: + ```bash + show mmu [OPTIONS] + ``` + +- Options: + - _-n,--namespace_: namespace name or all + - _-vv,--verbose_: enable verbose output + +- Example: + ```bash + admin@sonic:~$ show mmu + Pool: ingress_lossless_pool + ---- -------- + xoff 4194112 + type ingress + mode dynamic + size 10875072 + ---- -------- + + Pool: egress_lossless_pool + ---- -------- + type egress + mode static + size 15982720 + ---- -------- + + Pool: egress_lossy_pool + ---- ------- + type egress + mode dynamic + size 9243812 + ---- ------- + + Profile: egress_lossy_profile + ---------- ------------------------------- + dynamic_th 3 + pool [BUFFER_POOL|egress_lossy_pool] + size 1518 + ---------- ------------------------------- + + Profile: pg_lossless_100000_300m_profile + ---------- ----------------------------------- + xon_offset 2288 + dynamic_th -3 + xon 2288 + xoff 268736 + pool [BUFFER_POOL|ingress_lossless_pool] + size 1248 + ---------- ----------------------------------- + + Profile: egress_lossless_profile + --------- ---------------------------------- + static_th 3995680 + pool [BUFFER_POOL|egress_lossless_pool] + size 1518 + --------- ---------------------------------- + + Profile: pg_lossless_100000_40m_profile + ---------- ----------------------------------- + xon_offset 2288 + dynamic_th -3 + xon 2288 + xoff 177632 + pool [BUFFER_POOL|ingress_lossless_pool] + size 1248 + ---------- ----------------------------------- + + Profile: ingress_lossy_profile + ---------- ----------------------------------- + dynamic_th 3 + pool [BUFFER_POOL|ingress_lossless_pool] + size 0 + ---------- ----------------------------------- + + Profile: pg_lossless_40000_40m_profile + ---------- ----------------------------------- + xon_offset 2288 + dynamic_th -3 + xon 2288 + xoff 71552 + pool [BUFFER_POOL|ingress_lossless_pool] + size 1248 + ---------- ----------------------------------- + + Profile: q_lossy_profile + --------------------- ----------------- + packet_discard_action drop + dynamic_th 0 + pool egress_lossy_pool + size 0 + --------------------- ----------------- + ``` + +### MMU Config commands + +This subsection explains how to configure switch Memory Management Unit (MMU). + +**config mmu** + +This command is used to manage switch MMU configuration. + +- Usage: + ```bash + config mmu [OPTIONS] + ``` + +- Options: + - _-p_: profile name + - _-a_: set alpha for profile type dynamic + - _-s_: set staticth for profile type static + - _-t_: set packet trimming eligibility + - _-n,--namespace_: namespace name or all + - _-vv,--verbose_: enable verbose output + +- Examples: + ```bash + config mmu -p alpha_profile -a 2 + config mmu -p ingress_lossless_profile -s 12121215 + config mmu -p q_lossy_profile -t on + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#mmu) + ## NAT ### NAT Show commands @@ -7976,74 +9025,11 @@ Go Back To [Beginning of the document](#) or [Beginning of this section](#platfo ### Mellanox Platform Specific Commands -There are few commands that are platform specific. Mellanox has used this feature and implemented Mellanox specific commands as follows. - -**show platform mlnx sniffer** - -This command shows the SDK sniffer status - -- Usage: - ``` - show platform mlnx sniffer - ``` - -- Example: - ``` - admin@sonic:~$ show platform mlnx sniffer - sdk sniffer is disabled - ``` - -**show platform mlnx sniffer** - -Another show command available on ‘show platform mlnx’ which is the issu status. -This means if ISSU is enabled on this SKU or not. A warm boot command can be executed only when ISSU is enabled on the SKU. - -- Usage: - ``` - show platform mlnx issu - ``` - -- Example: - ``` - admin@sonic:~$ show platform mlnx issu - ISSU is enabled - ``` - -In the case ISSU is disabled and warm-boot is called, the user will get a notification message explaining that the command cannot be invoked. - -- Example: - ``` - admin@sonic:~$ sudo warm-reboot - ISSU is not enabled on this HWSKU - Warm reboot is not supported - ``` - -**config platform mlnx** - -This command is valid only on mellanox devices. The sub-commands for "config platform" gets populated only on mellanox platforms. -There are no other subcommands on non-Mellanox devices and hence this command appears empty and useless in other platforms. -The platform mellanox command currently includes a single sub command which is the SDK sniffer. -The SDK sniffer is a troubleshooting tool which records the RPC calls from the Mellanox SDK user API library to the sx_sdk task into a .pcap file. -This .pcap file can be replayed afterward to get the exact same configuration state on SDK and FW to reproduce and investigate issues. - -A new folder will be created to store the sniffer files: "/var/log/mellanox/sniffer/". The result file will be stored in a .pcap file, which includes a time stamp of the starting time in the file name, for example, "sx_sdk_sniffer_20180224081306.pcap" -In order to have a complete .pcap file with all the RPC calls, the user should disable the SDK sniffer. Swss service will be restarted and no capturing is taken place from that moment. -It is recommended to review the .pcap file while sniffing is disabled. -Once SDK sniffer is enabled/disabled, the user is requested to approve that swss service will be restarted. -For example: To change SDK sniffer status, swss service will be restarted, continue? [y/N]: -In order to avoid that confirmation the -y / --yes option should be used. +config platform mlnx -- Usage: - ``` - config platform mlnx sniffer sdk [-y|--yes] - ``` +This command is valid only on mellanox devices. The sub-commands for "config platform" gets populated only on mellanox platforms. There are no other subcommands on non-Mellanox devices and hence this command appears empty and useless in other platforms. -- Example: - ``` - admin@sonic:~$ config platform mlnx sniffer sdk - To change SDK sniffer status, swss service will be restarted, continue? [y/N]: y - NOTE: In order to avoid that confirmation the -y / --yes option should be used. - ``` +The platform mellanox command currently includes no sub command. ### Barefoot Platform Specific Commands @@ -8156,11 +9142,87 @@ This command adds or deletes a member port to/from the already created portchann Go Back To [Beginning of the document](#) or [Beginning of this section](#portchannels) -## NVGRE +# Packet Trimming This section explains the various show commands and configuration commands available for users. -### NVGRE show commands +### Packet Trimming Show commands + +This subsection explains how to display switch trimming configuration. + +**show switch-trimming global** + +This command displays switch trimming global configuration. + +- Usage: + ```bash + show switch-trimming global [OPTIONS] + ``` + +- Options: + - _-j,--json_: display in JSON format + +- Example: + ```bash + admin@sonic:~$ show switch-trimming global + +-----------------------------+---------+ + | Configuration | Value | + +=============================+=========+ + | Packet trimming size | 200 | + +-----------------------------+---------+ + | Packet trimming DSCP value | 20 | + +-----------------------------+---------+ + | Packet trimming queue index | 2 | + +-----------------------------+---------+ + + admin@sonic:~$ show switch-trimming global --json + { + "size": "200", + "dscp_value": "20", + "queue_index": "2" + } + ``` + +### Packet Trimming Config commands + +This subsection explains how to configure switch trimming. + +**config switch-trimming global** + +This command is used to manage switch trimming global configuration. + +- Usage: + ```bash + config switch-trimming global [OPTIONS] + ``` + +- Options: + - _-s,--size_: size (in bytes) to trim eligible packet + - _-d,--dscp_: dscp value assigned to a packet after trimming + - _-q,--queue_: queue index to use for transmission of a packet after trimming + +- Examples: + ```bash + admin@sonic:~$ config switch-trimming global \ + --size '128' \ + --dscp '48' \ + --queue '6' + ``` + +- Note: + - At least one option must be provided + - When `--queue` value is set to `dynamic`, the `--dscp` value is used for mapping to the queue + ```bash + admin@sonic:~$ config switch-trimming global --queue dynamic + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#packet-trimming) + +## NVGRE + +This section explains the various show commands and configuration commands available for users. + +### NVGRE show commands This subsection explains how to display the NVGRE configuration. @@ -8710,6 +9772,7 @@ This sub-section explains the following queue parameters that can be displayed u 2) queue watermark 3) priority-group watermark 4) queue persistent-watermark +5) queue wredcounters **show queue counters** @@ -8719,9 +9782,19 @@ This command can be used to clear the counters for all queues of all ports. Note - Usage: ``` - show queue counters [] + show queue counters [OPTIONS] [interface_name] ``` +- Parameters: + - _interface_name_: display counters for interface name only + +- Options: + - _-a,--all_: display all counters + - _-T,--trim_: display trimming counters only + - _-V,--voq_: display VOQ counters only + - _-nz,--nonzero_: display non zero counters + - _-j,--json_: display counters in JSON format + - Example: ``` admin@sonic:~$ show queue counters @@ -8772,6 +9845,30 @@ This command can be used to clear the counters for all queues of all ports. Note Ethernet4 MC9 0 0 0 0 ... + + admin@sonic:~$ show queue counters --trim + Port TxQ Trim/pkts + --------- ----- ----------- + Ethernet0 UC0 0 + Ethernet0 UC1 0 + Ethernet0 UC2 0 + Ethernet0 UC3 0 + Ethernet0 UC4 0 + Ethernet0 UC5 0 + Ethernet0 UC6 0 + Ethernet0 UC7 0 + Ethernet0 UC8 0 + Ethernet0 UC9 0 + Ethernet0 MC0 N/A + Ethernet0 MC1 N/A + Ethernet0 MC2 N/A + Ethernet0 MC3 N/A + Ethernet0 MC4 N/A + Ethernet0 MC5 N/A + Ethernet0 MC6 N/A + Ethernet0 MC7 N/A + Ethernet0 MC8 N/A + Ethernet0 MC9 N/A ``` Optionally, you can specify an interface name in order to display only that particular interface @@ -8903,6 +10000,83 @@ This command displays the user persistet-watermark for the queues (Egress shared admin@sonic:~$ sonic-clear priority-group drop counters ``` +**show queue wredcounters** + +This command displays wred-drop packet/byte and ecn-marked packet/byte counters for all queues of all ports or one specific-port given as arguement. +This command can be used to clear the counters for all queues of all ports. Note that port specific clear is not supported. + +- Usage: + ``` + show queue wredcounters [] + ``` + +- Example: + ``` + admin@sonic:~$ show queue wredcounters + Port TxQ WredDrp/pkts WredDrp/bytes EcnMarked/pkts EcnMarked/bytes + --------- ----- -------------- --------------- --------------- ---------------- + + Ethernet0 UC0 0 0 0 0 + Ethernet0 UC1 0 0 0 0 + Ethernet0 UC2 0 0 0 0 + Ethernet0 UC3 0 0 0 0 + Ethernet0 UC4 0 0 0 0 + Ethernet0 UC5 0 0 0 0 + Ethernet0 UC6 0 0 0 0 + Ethernet0 UC7 0 0 0 0 + Ethernet0 UC8 0 0 0 0 + Ethernet0 UC9 0 0 0 0 + Ethernet0 MC0 0 0 0 0 + Ethernet0 MC1 0 0 0 0 + Ethernet0 MC2 0 0 0 0 + Ethernet0 MC3 0 0 0 0 + Ethernet0 MC4 0 0 0 0 + Ethernet0 MC5 0 0 0 0 + Ethernet0 MC6 0 0 0 0 + Ethernet0 MC7 0 0 0 0 + Ethernet0 MC8 0 0 0 0 + Ethernet0 MC9 0 0 0 0 + + Port TxQ WredDrp/pkts WredDrp/bytes EcnMarked/pkts EcnMarked/bytes + --------- ----- -------------- --------------- --------------- ---------------- + + Ethernet4 UC0 0 0 0 0 + Ethernet4 UC1 0 0 0 0 + Ethernet4 UC2 0 0 0 0 + Ethernet4 UC3 0 0 0 0 + Ethernet4 UC4 0 0 0 0 + Ethernet4 UC5 0 0 0 0 + Ethernet4 UC6 0 0 0 0 + Ethernet4 UC7 0 0 0 0 + Ethernet4 UC8 0 0 0 0 + Ethernet4 UC9 0 0 0 0 + Ethernet4 MC0 0 0 0 0 + Ethernet4 MC1 0 0 0 0 + Ethernet4 MC2 0 0 0 0 + Ethernet4 MC3 0 0 0 0 + Ethernet4 MC4 0 0 0 0 + Ethernet4 MC5 0 0 0 0 + Ethernet4 MC6 0 0 0 0 + Ethernet4 MC7 0 0 0 0 + Ethernet4 MC8 0 0 0 0 + Ethernet4 MC9 0 0 0 0 + + ... + ``` + +Optionally, you can specify an interface name in order to display only that particular interface + +- Example: + ``` + admin@sonic:~$ show queue wredcounters Ethernet72 + ``` + +- NOTE: Queue counters can be cleared by the user with the following command: + ``` + admin@sonic:~$ sonic-clear queue wredcounters + ``` + + #### Buffer Pool This sub-section explains the following buffer pool parameters that can be displayed using "show buffer_pool" command. @@ -9028,6 +10202,7 @@ Some of the example QOS configurations that users can modify are given below. In this example, it uses the buffers.json.j2 file and qos.json.j2 file from platform specific folders. When there are no changes in the platform specific configutation files, they internally use the file "/usr/share/sonic/templates/buffers_config.j2" and "/usr/share/sonic/templates/qos_config.j2" to generate the configuration. + When an error occurs, such as "Operation not completed successfully, please save and reload configuration," the system will record the status, after executing all the latter commands, exit with code 1. ``` **config qos reload --ports port_list** @@ -9690,6 +10865,7 @@ This sub-section explains the show commands for displaying the running configura 6) acl 7) ports 8) syslog +9) copp **show runningconfiguration all** @@ -9816,6 +10992,20 @@ This command displays the running configuration of the snmp module. admin@sonic:~$ show runningconfiguration ports Ethernet0 ``` + **show runningconfiguration copp** + + This command displays the running configuration of copp + +- Usage: + ``` + show runningconfiguration copp + ``` + +- Example: + ``` + admin@sonic:~$ show runningconfiguration copp + ``` + Go Back To [Beginning of the document](#) or [Beginning of this section](#Startup--Running-Configuration) @@ -9951,6 +11141,31 @@ This sub-section explains how to configure subinterfaces. Go Back To [Beginning of the document](#) or [Beginning of this section](#subinterfaces) + +## Switchport Modes +### Switchport Modes Config Commands +This subsection explains how to configure switchport modes on a Port/PortChannel. +**config switchport mode ** +Usage: + ``` + config switchport mode + ``` +- Example (Config switchport mode access on "Ethernet0): + ``` + admin@sonic:~$ sudo config switchport mode access Ethernet0 + ``` +- Example (Config switchport mode trunk on "Ethernet4"): + ``` + admin@sonic:~$ sudo config switchport mode trunk Ethernet4 + ``` +- Example (Config switchport mode routed on "Ethernet12"): + ``` + admin@sonic:~$ sudo config switchport mode routed Ethernet12 + ` +`` +Go Back To [Beginning of the document](#) or [Beginning of this section](#switchport-modes) + + ## Syslog ### Syslog Show Commands @@ -9997,7 +11212,7 @@ This command displays rate limit configuration for containers. - Usage ``` - show syslog rate-limit-container [] + show syslog rate-limit-container [] -n [] ``` - Example: @@ -10021,6 +11236,37 @@ This command displays rate limit configuration for containers. SERVICE INTERVAL BURST -------------- ---------- ------- bgp 0 0 + + # Multi ASIC + show syslog rate-limit-container + SERVICE INTERVAL BURST + -------- ---------- -------- + bgp 500 N/A + snmp 300 20000 + swss 2000 12000 + Namespace asic0: + SERVICE INTERVAL BURST + -------- ---------- -------- + bgp 500 N/A + snmp 300 20000 + swss 2000 12000 + + # Multi ASIC + show syslog rate-limit-container bgp + SERVICE INTERVAL BURST + -------- ---------- -------- + bgp 500 5000 + Namespace asic0: + SERVICE INTERVAL BURST + -------- ---------- -------- + bgp 500 5000 + + # Multi ASIC + show syslog rate-limit-container bgp -n asic1 + Namespace asic1: + SERVICE INTERVAL BURST + -------- ---------- -------- + bgp 500 5000 ``` ### Syslog Config Commands @@ -10099,12 +11345,109 @@ This command is used to configure syslog rate limit for containers. - Parameters: - _interval_: determines the amount of time that is being measured for rate limiting. - _burst_: defines the amount of messages, that have to occur in the time limit of interval, to trigger rate limiting + - _namespace_: namespace name or all. Value "default" indicates global namespace. - Example: ``` + # Config bgp for all namespaces. For multi ASIC platforms, bgp service in all namespaces will be affected. + # For single ASIC platforms, bgp service in global namespace will be affected. admin@sonic:~$ sudo config syslog rate-limit-container bgp --interval 300 --burst 20000 + + # Config bgp for global namespace only. + config syslog rate-limit-container bgp --interval 300 --burst 20000 -n default + + # Config bgp for asic0 namespace only. + config syslog rate-limit-container bgp --interval 300 --burst 20000 -n asic0 ``` +**config syslog rate-limit-feature enable** + +This command is used to enable syslog rate limit feature. + +- Usage: + ``` + config syslog rate-limit-feature enable [] -n [] + ``` + +- Example: + ``` + # Enable syslog rate limit for all services in all namespaces + admin@sonic:~$ sudo config syslog rate-limit-feature enable + + # Enable syslog rate limit for all services in global namespace + config syslog rate-limit-feature enable -n default + + # Enable syslog rate limit for all services in asic0 namespace + config syslog rate-limit-feature enable -n asic0 + + # Enable syslog rate limit for database in all namespaces + config syslog rate-limit-feature enable database + + # Enable syslog rate limit for database in default namespace + config syslog rate-limit-feature enable database -n default + + # Enable syslog rate limit for database in asci0 namespace + config syslog rate-limit-feature enable database -n asci0 + ``` + +**config syslog rate-limit-feature disable** + +This command is used to disable syslog rate limit feature. + +- Usage: + ``` + config syslog rate-limit-feature disable [] -n [] + ``` + +- Example: + ``` + # Disable syslog rate limit for all services in all namespaces + admin@sonic:~$ sudo config syslog rate-limit-feature disable + + # Disable syslog rate limit for all services in global namespace + config syslog rate-limit-feature disable -n default + + # Disable syslog rate limit for all services in asic0 namespace + config syslog rate-limit-feature disable -n asic0 + + # Disable syslog rate limit for database in all namespaces + config syslog rate-limit-feature disable database + + # Disable syslog rate limit for database in default namespace + config syslog rate-limit-feature disable database -n default + + # Disable syslog rate limit for database in asci0 namespace + config syslog rate-limit-feature disable database -n asci0 + ``` + +**config syslog level** + +This command is used to configure log level for a given log identifier. + +- Usage: + ``` + config syslog level -i -l --container [] --program [] + + config syslog level -i -l --container [] --pid [] + + config syslog level -i -l ---pid [] + ``` + +- Example: + + ``` + # Update the log level without refresh the configuration + config syslog level -i xcvrd -l DEBUG + + # Update the log level and send SIGHUP to xcvrd running in PMON + config syslog level -i xcvrd -l DEBUG --container pmon --program xcvrd + + # Update the log level and send SIGHUP to PID 20 running in PMON + config syslog level -i xcvrd -l DEBUG --container pmon --pid 20 + + # Update the log level and send SIGHUP to PID 20 running in host + config syslog level -i xcvrd -l DEBUG --pid 20 + ``` Go Back To [Beginning of the document](#) or [Beginning of this section](#syslog) @@ -10299,92 +11642,6 @@ This command displays the system-wide memory utilization information – just a Swap: 0B 0B 0B ``` -**show mmu** - -This command displays virtual address to the physical address translation status of the Memory Management Unit (MMU). - -- Usage: - ``` - show mmu - ``` - -- Example: - ``` - admin@sonic:~$ show mmu - Pool: ingress_lossless_pool - ---- -------- - xoff 4194112 - type ingress - mode dynamic - size 10875072 - ---- -------- - - Pool: egress_lossless_pool - ---- -------- - type egress - mode static - size 15982720 - ---- -------- - - Pool: egress_lossy_pool - ---- ------- - type egress - mode dynamic - size 9243812 - ---- ------- - - Profile: egress_lossy_profile - ---------- ------------------------------- - dynamic_th 3 - pool [BUFFER_POOL|egress_lossy_pool] - size 1518 - ---------- ------------------------------- - - Profile: pg_lossless_100000_300m_profile - ---------- ----------------------------------- - xon_offset 2288 - dynamic_th -3 - xon 2288 - xoff 268736 - pool [BUFFER_POOL|ingress_lossless_pool] - size 1248 - ---------- ----------------------------------- - - Profile: egress_lossless_profile - --------- ---------------------------------- - static_th 3995680 - pool [BUFFER_POOL|egress_lossless_pool] - size 1518 - --------- ---------------------------------- - - Profile: pg_lossless_100000_40m_profile - ---------- ----------------------------------- - xon_offset 2288 - dynamic_th -3 - xon 2288 - xoff 177632 - pool [BUFFER_POOL|ingress_lossless_pool] - size 1248 - ---------- ----------------------------------- - - Profile: ingress_lossy_profile - ---------- ----------------------------------- - dynamic_th 3 - pool [BUFFER_POOL|ingress_lossless_pool] - size 0 - ---------- ----------------------------------- - - Profile: pg_lossless_40000_40m_profile - ---------- ----------------------------------- - xon_offset 2288 - dynamic_th -3 - xon 2288 - xoff 71552 - pool [BUFFER_POOL|ingress_lossless_pool] - size 1248 - ---------- ----------------------------------- - ``` - Go Back To [Beginning of the document](#) or [Beginning of this section](#System-State) ### System-Health @@ -10569,6 +11826,36 @@ In addition, displays a list of all current 'Services' and 'Hardware' being moni psu.voltage Ignored Device ``` +**show system-health dpu