diff --git a/show/vnet.py b/show/vnet.py index 177e25187..9e11ed66a 100644 --- a/show/vnet.py +++ b/show/vnet.py @@ -1,9 +1,14 @@ +import ipaddress +import shutil + import click -import utilities_common.cli as clicommon from natsort import natsorted from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector from tabulate import tabulate -import ipaddress + +from sonic_py_common import multi_asic +import utilities_common.cli as clicommon +import utilities_common.multi_asic as multi_asic_util # # 'vnet' command ("show vnet") @@ -442,31 +447,45 @@ def routes(): pass +def _ecmp_row_width(max_item_len, num_wrap_cols, fixed_cols_est): + """Compute how many ECMP items fit per row to stay within the current terminal width. + + :param max_item_len: length of the longest individual item string + :param num_wrap_cols: number of columns that wrap (2 for nexthop+interface, + 3 for endpoint+mac+vni) + :param fixed_cols_est: estimated width of non-wrapping column content plus all + inter-column tabulate spacing (2 spaces × (ncols-1)) + """ + if max_item_len <= 0: + return 1 + terminal_cols = shutil.get_terminal_size((80, 24)).columns + row_width = (terminal_cols - fixed_cols_est) // (num_wrap_cols * (max_item_len + 1)) + return max(1, row_width) + + def pretty_print_local(table, r, nexthop_val, ifname_val): - nexthops = [nexthop.strip() for nexthop in nexthop_val.split(',')] if nexthop_val else [] + nexthops = [nexthop.strip() for nexthop in nexthop_val.split(',')] if nexthop_val else [""] interfaces = [interface.strip() for interface in ifname_val.split(',')] if ifname_val else [] - if not nexthops: - nexthops = [""] all_items = list(nexthops) + list(interfaces) max_len = max((len(item) for item in all_items), default=0) - row_width = 2 if max_len > 15 else 3 + # route_header: ['vnet name', 'prefix', 'nexthop', 'interface'] + # fixed_cols_est = vnet_name(~15) + prefix(~18) + 4-col spacing(6) = 39 + # cap at 2 so nexthop/interface columns stay readable on any terminal width + row_width = min(2, _ecmp_row_width(max_len, num_wrap_cols=2, fixed_cols_est=39)) max_entries = max(len(nexthops), len(interfaces)) i = 0 while i < max_entries: r.append(",".join(nexthops[i:i + row_width]) if i < len(nexthops) else "") - if interfaces: - r.append(",".join(interfaces[i:i + row_width]) if i < len(interfaces) else "") - else: - r.append(ifname_val if i == 0 else "") + r.append(",".join(interfaces[i:i + row_width]) if i < len(interfaces) else "") i += row_width table.append(r) r = ["", ""] -def pretty_print(table, r, epval, mac_addr, vni, metric, state): - endpoints = epval.split(',') +def pretty_print_tunnel(table, r, epval, mac_addr, vni, metric, state): + endpoints = epval.split(',') if epval else [""] # When mac_address or vni is a per-endpoint list, split so all three fields # wrap in the same chunks, keeps rows aligned at any ECMP scale. macs = mac_addr.split(',') if mac_addr and ',' in mac_addr else None @@ -480,7 +499,9 @@ def pretty_print(table, r, epval, mac_addr, vni, metric, state): if vnis: all_items.extend(vnis) max_len = max((len(item) for item in all_items), default=0) - row_width = 2 if max_len > 15 else 3 + # tunnel_header: ['vnet name', 'prefix', 'endpoint', 'mac address', 'vni', 'metric', 'status'] + # fixed_cols_est = vnet_name(~15) + prefix(~16) + metric(6) + status(6) + 7-col spacing(12) = 55 + row_width = _ecmp_row_width(max_len, num_wrap_cols=3, fixed_cols_est=55) i = 0 while i < len(endpoints): @@ -579,13 +600,18 @@ def _show_tunnel_helper(vnet_name=None, appl_db=None, state_db=None): r.extend(k.split(":", 2)[1:]) state_db_key = '|'.join(k.split(":", 2)) val = appl_db.get_all(appl_db.APPL_DB, k) + if not val: + continue val_state = state_db.get_all(state_db.STATE_DB, state_db_key) epval = val.get('endpoint') + mac_addr = val.get('mac_address') or '' + vni = val.get('vni') or '' state = val_state.get('state') if val_state else "" raw_metric = val.get('metric') - metric = int(raw_metric) if raw_metric else '' - pretty_print(table, r, epval, - val.get('mac_address') or '', val.get('vni') or '', - metric, state) + try: + metric = int(raw_metric) if raw_metric else '' + except (ValueError, TypeError): + metric = raw_metric + pretty_print_tunnel(table, r, epval, mac_addr, vni, metric, state) click.echo(tabulate(table, tunnel_header)) diff --git a/tests/show_vnet_test.py b/tests/show_vnet_test.py index a42482877..d3e2d7b3b 100644 --- a/tests/show_vnet_test.py +++ b/tests/show_vnet_test.py @@ -1,17 +1,31 @@ # flake8: noqa: E501 import os +from unittest.mock import patch + import pytest from click.testing import CliRunner + from utilities_common.db import Db + import show.main as show import show.vnet as vnet from tests.mock_tables import dbconnector +# Pin terminal width for all vnet route tests so row_width is deterministic. +_MOCK_TERMINAL = os.terminal_size((200, 24)) + + class TestShowVnetRoutesAll(object): @classmethod def setup_class(cls): print("SETUP") os.environ["UTILITIES_UNIT_TESTING"] = "1" + cls._terminal_patcher = patch("show.vnet.shutil.get_terminal_size", return_value=_MOCK_TERMINAL) + cls._terminal_patcher.start() + + @classmethod + def teardown_class(cls): + cls._terminal_patcher.stop() def test_Preety_print(self): table =[] @@ -22,14 +36,14 @@ def test_Preety_print(self): state = "active" epval = "fddd:a100:a251::a10:1,fddd:a101:a251::a10:1" - vnet.pretty_print(table, row, epval, mac_addr, vni, metric, state) + vnet.pretty_print_tunnel(table, row, epval, mac_addr, vni, metric, state) expected_output = [['Vnet_v6_in_v6-0', 'fddd:a156:a251::a6:1/128', 'fddd:a100:a251::a10:1,fddd:a101:a251::a10:1', '', '', '0', 'active']] assert table == expected_output table =[] row = ["Vnet_v6_in_v6-0", "fddd:a156:a251::a6:1/128"] epval = "fddd:a100:a251::a10:1,fddd:a101:a251::a10:1,fddd:a100:a251::a11:1,fddd:a100:a251::a12:1,fddd:a100:a251::a13:1" - vnet.pretty_print(table, row, epval, mac_addr, vni, metric, state) + vnet.pretty_print_tunnel(table, row, epval, mac_addr, vni, metric, state) expected_output = [ ['Vnet_v6_in_v6-0', 'fddd:a156:a251::a6:1/128', 'fddd:a100:a251::a10:1,fddd:a101:a251::a10:1', '', '', '0', 'active'], ['', '', 'fddd:a100:a251::a11:1,fddd:a100:a251::a12:1', '', '', '', ''], @@ -40,7 +54,7 @@ def test_Preety_print(self): table =[] row = ["Vnet_v6_in_v6-0", "fddd:a156:a251::a6:1/128"] epval = "192.168.1.1,192.168.1.2,192.168.1.3,192.168.1.4,192.168.1.5,192.168.1.6,192.168.1.7,192.168.1.8,192.168.1.9,192.168.1.10,192.168.1.11,192.168.1.12,192.168.1.13,192.168.1.14,192.168.1.15" - vnet.pretty_print(table, row, epval, mac_addr, vni, metric, state) + vnet.pretty_print_tunnel(table, row, epval, mac_addr, vni, metric, state) expected_output =[ ['Vnet_v6_in_v6-0', 'fddd:a156:a251::a6:1/128', '192.168.1.1,192.168.1.2,192.168.1.3', '', '', '0', 'active'], ['', '', '192.168.1.4,192.168.1.5,192.168.1.6', '', '', '', ''], @@ -52,7 +66,7 @@ def test_Preety_print(self): table =[] row = ["Vnet_v6_in_v6-0", "fddd:a156:a251::a6:1/128"] epval = "192.168.1.1" - vnet.pretty_print(table, row, epval, mac_addr, vni, metric, state) + vnet.pretty_print_tunnel(table, row, epval, mac_addr, vni, metric, state) expected_output =[ ['Vnet_v6_in_v6-0', 'fddd:a156:a251::a6:1/128', '192.168.1.1', '', '', '0', 'active']] assert table == expected_output @@ -64,23 +78,23 @@ def test_Preety_print(self): mac_addr = "aa:bb:cc:00:00:01,aa:bb:cc:00:00:02,aa:bb:cc:00:00:03,aa:bb:cc:00:00:04" vni = "100,200,300,400" metric = "" - # MAC items are 17 chars > 15, so row_width=2 - vnet.pretty_print(table, row, epval, mac_addr, vni, metric, state) + # MAC items are 17 chars; at T=200, row_width=(200-55)//(3*18)=2 + vnet.pretty_print_tunnel(table, row, epval, mac_addr, vni, metric, state) expected_output = [ ["TestVnet", "10.0.0.1/32", "1.1.1.1,1.1.1.1", "aa:bb:cc:00:00:01,aa:bb:cc:00:00:02", "100,200", "", "active"], ["", "", "1.1.1.1,1.1.1.1", "aa:bb:cc:00:00:03,aa:bb:cc:00:00:04", "300,400", "", ""], ] assert table == expected_output - # row_width decided by MAC item length, not just endpoint length + # row_width decided by MAC item length (max_len=17), not just endpoint length table = [] row = ["TestVnet", "10.0.0.1/32"] epval = "1.1.1.1,2.2.2.2,3.3.3.3" mac_addr = "aa:bb:cc:00:00:01,aa:bb:cc:00:00:02,aa:bb:cc:00:00:03" vni = "100,200,300" metric = "5" - # All endpoints are <=7 chars, MAC items are 17 chars > 15 → row_width=2 - vnet.pretty_print(table, row, epval, mac_addr, vni, metric, state) + # max_len=17 (MACs); at T=200, row_width=2 + vnet.pretty_print_tunnel(table, row, epval, mac_addr, vni, metric, state) expected_output = [ ["TestVnet", "10.0.0.1/32", "1.1.1.1,2.2.2.2", "aa:bb:cc:00:00:01,aa:bb:cc:00:00:02", "100,200", "5", "active"], ["", "", "3.3.3.3", "aa:bb:cc:00:00:03", "300", "", ""], @@ -103,7 +117,7 @@ def test_pretty_print_scale(self, N, vnet_name, prefix, metric): table = [] row = [vnet_name, prefix] - vnet.pretty_print(table, row, + vnet.pretty_print_tunnel(table, row, ",".join(endpoints_list), ",".join(macs_list), ",".join(vnis_list), @@ -137,29 +151,36 @@ def test_pretty_print_local(self): vnet.pretty_print_local(table, row, "192.168.1.1", "Ethernet1") assert table == [["TestVnet", "10.0.0.0/24", "192.168.1.1", "Ethernet1"]] - # 3 short nexthops, row_width=3, all fit on one row + # 3 short nexthops — row_width=2, wraps to 2 rows (2+1) table = [] row = ["TestVnet", "10.0.0.0/24"] vnet.pretty_print_local(table, row, "1.1.1.1,2.2.2.2,3.3.3.3", "Eth1,Eth2,Eth3") - assert table == [["TestVnet", "10.0.0.0/24", "1.1.1.1,2.2.2.2,3.3.3.3", "Eth1,Eth2,Eth3"]] + assert table == [ + ["TestVnet", "10.0.0.0/24", "1.1.1.1,2.2.2.2", "Eth1,Eth2"], + ["", "", "3.3.3.3", "Eth3"], + ] # Spaces in DB values are stripped table = [] row = ["TestVnet", "10.0.0.0/24"] vnet.pretty_print_local(table, row, "1.1.1.1, 2.2.2.2, 3.3.3.3", "Eth1, Eth2, Eth3") - assert table == [["TestVnet", "10.0.0.0/24", "1.1.1.1,2.2.2.2,3.3.3.3", "Eth1,Eth2,Eth3"]] + assert table == [ + ["TestVnet", "10.0.0.0/24", "1.1.1.1,2.2.2.2", "Eth1,Eth2"], + ["", "", "3.3.3.3", "Eth3"], + ] - # 5 short nexthops — wraps into 2 rows (3+2) + # 5 short nexthops — row_width=2, wraps into 3 rows (2+2+1) table = [] row = ["TestVnet", "10.0.0.0/24"] vnet.pretty_print_local(table, row, "1.1.1.1,2.2.2.2,3.3.3.3,4.4.4.4,5.5.5.5", "Eth1,Eth2,Eth3,Eth4,Eth5") assert table == [ - ["TestVnet", "10.0.0.0/24", "1.1.1.1,2.2.2.2,3.3.3.3", "Eth1,Eth2,Eth3"], - ["", "", "4.4.4.4,5.5.5.5", "Eth4,Eth5"], + ["TestVnet", "10.0.0.0/24", "1.1.1.1,2.2.2.2", "Eth1,Eth2"], + ["", "", "3.3.3.3,4.4.4.4", "Eth3,Eth4"], + ["", "", "5.5.5.5", "Eth5"], ] - # Long interface names (>15 chars) → row_width=2 + # PortChannel interface names — row_width=2 table = [] row = ["TestVnet", "10.0.0.0/24"] vnet.pretty_print_local(table, row, @@ -177,17 +198,22 @@ def test_show_vnet_routes_all_basic(self): result = runner.invoke(show.cli.commands['vnet'].commands['routes'].commands['all'], [], obj=db) assert result.exit_code == 0 expected_output = """\ -vnet name prefix nexthop interface ---------------- ---------------- -------------------------------------- -------------------------------- -Vnet_7959668 10.32.0.0/17 10.33.254.1,10.33.254.3,10.33.254.5 Po1031.106,Po1032.106,Po1033.106 - 10.33.254.7,10.33.254.9,10.33.254.11 Po1034.106,Po1035.106,Po1036.106 - 10.33.254.13,10.33.254.15,10.33.254.17 Po1037.106,Po1038.106,Po1039.106 - 10.33.254.19,10.33.254.23,10.33.254.25 Po1040.106,Po1042.106,Po1043.106 - 10.33.254.27,10.33.254.29,10.33.254.31 Po1044.106,Po1045.106,Po1046.106 -test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 -test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 -test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2,100.102.4.3 Ethernet1,Ethernet2,Ethernet3 -test_v4_in_v4-1 160.165.191.1/32 100.103.4.1,100.103.4.2,100.103.4.3 Ethernet1,Ethernet2,Ethernet3 +vnet name prefix nexthop interface +--------------- ---------------- ------------------------- --------------------- +Vnet_7959668 10.32.0.0/17 10.33.254.1,10.33.254.3 Po1031.106,Po1032.106 + 10.33.254.5,10.33.254.7 Po1033.106,Po1034.106 + 10.33.254.9,10.33.254.11 Po1035.106,Po1036.106 + 10.33.254.13,10.33.254.15 Po1037.106,Po1038.106 + 10.33.254.17,10.33.254.19 Po1039.106,Po1040.106 + 10.33.254.23,10.33.254.25 Po1042.106,Po1043.106 + 10.33.254.27,10.33.254.29 Po1044.106,Po1045.106 + 10.33.254.31 Po1046.106 +test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 +test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 +test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2 Ethernet1,Ethernet2 + 100.102.4.3 Ethernet3 +test_v4_in_v4-1 160.165.191.1/32 100.103.4.1,100.103.4.2 Ethernet1,Ethernet2 + 100.103.4.3 Ethernet3 vnet name prefix endpoint mac address vni metric status ------------------ ------------------------ ------------------------------------------- ----------------------------------- --------------- -------- -------- @@ -216,11 +242,12 @@ def test_show_vnet_routes_all_vnetname(self): ['test_v4_in_v4-0'], obj=db) assert result.exit_code == 0 expected_output = """\ -vnet name prefix nexthop interface ---------------- ---------------- ----------------------------------- ----------------------------- -test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 -test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 -test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2,100.102.4.3 Ethernet1,Ethernet2,Ethernet3 +vnet name prefix nexthop interface +--------------- ---------------- ----------------------- ------------------- +test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 +test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 +test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2 Ethernet1,Ethernet2 + 100.102.4.3 Ethernet3 vnet name prefix endpoint mac address vni metric status --------------- ---------------- ----------- ------------- ----- -------- -------- @@ -279,33 +306,22 @@ def test_show_vnet_routes_local_basic(self): result = runner.invoke(show.cli.commands['vnet'].commands['routes'].commands['local'], [], obj=db) assert result.exit_code == 0 expected_output = """\ -vnet name prefix nexthop interface ---------------- ---------------- -------------------------------------- -------------------------------- -Vnet_7959668 10.32.0.0/17 10.33.254.1,10.33.254.3,10.33.254.5 Po1031.106,Po1032.106,Po1033.106 - 10.33.254.7,10.33.254.9,10.33.254.11 Po1034.106,Po1035.106,Po1036.106 - 10.33.254.13,10.33.254.15,10.33.254.17 Po1037.106,Po1038.106,Po1039.106 - 10.33.254.19,10.33.254.23,10.33.254.25 Po1040.106,Po1042.106,Po1043.106 - 10.33.254.27,10.33.254.29,10.33.254.31 Po1044.106,Po1045.106,Po1046.106 -test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 -test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 -test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2,100.102.4.3 Ethernet1,Ethernet2,Ethernet3 -test_v4_in_v4-1 160.165.191.1/32 100.103.4.1,100.103.4.2,100.103.4.3 Ethernet1,Ethernet2,Ethernet3 -""" - assert result.output == expected_output - - def test_show_vnet_routes_local_vnetname(self): - runner = CliRunner() - db = Db() - - result = runner.invoke(show.cli.commands['vnet'].commands['routes'].commands['local'], - ['test_v4_in_v4-0'], obj=db) - assert result.exit_code == 0 - expected_output = """\ -vnet name prefix nexthop interface ---------------- ---------------- ----------------------------------- ----------------------------- -test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 -test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 -test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2,100.102.4.3 Ethernet1,Ethernet2,Ethernet3 +vnet name prefix nexthop interface +--------------- ---------------- ------------------------- --------------------- +Vnet_7959668 10.32.0.0/17 10.33.254.1,10.33.254.3 Po1031.106,Po1032.106 + 10.33.254.5,10.33.254.7 Po1033.106,Po1034.106 + 10.33.254.9,10.33.254.11 Po1035.106,Po1036.106 + 10.33.254.13,10.33.254.15 Po1037.106,Po1038.106 + 10.33.254.17,10.33.254.19 Po1039.106,Po1040.106 + 10.33.254.23,10.33.254.25 Po1042.106,Po1043.106 + 10.33.254.27,10.33.254.29 Po1044.106,Po1045.106 + 10.33.254.31 Po1046.106 +test_v4_in_v4-0 160.162.191.1/32 100.100.4.1 Ethernet1 +test_v4_in_v4-0 160.163.191.1/32 100.101.4.1,100.101.4.2 Ethernet1,Ethernet2 +test_v4_in_v4-0 160.164.191.1/32 100.102.4.1,100.102.4.2 Ethernet1,Ethernet2 + 100.102.4.3 Ethernet3 +test_v4_in_v4-1 160.165.191.1/32 100.103.4.1,100.103.4.2 Ethernet1,Ethernet2 + 100.103.4.3 Ethernet3 """ assert result.output == expected_output @@ -331,10 +347,13 @@ def setup_class(cls): print("SETUP") os.environ["UTILITIES_UNIT_TESTING"] = "1" dbconnector.topo = "vnet_ecmp" + cls._terminal_patcher = patch("show.vnet.shutil.get_terminal_size", return_value=_MOCK_TERMINAL) + cls._terminal_patcher.start() @classmethod def teardown_class(cls): dbconnector.topo = None + cls._terminal_patcher.stop() def test_show_vnet_routes_tunnel_real_world_ecmp(self): """Real-world ECMP dataset: 287 entries (10 unique ep/MAC pairs repeated) for Vnet_7127926. @@ -349,7 +368,7 @@ def test_show_vnet_routes_tunnel_real_world_ecmp(self): assert result.exit_code == 0 output = result.output - # 30.0.21.0/24: 12 unique endpoints, 2-per-row (12 > 2 → row_width=2). Exact output pinned. + # 30.0.21.0/24: 12 unique endpoints. MAC items are 17 chars; at T=200, row_width=2. Exact output pinned. expected_21 = ( "Vnet_7127926 30.0.21.0/24 100.106.230.44,10.134.85.10 00:22:48:03:8c:f8,60:45:bd:a3:8d:ab 7127926,7127926 5 active\n" " 100.106.229.38,100.106.229.170 60:45:bd:a3:21:88,60:45:bd:a2:e4:39 7127926,7127926\n"