From c14d4d5b380b48160b0d622e63c97018702f086f Mon Sep 17 00:00:00 2001 From: cliffchen Date: Mon, 15 Dec 2025 06:04:14 +0000 Subject: [PATCH] [tests/conftest]: Refactor fanouthosts fixture, graph_utils to support serial connections * Refactored fanouthosts fixture to support both Ethernet and Serial connections * Added support for device_serial_link from conn_graph_facts for Console Server topologies (mc0/c0) Signed-off-by: cliffchen --- ansible/files/sonic_lab_serial_link.csv | 5 + ansible/module_utils/graph_utils.py | 33 ++++ tests/conftest.py | 220 ++++++++++++++++-------- 3 files changed, 187 insertions(+), 71 deletions(-) create mode 100644 ansible/files/sonic_lab_serial_link.csv diff --git a/ansible/files/sonic_lab_serial_link.csv b/ansible/files/sonic_lab_serial_link.csv new file mode 100644 index 00000000000..8b03346e26f --- /dev/null +++ b/ansible/files/sonic_lab_serial_link.csv @@ -0,0 +1,5 @@ +StartDevice,StartPort,EndDevice,EndPort,BaudRate,FlowControl +str-msn2700-01,1,str-7260-10,1,9600,0 +str-msn2700-01,2,str-7260-10,2,9600,0 +str-msn2700-01,3,str-7260-10,3,9600,0 +str-msn2700-01,4,str-7260-10,4,9600,0 \ No newline at end of file diff --git a/ansible/module_utils/graph_utils.py b/ansible/module_utils/graph_utils.py index cb334c9188e..cc3f5960eed 100644 --- a/ansible/module_utils/graph_utils.py +++ b/ansible/module_utils/graph_utils.py @@ -22,6 +22,7 @@ class LabGraph(object): "console_links": "sonic_{}_console_links.csv", "bmc_links": "sonic_{}_bmc_links.csv", "l1_links": "sonic_{}_l1_links.csv", + "serial_links": "sonic_{}_serial_link.csv", } def __init__(self, path, group): @@ -362,6 +363,35 @@ def csv_to_graph_facts(self): self.graph_facts["from_l1_links"] = from_l1_links self.graph_facts["to_l1_links"] = to_l1_links + # Process serial links + serial_links = {} + for entry in self.csv_facts["serial_links"]: + start_device = entry["StartDevice"] + start_port = entry["StartPort"] + end_device = entry["EndDevice"] + end_port = entry["EndPort"] + + if start_device not in serial_links: + serial_links[start_device] = {} + if end_device not in serial_links: + serial_links[end_device] = {} + + serial_links[start_device][start_port] = { + "peerdevice": end_device, + "peerport": end_port, + "baud_rate": entry.get("BaudRate", "9600"), + "flow_control": entry.get("FlowControl", "0"), + } + serial_links[end_device][end_port] = { + "peerdevice": start_device, + "peerport": start_port, + "baud_rate": entry.get("BaudRate", "9600"), + "flow_control": entry.get("FlowControl", "0"), + } + + logging.debug("Found serial links: {}".format(serial_links)) + self.graph_facts["serial_links"] = serial_links + def build_results(self, hostnames, ignore_error=False): device_info = {} device_conn = {} @@ -381,6 +411,7 @@ def build_results(self, hostnames, ignore_error=False): device_from_l1_links = {} device_to_l1_links = {} device_l1_cross_connects = {} + device_serial_link = {} msg = "" logging.debug("Building results for hostnames: {}".format(hostnames)) @@ -491,6 +522,8 @@ def build_results(self, hostnames, ignore_error=False): device_from_l1_links[hostname] = self.graph_facts["from_l1_links"].get(hostname, {}) device_to_l1_links[hostname] = self.graph_facts["to_l1_links"].get(hostname, {}) + device_serial_link[hostname] = self.graph_facts["serial_links"].get(hostname, {}) + filtered_linked_ports = self._filter_linked_ports(hostnames) l1_cross_connects = self._create_l1_cross_connects(filtered_linked_ports) diff --git a/tests/conftest.py b/tests/conftest.py index 2537b8b16f8..f14fd84b110 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -935,89 +935,138 @@ def initial_neighbor(neighbor_name, vm_name): logger.info("Fixture nbrhosts finished") return devices - @pytest.fixture(scope="module") def fanouthosts(enhance_inventory, ansible_adhoc, tbinfo, conn_graph_facts, creds, duthosts): # noqa: F811 """ Shortcut fixture for getting Fanout hosts - """ - - dev_conn = conn_graph_facts.get('device_conn', {}) + Supports both Ethernet connections and Serial connections + + For Ethernet connections: Uses device_conn from conn_graph_facts + For Serial connections: Uses device_serial_link from conn_graph_facts + """ + # Internal helper functions + def create_or_get_fanout(fanout_hosts, fanout_name, dut_host) -> FanoutHost | None: + """ + Create FanoutHost if not exists, or return existing one. + This centralizes fanout creation logic for both Ethernet and Serial connections. + + Args: + fanout_hosts (dict): Dictionary of existing fanout hosts + fanout_name (str): Fanout device hostname + dut_host (str): DUT hostname that connects to this fanout + + Returns: + FanoutHost: Fanout host object + """ + # Return existing fanout if already created + if fanout_name in fanout_hosts: + fanout = fanout_hosts[fanout_name] + if dut_host not in fanout.dut_hostnames: + fanout.dut_hostnames.append(dut_host) + return fanout + + # Get fanout device info from inventory + try: + host_vars = ansible_adhoc().options['inventory_manager'].get_host(fanout_name).vars + except Exception as e: + logging.warning(f"Cannot get inventory for fanout {fanout_name}: {e}") + return None + + os_type = host_vars.get('os', 'eos') + + # Get credentials based on OS type + if 'fanout_tacacs_user' in creds: + fanout_user = creds['fanout_tacacs_user'] + fanout_password = creds['fanout_tacacs_password'] + elif 'fanout_tacacs_{}_user'.format(os_type) in creds: + fanout_user = creds['fanout_tacacs_{}_user'.format(os_type)] + fanout_password = creds['fanout_tacacs_{}_password'.format(os_type)] + elif os_type == 'sonic': + fanout_user = creds.get('fanout_sonic_user', None) + fanout_password = creds.get('fanout_sonic_password', None) + elif os_type == 'eos': + fanout_user = creds.get('fanout_network_user', None) + fanout_password = creds.get('fanout_network_password', None) + elif os_type == 'onyx': + fanout_user = creds.get('fanout_mlnx_user', None) + fanout_password = creds.get('fanout_mlnx_password', None) + elif os_type == 'ixia': + # Skip for ixia device which has no fanout + logging.info(f"Skipping ixia device {fanout_name}") + return None + else: + logging.warning(f"Unsupported fanout OS type: {os_type}") + return None + + # EOS specific shell credentials + eos_shell_user = None + eos_shell_password = None + if os_type == "eos": + admin_user = creds['fanout_admin_user'] + admin_password = creds['fanout_admin_password'] + eos_shell_user = creds.get('fanout_shell_user', admin_user) + eos_shell_password = creds.get('fanout_shell_password', admin_password) + + # Create FanoutHost object + fanout = FanoutHost( + ansible_adhoc, + os_type, + fanout_name, + 'FanoutLeaf', + fanout_user, + fanout_password, + eos_shell_user=eos_shell_user, + eos_shell_passwd=eos_shell_password + ) + fanout.dut_hostnames = [dut_host] + fanout_hosts[fanout_name] = fanout + + # For SONiC fanout, get port alias to name mapping + if fanout.os == 'sonic': + ifs_status = fanout.host.get_interfaces_status() + for key, interface_info in list(ifs_status.items()): + fanout.fanout_port_alias_to_name[interface_info['alias']] = interface_info['interface'] + logging.info(f"fanout {fanout_name} fanout_port_alias_to_name {fanout.fanout_port_alias_to_name}") + + return fanout + + # Main fixture logic fanout_hosts = {} - + + # Skip NUT topologies that have no fanout if tbinfo['topo']['name'].startswith('nut-'): - # Nut topology has no fanout + logging.info("Nut topology has no fanout") return fanout_hosts + + # Process Ethernet connections + dev_conn = conn_graph_facts.get('device_conn', {}) - # WA for virtual testbed which has no fanout - for dut_host, value in list(dev_conn.items()): + for dut_host, ethernet_ports in dev_conn.items(): + duthost = duthosts[dut_host] + + # Skip virtual testbed which has no fanout if duthost.facts['platform'] == 'x86_64-kvm_x86_64-r0': - continue # skip for kvm platform which has no fanout + logging.info(f"Skipping kvm platform {dut_host}") + continue + mg_facts = duthost.minigraph_facts(host=duthost.hostname)['ansible_facts'] - for dut_port in list(value.keys()): - fanout_rec = value[dut_port] + + # Process each Ethernet port connection + for dut_port, fanout_rec in ethernet_ports.items(): fanout_host = str(fanout_rec['peerdevice']) fanout_port = str(fanout_rec['peerport']) - - if fanout_host in list(fanout_hosts.keys()): - fanout = fanout_hosts[fanout_host] - else: - host_vars = ansible_adhoc().options[ - 'inventory_manager'].get_host(fanout_host).vars - os_type = host_vars.get('os', 'eos') - if 'fanout_tacacs_user' in creds: - fanout_user = creds['fanout_tacacs_user'] - fanout_password = creds['fanout_tacacs_password'] - elif 'fanout_tacacs_{}_user'.format(os_type) in creds: - fanout_user = creds['fanout_tacacs_{}_user'.format(os_type)] - fanout_password = creds['fanout_tacacs_{}_password'.format(os_type)] - elif os_type == 'sonic': - fanout_user = creds.get('fanout_sonic_user', None) - fanout_password = creds.get('fanout_sonic_password', None) - elif os_type == 'eos': - fanout_user = creds.get('fanout_network_user', None) - fanout_password = creds.get('fanout_network_password', None) - elif os_type == 'onyx': - fanout_user = creds.get('fanout_mlnx_user', None) - fanout_password = creds.get('fanout_mlnx_password', None) - elif os_type == 'ixia': - # Skip for ixia device which has no fanout - continue - else: - # when os is mellanox, not supported - pytest.fail("os other than sonic and eos not supported") - - eos_shell_user = None - eos_shell_password = None - if os_type == "eos": - admin_user = creds['fanout_admin_user'] - admin_password = creds['fanout_admin_password'] - eos_shell_user = creds.get('fanout_shell_user', admin_user) - eos_shell_password = creds.get('fanout_shell_password', admin_password) - - fanout = FanoutHost(ansible_adhoc, - os_type, - fanout_host, - 'FanoutLeaf', - fanout_user, - fanout_password, - eos_shell_user=eos_shell_user, - eos_shell_passwd=eos_shell_password) - fanout.dut_hostnames = [dut_host] - fanout_hosts[fanout_host] = fanout - - if fanout.os == 'sonic': - ifs_status = fanout.host.get_interfaces_status() - for key, interface_info in list(ifs_status.items()): - fanout.fanout_port_alias_to_name[interface_info['alias']] = interface_info['interface'] - logging.info("fanout {} fanout_port_alias_to_name {}" - .format(fanout_host, fanout.fanout_port_alias_to_name)) - + + # Create or get fanout object + fanout = create_or_get_fanout(fanout_hosts, fanout_host, dut_host) + if fanout is None: + continue + + # Add Ethernet port mapping: DUT port -> Fanout port fanout.add_port_map(encode_dut_port_name(dut_host, dut_port), fanout_port) - # Add port name to fanout port mapping port if dut_port is alias. - if dut_port in mg_facts['minigraph_port_alias_to_name_map']: + # Handle port alias mapping if available + if dut_port in mg_facts.get('minigraph_port_alias_to_name_map', {}): mapped_port = mg_facts['minigraph_port_alias_to_name_map'][dut_port] # only add the mapped port which isn't in device_conn ports to avoid overwriting port map wrongly, # it happens when an interface has the same name with another alias, for example: @@ -1025,11 +1074,40 @@ def fanouthosts(enhance_inventory, ansible_adhoc, tbinfo, conn_graph_facts, cred # -------------------- # Ethernet108 Ethernet32 # Ethernet32 Ethernet13/1 - if mapped_port not in list(value.keys()): + if mapped_port not in list(ethernet_ports.keys()): fanout.add_port_map(encode_dut_port_name(dut_host, mapped_port), fanout_port) + + # Process Serial connections - if dut_host not in fanout.dut_hostnames: - fanout.dut_hostnames.append(dut_host) + dev_serial_link = conn_graph_facts.get('device_serial_link', {}) + + for dut_host, serial_ports in dev_serial_link.items(): + + duthost = duthosts[dut_host] + + # Skip virtual testbed which has no fanout + if duthost.facts['platform'] == 'x86_64-kvm_x86_64-r0': + logging.info(f"Skipping kvm platform {dut_host} for serial links") + continue + + # Process each Serial port connection + for serial_port_num, link_info in serial_ports.items(): + fanout_host = str(link_info['peerdevice']) + fanout_port = str(link_info['peerport']) + + # Create or get fanout object (reuses same function as Ethernet) + fanout = create_or_get_fanout(fanout_hosts, fanout_host, dut_host) + if fanout is None: + continue + + # Add Serial port mapping with Console prefix + serial_port_key = f"C0_{serial_port_num}" + fanout.add_port_map(encode_dut_port_name(dut_host, serial_port_key), fanout_port) + + logging.debug(f"Added serial port mapping: {dut_host} Console{serial_port_num} -> " + f"{fanout_host}:{fanout_port} (baud={link_info.get('baud_rate', '9600')})") + + logging.info(f"fanouthosts fixture initialized with {len(fanout_hosts)} fanout devices") return fanout_hosts