diff --git a/config/main.py b/config/main.py index 95313a1d5d..d0ab56ed64 100644 --- a/config/main.py +++ b/config/main.py @@ -3017,11 +3017,16 @@ def del_portchannel_member(ctx, portchannel_name, port_name): @portchannel.group(cls=clicommon.AbbreviationGroup, name='retry-count') @click.pass_context def portchannel_retry_count(ctx): - pass + teamdctl_command = ["teamdctl"] + if ctx.obj["namespace"] != DEFAULT_NAMESPACE: + teamdctl_command += ["-n", ctx.obj['namespace'].removeprefix("asic")] + ctx.obj["teamdctl_command"] = teamdctl_command def check_if_retry_count_is_enabled(ctx, portchannel_name): try: - proc = subprocess.Popen(["teamdctl", portchannel_name, "state", "item", "get", "runner.enable_retry_count_feature"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ctx.obj["teamdctl_command"] + [portchannel_name, "state", "item", "get", + "runner.enable_retry_count_feature"] + proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, err = proc.communicate(timeout=10) if proc.returncode != 0: ctx.fail("Unable to determine if the retry count feature is enabled or not: {}".format(err.strip())) @@ -3052,7 +3057,9 @@ def get_portchannel_retry_count(ctx, portchannel_name): if not is_retry_count_enabled: ctx.fail("Retry count feature is not enabled!") - proc = subprocess.Popen(["teamdctl", portchannel_name, "state", "item", "get", "runner.retry_count"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ctx.obj["teamdctl_command"] + [portchannel_name, "state", "item", "get", + "runner.retry_count"] + proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, err = proc.communicate(timeout=10) if proc.returncode != 0: ctx.fail("Unable to get the retry count: {}".format(err.strip())) @@ -3088,7 +3095,9 @@ def set_portchannel_retry_count(ctx, portchannel_name, retry_count): if not is_retry_count_enabled: ctx.fail("Retry count feature is not enabled!") - proc = subprocess.Popen(["teamdctl", portchannel_name, "state", "item", "set", "runner.retry_count", str(retry_count)], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd = ctx.obj["teamdctl_command"] + [portchannel_name, "state", "item", "set", + "runner.retry_count", str(retry_count)] + proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, err = proc.communicate(timeout=10) if proc.returncode != 0: ctx.fail("Unable to set the retry count: {}".format(err.strip())) diff --git a/scripts/teamd_increase_retry_count.py b/scripts/teamd_increase_retry_count.py index d5151b69b9..ddc3ec1a09 100755 --- a/scripts/teamd_increase_retry_count.py +++ b/scripts/teamd_increase_retry_count.py @@ -2,14 +2,8 @@ import subprocess import json +from pyroute2 import netns from scapy.config import conf -conf.ipv6_enabled = False -conf.verb = False -from scapy.fields import ByteField, ShortField, MACField, XStrFixedLenField, ConditionalField -from scapy.layers.l2 import Ether -from scapy.sendrecv import sendp, sniff -from scapy.packet import Packet, split_layers, bind_layers -import scapy.contrib.lacp import os import re import sys @@ -18,10 +12,18 @@ import argparse import signal -from sonic_py_common import logger -from swsscommon.swsscommon import DBConnector, Table +from sonic_py_common import logger, multi_asic +from swsscommon.swsscommon import DBConnector, Table, SonicDBConfig, SonicDBKey + +conf.ipv6_enabled = False +conf.verb = False +from scapy.fields import ByteField, ShortField, MACField, XStrFixedLenField, ConditionalField # noqa: E402 +from scapy.layers.l2 import Ether # noqa: E402 +from scapy.sendrecv import sendp, sniff # noqa: E402 +from scapy.packet import Packet, split_layers, bind_layers # noqa: E402 +import scapy.contrib.lacp # noqa: E402 -log = logger.Logger() +log = None revertTeamdRetryCountChanges = False DEFAULT_RETRY_COUNT = 3 EXTENDED_RETRY_COUNT = 5 @@ -88,9 +90,12 @@ def run(self): sniff(stop_filter=self.lacpPacketCallback, iface=self.port, filter="ether proto {} and ether src {}".format(LACP_ETHERTYPE, self.targetMacAddress), store=0, timeout=30, started_callback=self.sendReadyEvent.set) -def getPortChannels(): - applDb = DBConnector("APPL_DB", 0) - configDb = DBConnector("CONFIG_DB", 0) + +def getPortChannels(namespace=""): + key = SonicDBKey(namespace) + is_tcp_conn = False + applDb = DBConnector("APPL_DB", 0, is_tcp_conn, key) + configDb = DBConnector("CONFIG_DB", 0, is_tcp_conn, key) portChannelTable = Table(applDb, "LAG_TABLE") portChannels = portChannelTable.getKeys() activePortChannels = [] @@ -159,12 +164,20 @@ def getPortChannels(): return set([portChannelData[x]["portChannel"] for x in portChannelData.keys() if portChannelData[x]["adminUp"]]) -def getPortChannelConfig(portChannelName): - (processStdout, _) = getCmdOutput(["teamdctl", portChannelName, "state", "dump"]) + +def getPortChannelConfig(portChannelName, namespace=""): + teamdctl_command = ["teamdctl"] + if namespace: + teamdctl_command += ["-n", namespace.removeprefix("asic")] + (processStdout, _) = getCmdOutput(teamdctl_command + [portChannelName, "state", "dump"]) return json.loads(processStdout) -def getLldpNeighbors(): - (processStdout, _) = getCmdOutput(["lldpctl", "-f", "json"]) + +def getLldpNeighbors(namespace=""): + container = "lldp" + if namespace: + container += namespace.removeprefix("asic") + (processStdout, _) = getCmdOutput(["docker", "exec", container, "lldpctl", "-f", "json"]) return json.loads(processStdout) def craftLacpPacket(portChannelConfig, portName, isResetPacket=False, newVersion=True): @@ -172,7 +185,7 @@ def craftLacpPacket(portChannelConfig, portName, isResetPacket=False, newVersion actorConfig = portConfig["runner"]["actor_lacpdu_info"] partnerConfig = portConfig["runner"]["partner_lacpdu_info"] l2 = Ether(dst=SLOW_PROTOCOL_MAC_ADDRESS, src=portConfig["ifinfo"]["dev_addr"], type=LACP_ETHERTYPE) - l3 = scapy.contrib.lacp.SlowProtocol(subtype=0x01) + l3 = scapy.contrib.lacp.SlowProtocol(subtype=0x01) l4 = LACPRetryCount() if newVersion: l4.version = 0xf1 @@ -215,11 +228,16 @@ def getCmdOutput(cmd): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) return proc.communicate()[0], proc.returncode -def main(probeOnly=False): + +def main(probeOnly=False, namespace=""): if os.geteuid() != 0: log.log_error("Root privileges required for this operation", also_print_to_console=True) sys.exit(1) - portChannels = getPortChannels() + if namespace: + netns.setns(namespace) + if multi_asic.is_multi_asic(): + SonicDBConfig.initializeGlobalConfig() + portChannels = getPortChannels(namespace) if not portChannels: log.log_info("No port channels retrieved; exiting") return @@ -227,7 +245,7 @@ def main(probeOnly=False): if probeOnly: for portChannel in portChannels: config = getPortChannelConfig(portChannel) - lldpInfo = getLldpNeighbors() + lldpInfo = getLldpNeighbors(namespace) portChannelChecked = False for portName in config["ports"].keys(): if not "runner" in config["ports"][portName] or \ @@ -288,11 +306,12 @@ def main(probeOnly=False): global revertTeamdRetryCountChanges signal.signal(signal.SIGUSR1, abortTeamdChanges) signal.signal(signal.SIGTERM, abortTeamdChanges) - (_, rc) = getCmdOutput(["config", "portchannel", "retry-count", "get", list(portChannels)[0]]) + (_, rc) = getCmdOutput(["config", "portchannel", "-n", namespace, "retry-count", "get", list(portChannels)[0]]) if rc == 0: # Currently running on SONiC version with teamd retry count feature for portChannel in portChannels: - getCmdOutput(["config", "portchannel", "retry-count", "set", portChannel, str(EXTENDED_RETRY_COUNT)]) + getCmdOutput(["config", "portchannel", "-n", namespace, "retry-count", "set", + portChannel, str(EXTENDED_RETRY_COUNT)]) pid = os.fork() if pid == 0: # Running in a new process, detached from parent process @@ -300,7 +319,8 @@ def main(probeOnly=False): time.sleep(15) if revertTeamdRetryCountChanges: for portChannel in portChannels: - getCmdOutput(["config", "portchannel", "retry-count", "set", portChannel, str(DEFAULT_RETRY_COUNT)]) + getCmdOutput(["config", "portchannel", "-n", namespace, "retry-count", "set", + portChannel, str(DEFAULT_RETRY_COUNT)]) else: lacpPackets = [] revertLacpPackets = [] @@ -326,5 +346,10 @@ def main(probeOnly=False): parser = argparse.ArgumentParser(description='Teamd retry count changer.') parser.add_argument('--probe-only', action='store_true', help='Probe the peer devices only, to verify that they support the teamd retry count feature') + parser.add_argument('-n', '--namespace', default="", type=str, help='namespace to use') args = parser.parse_args() - main(args.probe_only) + log_identifier = "teamd_increase_retry_count" + if args.namespace: + log_identifier += f"_{args.namespace}" + log = logger.Logger(log_identifier=log_identifier) + main(args.probe_only, args.namespace) diff --git a/tests/portchannel_test.py b/tests/portchannel_test.py index b2b5e39991..2c5c97a3db 100644 --- a/tests/portchannel_test.py +++ b/tests/portchannel_test.py @@ -365,12 +365,15 @@ def __call__(self, *args, **kwargs): return TestPortChannel.originalSubprocessPopen(*args, **kwargs) if self.timeout: return TestPortChannel.originalSubprocessPopen(["sleep", "90"], **kwargs) - if commandArgs[5] == "runner.enable_retry_count_feature": + # Find the runner item in the command args (handles both with and without -n flag) + # For "get": [..., "get", "runner.xxx"] - runner item is last + # For "set": [..., "set", "runner.retry_count", value] - runner item is second to last + if "runner.enable_retry_count_feature" in commandArgs: return TestPortChannel.originalSubprocessPopen(["echo", "true" if self.retryCountEnabled else "false"], **kwargs) - elif commandArgs[5] == "runner.retry_count": - if commandArgs[4] == "get": + elif "runner.retry_count" in commandArgs: + if "get" in commandArgs: return TestPortChannel.originalSubprocessPopen(["echo", "3"], **kwargs) - elif commandArgs[4] == "set": + elif "set" in commandArgs: return TestPortChannel.originalSubprocessPopen(["echo", ""], **kwargs) else: return TestPortChannel.originalSubprocessPopen(["false"], **kwargs) @@ -381,7 +384,7 @@ def __call__(self, *args, **kwargs): def test_get_portchannel_retry_count_disabled(self, subprocessMock): runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'db': db.cfgdb, 'teamdctl_command': ['teamdctl']} subprocessMock.retryCountEnabled = False @@ -396,7 +399,7 @@ def test_get_portchannel_retry_count_disabled(self, subprocessMock): def test_set_portchannel_retry_count_disabled(self, subprocessMock): runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'db': db.cfgdb, 'teamdctl_command': ['teamdctl']} subprocessMock.retryCountEnabled = False @@ -411,7 +414,7 @@ def test_set_portchannel_retry_count_disabled(self, subprocessMock): def test_get_portchannel_retry_count_timeout(self, subprocessMock): runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'db': db.cfgdb, 'teamdctl_command': ['teamdctl']} subprocessMock.retryCountEnabled = True subprocessMock.timeout = True @@ -428,7 +431,7 @@ def test_get_portchannel_retry_count_timeout(self, subprocessMock): def test_set_portchannel_retry_count_timeout(self, subprocessMock): runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'db': db.cfgdb, 'teamdctl_command': ['teamdctl']} subprocessMock.retryCountEnabled = True subprocessMock.timeout = True @@ -445,7 +448,7 @@ def test_set_portchannel_retry_count_timeout(self, subprocessMock): def test_get_portchannel_retry_count(self, subprocessMock): runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'db': db.cfgdb, 'teamdctl_command': ['teamdctl']} subprocessMock.retryCountEnabled = True @@ -461,7 +464,7 @@ def test_get_portchannel_retry_count(self, subprocessMock): def test_set_portchannel_retry_count(self, subprocessMock): runner = CliRunner() db = Db() - obj = {'db':db.cfgdb} + obj = {'db': db.cfgdb, 'teamdctl_command': ['teamdctl']} subprocessMock.retryCountEnabled = True @@ -473,6 +476,45 @@ def test_set_portchannel_retry_count(self, subprocessMock): assert result.exit_code == 0 assert result.output == "" + @patch("subprocess.Popen", new_callable=SubprocessMock) + def test_portchannel_retry_count_group_default_namespace(self, subprocessMock): + """Test that portchannel_retry_count group sets up teamdctl_command for default namespace""" + runner = CliRunner() + db = Db() + # Don't pass teamdctl_command - let the group function set it up + obj = {'db': db.cfgdb, 'namespace': ''} + + subprocessMock.retryCountEnabled = True + + # Invoke through retry-count group command to trigger the group callback + result = runner.invoke( + config.config.commands["portchannel"].commands["retry-count"], + ["get", "PortChannel1001"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output.strip() == "3" + + @patch("subprocess.Popen", new_callable=SubprocessMock) + def test_portchannel_retry_count_group_multi_asic(self, subprocessMock): + """Test that portchannel_retry_count group sets up teamdctl_command for non-default namespace (multi-asic)""" + runner = CliRunner() + db = Db() + # Don't pass teamdctl_command - let the group function set it up + # Use a non-default namespace to cover the multi-asic code path + obj = {'db': db.cfgdb, 'namespace': 'asic0'} + + subprocessMock.retryCountEnabled = True + + # Invoke through retry-count group command to trigger the group callback + result = runner.invoke( + config.config.commands["portchannel"].commands["retry-count"], + ["get", "PortChannel1001"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output.strip() == "3" + @classmethod def teardown_class(cls): os.environ['UTILITIES_UNIT_TESTING'] = "0"