X-Git-Url: https://gerrit.fd.io/r/gitweb?p=csit.git;a=blobdiff_plain;f=resources%2Flibraries%2Fpython%2Ftopology.py;h=c39e5afabb97c17c89bca4d3469f556da5e77ece;hp=3ced69d7fe1bee79522702ad7c8a5bd4502c4625;hb=HEAD;hpb=e6cb3709bd670738471fff40ce13dcb5aff8692f diff --git a/resources/libraries/python/topology.py b/resources/libraries/python/topology.py index 3ced69d7fe..22ed3666c3 100644 --- a/resources/libraries/python/topology.py +++ b/resources/libraries/python/topology.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cisco and/or its affiliates. +# Copyright (c) 2024 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: @@ -13,56 +13,334 @@ """Defines nodes and topology structure.""" -from resources.libraries.python.parsers.JsonParser import JsonParser -from resources.libraries.python.VatExecutor import VatExecutor -from resources.libraries.python.ssh import SSH -from resources.libraries.python.InterfaceSetup import InterfaceSetup +import re + +from collections import Counter + +from yaml import safe_load + from robot.api import logger -from robot.libraries.BuiltIn import BuiltIn -from robot.api.deco import keyword -from yaml import load +from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError -__all__ = ["DICT__nodes", 'Topology'] +from resources.libraries.python.Constants import Constants + +__all__ = [ + u"DICT__nodes", u"Topology", u"NodeType", u"SocketType", u"NodeSubTypeTG" +] def load_topo_from_yaml(): - """Loads topology from file defined in "${TOPOLOGY_PATH}" variable + """Load topology from file defined in "${TOPOLOGY_PATH}" variable. - :return: nodes from loaded topology + :returns: Nodes from loaded topology. """ - topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}") + try: + topo_path = BuiltIn().get_variable_value(u"${TOPOLOGY_PATH}") + except RobotNotRunningError: + return '' with open(topo_path) as work_file: - return load(work_file.read())['nodes'] + return safe_load(work_file.read())[u"nodes"] -class NodeType(object): - """Defines node types used in topology dictionaries""" +class NodeType: + """Defines node types used in topology dictionaries.""" # Device Under Test (this node has VPP running on it) - DUT = 'DUT' + DUT = u"DUT" # Traffic Generator (this node has traffic generator on it) - TG = 'TG' + # pylint: disable=invalid-name + TG = u"TG" + # Virtual Machine (this node running on DUT node) + # pylint: disable=invalid-name + VM = u"VM" + -class NodeSubTypeTG(object): - #T-Rex traffic generator - TREX = 'TREX' +class NodeSubTypeTG: + """Defines node sub-type TG - traffic generator.""" + # T-Rex traffic generator + TREX = u"TREX" # Moongen - MOONGEN = 'MOONGEN' - #IxNetwork - IXNET = 'IXNET' + MOONGEN = u"MOONGEN" + # IxNetwork + IXNET = u"IXNET" + + +class SocketType: + """Defines socket types used in topology dictionaries.""" + # VPP Socket PAPI + PAPI = u"PAPI" + # VPP PAPI Stats (legacy option until stats are migrated to Socket PAPI) + STATS = u"STATS" + # VPP Socket CLI + CLI = u"CLI" + DICT__nodes = load_topo_from_yaml() -class Topology(object): - """Topology data manipulation and extraction methods +class Topology: + """Topology data manipulation and extraction methods. Defines methods used for manipulation and extraction of data from - the used topology. + the active topology. + + "Active topology" contains initially data from the topology file and can be + extended with additional data from the DUTs like internal interface indexes + or names. Additional data which can be filled to the active topology are + + - additional internal representation (index, name, ...) + - operational data (dynamic ports) + + To access the port data it is recommended to use a port key because the key + does not rely on the data retrieved from nodes, this allows to call most of + the methods without having filled active topology with internal nodes data. """ + @staticmethod + def add_node_item(node, value, path): + """Add item to topology node. + + :param node: Topology node. + :param value: Value to insert. + :param path: Path where to insert item. + :type node: dict + :type value: str + :type path: list + """ + if len(path) == 1: + node[path[0]] = value + return + if path[0] not in node: + node[path[0]] = dict() + elif isinstance(node[path[0]], str): + node[path[0]] = dict() if node[path[0]] == u"" \ + else {node[path[0]]: u""} + Topology.add_node_item(node[path[0]], value, path[1:]) + + @staticmethod + def add_new_port(node, ptype): + """Add new port to the node to active topology. + + :param node: Node to add new port on. + :param ptype: Port type, used as key prefix. + :type node: dict + :type ptype: str + :returns: Port key or None + :rtype: string or None + """ + max_ports = 1000000 + iface = None + for i in range(1, max_ports): + if node[u"interfaces"].get(str(ptype) + str(i)) is None: + iface = str(ptype) + str(i) + node[u"interfaces"][iface] = dict() + break + return iface + + @staticmethod + def remove_port(node, iface_key): + """Remove required port from active topology. + + :param node: Node to remove port on. + :param: iface_key: Topology key of the interface. + :type node: dict + :type iface_key: str + :returns: Nothing + """ + try: + node[u"interfaces"].pop(iface_key) + except KeyError: + pass + + @staticmethod + def remove_all_ports(node, ptype): + """Remove all ports with ptype as prefix. + + :param node: Node to remove ports on. + :param: ptype: Port type, used as key prefix. + :type node: dict + :type ptype: str + :returns: Nothing + """ + for if_key in list(node[u"interfaces"]): + if if_key.startswith(str(ptype)): + node[u"interfaces"].pop(if_key) + + @staticmethod + def remove_all_added_ports_on_all_duts_from_topology(nodes): + """Remove all added ports on all DUT nodes in the topology. + + :param nodes: Nodes in the topology. + :type nodes: dict + :returns: Nothing + """ + port_types = ( + u"subinterface", u"vlan_subif", u"memif", u"tap", u"vhost", + u"loopback", u"gre_tunnel", u"vxlan_tunnel", u"eth_bond", + u"eth_avf", u"eth_rdma", u"geneve_tunnel", u"eth_af_xdp", + u"gtpu_tunnel" + ) + + for node_data in nodes.values(): + if node_data[u"type"] == NodeType.DUT: + for ptype in port_types: + Topology.remove_all_ports(node_data, ptype) + + @staticmethod + def remove_all_vif_ports(node): + """Remove all Virtual Interfaces on DUT node. + + :param node: Node to remove VIF ports on. + :type node: dict + :returns: Nothing + """ + reg_ex = re.compile(r"port\d+_vif\d+") + for if_key in list(node[u"interfaces"]): + if re.match(reg_ex, if_key): + node[u"interfaces"].pop(if_key) + + @staticmethod + def remove_all_added_vif_ports_on_all_duts_from_topology(nodes): + """Remove all added Virtual Interfaces on all DUT nodes in + the topology. + + :param nodes: Nodes in the topology. + :type nodes: dict + :returns: Nothing + """ + for node_data in nodes.values(): + if node_data[u"type"] == NodeType.DUT: + Topology.remove_all_vif_ports(node_data) + + @staticmethod + def update_interface_sw_if_index(node, iface_key, sw_if_index): + """Update sw_if_index on the interface from the node. + + :param node: Node to update sw_if_index on. + :param iface_key: Topology key of the interface. + :param sw_if_index: Internal index to store. + :type node: dict + :type iface_key: str + :type sw_if_index: int + """ + node[u"interfaces"][iface_key][u"vpp_sw_index"] = int(sw_if_index) + + @staticmethod + def update_interface_name(node, iface_key, name): + """Update name on the interface from the node. + + :param node: Node to update name on. + :param iface_key: Topology key of the interface. + :param name: Interface name to store. + :type node: dict + :type iface_key: str + :type name: str + """ + node[u"interfaces"][iface_key][u"name"] = str(name) + + @staticmethod + def update_interface_mac_address(node, iface_key, mac_address): + """Update mac_address on the interface from the node. + + :param node: Node to update MAC on. + :param iface_key: Topology key of the interface. + :param mac_address: MAC address. + :type node: dict + :type iface_key: str + :type mac_address: str + """ + node[u"interfaces"][iface_key][u"mac_address"] = str(mac_address) + + @staticmethod + def update_interface_pci_address(node, iface_key, pci_address): + """Update pci_address on the interface from the node. + + :param node: Node to update PCI on. + :param iface_key: Topology key of the interface. + :param pci_address: PCI address. + :type node: dict + :type iface_key: str + :type pci_address: str + """ + node[u"interfaces"][iface_key][u"pci_address"] = str(pci_address) + + @staticmethod + def update_interface_vlan(node, iface_key, vlan): + """Update VLAN on the interface from the node. - def __init__(self): - pass + :param node: Node to update VLAN on. + :param iface_key: Topology key of the interface. + :param vlan: VLAN ID. + :type node: dict + :type iface_key: str + :type vlan: str + """ + node[u"interfaces"][iface_key][u"vlan"] = int(vlan) + + @staticmethod + def update_interface_vhost_socket(node, iface_key, vhost_socket): + """Update vhost socket name on the interface from the node. + + :param node: Node to update socket name on. + :param iface_key: Topology key of the interface. + :param vhost_socket: Path to named socket on node. + :type node: dict + :type iface_key: str + :type vhost_socket: str + """ + node[u"interfaces"][iface_key][u"vhost_socket"] = str(vhost_socket) + + @staticmethod + def update_interface_memif_socket(node, iface_key, memif_socket): + """Update memif socket name on the interface from the node. + + :param node: Node to update socket name on. + :param iface_key: Topology key of the interface. + :param memif_socket: Path to named socket on node. + :type node: dict + :type iface_key: str + :type memif_socket: str + """ + node[u"interfaces"][iface_key][u"memif_socket"] = str(memif_socket) + + @staticmethod + def update_interface_memif_id(node, iface_key, memif_id): + """Update memif ID on the interface from the node. + + :param node: Node to update memif ID on. + :param iface_key: Topology key of the interface. + :param memif_id: Memif interface ID. + :type node: dict + :type iface_key: str + :type memif_id: str + """ + node[u"interfaces"][iface_key][u"memif_id"] = str(memif_id) + + @staticmethod + def update_interface_memif_role(node, iface_key, memif_role): + """Update memif role on the interface from the node. + + :param node: Node to update memif role on. + :param iface_key: Topology key of the interface. + :param memif_role: Memif role. + :type node: dict + :type iface_key: str + :type memif_role: str + """ + node[u"interfaces"][iface_key][u"memif_role"] = str(memif_role) + + @staticmethod + def update_interface_tap_dev_name(node, iface_key, dev_name): + """Update device name on the tap interface from the node. + + :param node: Node to update tap device name on. + :param iface_key: Topology key of the interface. + :param dev_name: Device name of the tap interface. + :type node: dict + :type iface_key: str + :type dev_name: str + :returns: Nothing + """ + node[u"interfaces"][iface_key][u"dev_name"] = str(dev_name) @staticmethod def get_node_by_hostname(nodes, hostname): @@ -72,10 +350,10 @@ class Topology(object): :param hostname: Host name. :type nodes: dict :type hostname: str - :return: Node dictionary or None if not found. + :returns: Node dictionary or None if not found. """ for node in nodes.values(): - if node['host'] == hostname: + if node[u"host"] == hostname: return node return None @@ -86,14 +364,14 @@ class Topology(object): :param nodes: Nodes of the test topology. :type nodes: dict - :return: Links in the topology. + :returns: Links in the topology. :rtype: list """ - links = [] + links = list() for node in nodes.values(): - for interface in node['interfaces'].values(): - link = interface.get('link') + for interface in node[u"interfaces"].values(): + link = interface.get(u"link") if link is not None: if link not in links: links.append(link) @@ -101,354 +379,385 @@ class Topology(object): return links @staticmethod - def _get_interface_by_key_value(node, key, value): - """ Return node interface name according to key and value - - :param node: :param node: the node dictionary - :param key: key by which to select the interface. - :param value: value that should be found using the key. - :return: + def _get_interface_by_key_value(node, key, value, subsequent=False): + """Return node interface key from topology file + according to key and value. + + :param node: The node dictionary. + :param key: Key by which to select the interface. + :param value: Value that should be found using the key. + :param subsequent: Use second interface of the link. Useful for + back-to-back links. Default: False + :type node: dict + :type key: string + :type value: string + :type subsequent: bool + :returns: Interface key from topology file + :rtype: string """ - - interfaces = node['interfaces'] + interfaces = node[u"interfaces"] retval = None - for interface in interfaces.values(): - k_val = interface.get(key) + for if_key, if_val in interfaces.items(): + k_val = if_val.get(key) if k_val is not None: if k_val == value: - retval = interface['name'] - break + if subsequent: + subsequent = False + else: + retval = if_key + break return retval - def get_interface_by_link_name(self, node, link_name): - """Return interface name of link on node. + @staticmethod + def get_interface_by_name(node, iface_name): + """Return interface key based on name from DUT/TG. - This method returns the interface name asociated with a given link - for a given node. - :param link_name: name of the link that a interface is connected to. - :param node: the node topology dictionary - :return: interface name of the interface connected to the given link + This method returns interface key based on interface name + retrieved from the DUT, or TG. + + :param node: The node topology dictionary. + :param iface_name: Interface name (string form). + :type node: dict + :type iface_name: string + :returns: Interface key. + :rtype: str """ + return Topology._get_interface_by_key_value(node, u"name", iface_name) - return self._get_interface_by_key_value(node, "link", link_name) + @staticmethod + def get_interface_by_link_name(node, link_name, subsequent=False): + """Return interface key of link on node. + + This method returns the interface name associated with a given link + for a given node. + + :param node: The node topology dictionary. + :param link_name: Name of the link that a interface is connected to. + :param subsequent: Use second interface of the link. Useful for + back-to-back links. Default: False + :type node: dict + :type link_name: string + :type subsequent: bool + :returns: Interface key of the interface connected to the given link. + :rtype: str + """ + return Topology._get_interface_by_key_value( + node, u"link", link_name, subsequent=subsequent + ) def get_interfaces_by_link_names(self, node, link_names): - """Return dictionary of dicitonaries {"interfaceN", interface name}. + """Return dictionary of dictionaries {"interfaceN", interface name}. - This method returns the interface names asociated with given links + This method returns the interface names associated with given links for a given node. - The resulting dictionary can be then used to with VatConfigGenerator - to generate a VAT script with proper interface names. - :param link_names: list of names of the link that a interface is - connected to. - :param node: the node topology directory - :return: dictionary of interface names that are connected to the given - links - """ - retval = {} - interface_key_tpl = "interface{}" + :param node: The node topology directory. + :param link_names: List of names of the link that a interface is + connected to. + :type node: dict + :type link_names: list + :returns: Dictionary of interface names that are connected to the given + links. + :rtype: dict + """ + retval = dict() interface_number = 1 for link_name in link_names: - interface_name = self.get_interface_by_link_name(node, link_name) - interface_key = interface_key_tpl.format(str(interface_number)) - retval[interface_key] = interface_name + interface = self.get_interface_by_link_name(node, link_name) + retval[f"interface{str(interface_number)}"] = \ + self.get_interface_name(node, interface) interface_number += 1 return retval - def get_interface_by_sw_index(self, node, sw_index): + @staticmethod + def get_interface_by_sw_index(node, sw_if_index): """Return interface name of link on node. - This method returns the interface name asociated with a software index - assigned to the interface by vpp for a given node. - :param sw_index: sw_index of the link that a interface is connected to. - :param node: the node topology dictionary - :return: interface name of the interface connected to the given link - """ - - return self._get_interface_by_key_value(node, "vpp_sw_index", sw_index) - - @staticmethod - def convert_mac_to_number_list(mac_address): - """Convert mac address string to list of decimal numbers. - - Converts a : separated mac address to decimal number list as used - in json interface dump. - :param mac_address: string mac address - :return: list representation of mac address - """ - - list_mac = [] - for num in mac_address.split(":"): - list_mac.append(int(num, 16)) - return list_mac - - def _extract_vpp_interface_by_mac(self, interfaces_list, mac_address): - """Returns interface dictionary from interface_list by mac address. - - Extracts interface dictionary from all of the interfaces in interfaces - list parsed from json according to mac_address of the interface - :param interfaces_list: dictionary of all interfaces parsed from json - :param mac_address: string mac address of interface we are looking for - :return: interface dictionary from json - """ - - interface_dict = {} - list_mac_address = self.convert_mac_to_number_list(mac_address) - logger.trace(list_mac_address.__str__()) - for interface in interfaces_list: - # TODO: create vat json integrity checking and move there - if "l2_address" not in interface: - raise KeyError( - "key l2_address not found in interface dict." - "Probably input list is not parsed from correct VAT " - "json output.") - if "l2_address_length" not in interface: - raise KeyError( - "key l2_address_length not found in interface " - "dict. Probably input list is not parsed from correct " - "VAT json output.") - mac_from_json = interface["l2_address"][:6] - if mac_from_json == list_mac_address: - if interface["l2_address_length"] != 6: - raise ValueError("l2_address_length value is not 6.") - interface_dict = interface - break - return interface_dict - - def vpp_interface_name_from_json_by_mac(self, json_data, mac_address): - """Return vpp interface name string from VAT interface dump json output - - Extracts the name given to an interface by VPP. - These interface names differ from what you would see if you - used the ipconfig or similar command. - Required json data can be obtained by calling : - VatExecutor.execute_script_json_out("dump_interfaces.vat", node) - :param json_data: string json data from sw_interface_dump VAT command - :param mac_address: string containing mac address of interface - whose vpp name we wish to discover. - :return: string vpp interface name - """ - - interfaces_list = JsonParser().parse_data(json_data) - # TODO: checking if json data is parsed correctly - interface_dict = self._extract_vpp_interface_by_mac(interfaces_list, - mac_address) - interface_name = interface_dict["interface_name"] - return interface_name - - def _update_node_interface_data_from_json(self, node, interface_dump_json): - """ Update node vpp data in node__DICT from json interface dump. - - This method updates vpp interface names and sw indexexs according to - interface mac addresses found in interface_dump_json - :param node: node dictionary - :param interface_dump_json: json output from dump_interface_list VAT - command - """ - - interface_list = JsonParser().parse_data(interface_dump_json) - for ifc in node['interfaces'].values(): - if 'link' not in ifc: - continue - if_mac = ifc['mac_address'] - interface_dict = self._extract_vpp_interface_by_mac(interface_list, - if_mac) - ifc['name'] = interface_dict["interface_name"] - ifc['vpp_sw_index'] = interface_dict["sw_if_index"] + This method returns the interface name associated with a software + interface index assigned to the interface by vpp for a given node. - def update_vpp_interface_data_on_node(self, node): - """Update vpp generated interface data for a given node in DICT__nodes - - Updates interface names, software index numbers and any other details - generated specifically by vpp that are unknown before testcase run. - :param node: Node selected from DICT__nodes + :param node: The node topology dictionary. + :param sw_if_index: sw_if_index of the link that a interface is + connected to. + :type node: dict + :type sw_if_index: int + :returns: Interface name of the interface connected to the given link. + :rtype: str """ - - vat_executor = VatExecutor() - vat_executor.execute_script_json_out("dump_interfaces.vat", node) - interface_dump_json = vat_executor.get_script_stdout() - self._update_node_interface_data_from_json(node, - interface_dump_json) + return Topology._get_interface_by_key_value( + node, u"vpp_sw_index", sw_if_index + ) @staticmethod - def update_tg_interface_data_on_node(node): - """Update interface name for TG/linux node in DICT__nodes + def get_interface_sw_index(node, iface_key): + """Get VPP sw_if_index for the interface using interface key. - :param node: Node selected from DICT__nodes. + :param node: Node to get interface sw_if_index on. + :param iface_key: Interface key from topology file, or sw_if_index. :type node: dict + :type iface_key: str/int + :returns: Return sw_if_index or None if not found. + :rtype: int or None + """ + try: + if isinstance(iface_key, str): + return node[u"interfaces"][iface_key].get(u"vpp_sw_index") + # TODO: use only iface_key, do not use integer + return int(iface_key) + except (KeyError, ValueError): + return None - .. note:: - # for dev in `ls /sys/class/net/`; - > do echo "\"`cat /sys/class/net/$dev/address`\": \"$dev\""; done - "52:54:00:9f:82:63": "eth0" - "52:54:00:77:ae:a9": "eth1" - "52:54:00:e1:8a:0f": "eth2" - "00:00:00:00:00:00": "lo" + @staticmethod + def get_interface_sw_index_by_name(node, iface_name): + """Get VPP sw_if_index for the interface using interface name. - .. todo:: parse lshw -json instead + :param node: Node to get interface sw_if_index on. + :param iface_name: Interface name. + :type node: dict + :type iface_name: str + :returns: Return sw_if_index or None if not found. + :raises TypeError: If provided interface name is not a string. """ - # First setup interface driver specified in yaml file - InterfaceSetup.tg_set_interfaces_default_driver(node) + try: + if not isinstance(iface_name, str): + raise TypeError(u"Interface name must be a string.") + iface_key = Topology.get_interface_by_name(node, iface_name) + return node[u"interfaces"][iface_key].get(u"vpp_sw_index") + except (KeyError, ValueError): + return None - # Get interface names - ssh = SSH() - ssh.connect(node) + @staticmethod + def get_interface_mtu(node, iface_key): + """Get interface MTU. - cmd = 'for dev in `ls /sys/class/net/`; do echo "\\"`cat ' \ - '/sys/class/net/$dev/address`\\": \\"$dev\\""; done;' + Returns physical layer MTU (max. size of Ethernet frame). + :param node: Node to get interface MTU on. + :param iface_key: Interface key from topology file. + :type node: dict + :type iface_key: str + :returns: MTU or None if not found. + :rtype: int + """ + try: + return node[u"interfaces"][iface_key].get(u"mtu") + except KeyError: + return None - (ret_code, stdout, _) = ssh.exec_command(cmd) - if int(ret_code) != 0: - raise Exception('Get interface name and MAC failed') - tmp = "{" + stdout.rstrip().replace('\n', ',') + "}" - interfaces = JsonParser().parse_data(tmp) - for if_k, if_v in node['interfaces'].items(): - if if_k == 'mgmt': - continue - name = interfaces.get(if_v['mac_address']) - if name is None: - continue - if_v['name'] = name + @staticmethod + def get_interface_name(node, iface_key): + """Get interface name (retrieved from DUT/TG). - # Set udev rules for interfaces - InterfaceSetup.tg_set_interfaces_udev_rules(node) + Returns name in string format, retrieved from the node. + :param node: Node to get interface name on. + :param iface_key: Interface key from topology file. + :type node: dict + :type iface_key: str + :returns: Interface name or None if not found. + :rtype: str + """ + try: + return node[u"interfaces"][iface_key].get(u"name") + except KeyError: + return None - def update_all_interface_data_on_all_nodes(self, nodes): - """ Update interface names on all nodes in DICT__nodes + @staticmethod + def convert_interface_reference_to_key(node, interface): + """Takes interface reference in any format + (name, link name, interface key or sw_if_index) + and converts to interface key using Topology methods. + + :param node: Node in topology. + :param interface: Name, sw_if_index, link name or key of an interface + on the node. + :type node: dict + :type interface: str or int - :param nodes: Nodes in the topology. - :type nodes: dict + :returns: Interface key. + :rtype: str - This method updates the topology dictionary by querying interface lists - of all nodes mentioned in the topology dictionary. - It does this by dumping interface list to json output from all devices - using vpe_api_test, and pairing known information from topology - (mac address/pci address of interface) to state from VPP. - For TG/linux nodes add interface name only. + :raises TypeError: If provided with invalid interface argument. + :raises RuntimeError: If the interface does not exist in topology. """ - for node_data in nodes.values(): - if node_data['type'] == NodeType.DUT: - self.update_vpp_interface_data_on_node(node_data) - elif node_data['type'] == NodeType.TG: - self.update_tg_interface_data_on_node(node_data) + if isinstance(interface, int): + key = Topology.get_interface_by_sw_index(node, interface) + if key is None: + raise RuntimeError( + f"Interface with sw_if_index={interface} does not exist " + f"in topology." + ) + elif interface in Topology.get_node_interfaces(node): + key = interface + elif interface in Topology.get_links({u"dut": node}): + key = Topology.get_interface_by_link_name(node, interface) + elif isinstance(interface, str): + key = Topology.get_interface_by_name(node, interface) + if key is None: + raise RuntimeError( + f"Interface with key, name or link name \"{interface}\" " + f"does not exist in topology." + ) + else: + raise TypeError( + u"Type of interface argument must be integer or string." + ) + return key @staticmethod - def get_interface_sw_index(node, interface): - """Get VPP sw_index for the interface. - - :param node: Node to get interface sw_index on. - :param interface: Interface name. + def convert_interface_reference(node, interface, wanted_format): + """Takes interface reference in any format + (name, link name, topology key or sw_if_index) and returns + its equivalent in the desired format. + + :param node: Node in topology. + :param interface: Name, sw_if_index, link name or key of an interface + on the node. + :param wanted_format: Format of return value wanted. + Valid options are: sw_if_index, key, name. :type node: dict - :type interface: str - :return: Return sw_index or None if not found. + :type interface: str or int + :type wanted_format: str + :returns: Interface name, interface key or sw_if_index. + :rtype: str or int + :raises TypeError, ValueError: If provided with invalid arguments. + :raises RuntimeError: If the interface does not exist in topology. """ - for port in node['interfaces'].values(): - port_name = port.get('name') - if port_name is None: - continue - if port_name == interface: - return port.get('vpp_sw_index') - return None + key = Topology.convert_interface_reference_to_key(node, interface) + + conversions = { + u"key": lambda x, y: y, + u"name": Topology.get_interface_name, + u"sw_if_index": Topology.get_interface_sw_index + } + + try: + return conversions[wanted_format](node, key) + except KeyError: + raise ValueError( + f"Unrecognized return value wanted: {wanted_format}." + f"Valid options are key, name, sw_if_index" + ) @staticmethod - def get_interface_mac_by_port_key(node, port_key): - """Get MAC address for the interface based on port key. + def get_interface_numa_node(node, iface_key): + """Get interface numa node. - :param node: Node to get interface mac on. - :param port_key: Dictionary key name of interface. + Returns physical relation to numa node, numa_id. + + :param node: Node to get numa id on. + :param iface_key: Interface key from topology file. :type node: dict - :type port_key: str - :return: Return MAC or None if not found. + :type iface_key: str + :returns: numa node id, None if not available. + :rtype: int """ - for port_name, port_data in node['interfaces'].iteritems(): - if port_name == port_key: - return port_data['mac_address'] + try: + return node[u"interfaces"][iface_key].get(u"numa_node") + except KeyError: + return None - return None + @staticmethod + def get_interfaces_numa_node(node, *iface_keys): + """Get numa node on which are located most of the interfaces. + + Return numa node with highest count of interfaces provided as arguments. + Return 0 if the interface does not have numa_node information available. + If all interfaces have unknown location (-1), then return 0. + If most of interfaces have unknown location (-1), but there are + some interfaces with known location, then return the second most + location of the provided interfaces. + + :param node: Node from DICT__nodes. + :param iface_keys: Interface keys for lookup. + :type node: dict + :type iface_keys: strings + :returns: Numa node of most given interfaces or 0. + :rtype: int + """ + numa_list = [] + for if_key in iface_keys: + try: + numa_list.append(node[u"interfaces"][if_key].get(u"numa_node")) + except KeyError: + pass + + numa_cnt_mc = Counter(numa_list).most_common() + + if numa_cnt_mc and numa_cnt_mc[0][0] != -1: + return numa_cnt_mc[0][0] + if len(numa_cnt_mc) > 1 and numa_cnt_mc[0][0] == -1: + return numa_cnt_mc[1][0] + return 0 @staticmethod - def get_interface_mac(node, interface): + def get_interface_mac(node, iface_key): """Get MAC address for the interface. - :param node: Node to get interface sw_index on. - :param interface: Interface name. + :param node: Node to get interface mac on. + :param iface_key: Interface key from topology file. :type node: dict - :type interface: str - :return: Return MAC or None if not found. + :type iface_key: str + :returns: Return MAC or None if not found. """ - for port in node['interfaces'].values(): - port_name = port.get('name') - if port_name is None: - continue - if port_name == interface: - return port.get('mac_address') - - return None + try: + return node[u"interfaces"][iface_key].get(u"mac_address") + except KeyError: + return None @staticmethod - def get_adjacent_node_and_interface_by_key(nodes_info, node, port_key): - """Get node and interface adjacent to specified interface - on local network. + def get_interface_ip4(node, iface_key): + """Get IP4 address for the interface. - :param nodes_info: Dictionary containing information on all nodes - in topology. - :param node: Node that contains specified interface. - :param port_key: Interface port key. - :type nodes_info: dict + :param node: Node to get interface mac on. + :param iface_key: Interface key from topology file. :type node: dict - :type port_key: str - :return: Return (node, interface info) tuple or None if not found. - :rtype: (dict, dict) + :type iface_key: str + :returns: Return IP4 or None if not found. """ - link_name = None - # get link name where the interface belongs to - for port_name, port_data in node['interfaces'].iteritems(): - if port_name == 'mgmt': - continue - if port_name == port_key: - link_name = port_data['link'] - break - - if link_name is None: + try: + return node[u"interfaces"][iface_key].get(u"ip4_address") + except KeyError: return None - # find link - for node_data in nodes_info.values(): - # skip self - if node_data['host'] == node['host']: - continue - for interface, interface_data \ - in node_data['interfaces'].iteritems(): - if 'link' not in interface_data: - continue - if interface_data['link'] == link_name: - return node_data, node_data['interfaces'][interface] + @staticmethod + def get_interface_ip4_prefix_length(node, iface_key): + """Get IP4 address prefix length for the interface. + + :param node: Node to get prefix length on. + :param iface_key: Interface key from topology file. + :type node: dict + :type iface_key: str + :returns: Prefix length from topology file or the default + IP4 prefix length if not found. + :rtype: int + :raises: KeyError if iface_key is not found. + """ + return node[u"interfaces"][iface_key].get(u"ip4_prefix_length", \ + Constants.DEFAULT_IP4_PREFIX_LENGTH) @staticmethod - def get_adjacent_node_and_interface(nodes_info, node, interface_name): + def get_adjacent_node_and_interface(nodes_info, node, iface_key): """Get node and interface adjacent to specified interface on local network. :param nodes_info: Dictionary containing information on all nodes - in topology. + in topology. :param node: Node that contains specified interface. - :param interface_name: Interface name. + :param iface_key: Interface key from topology file. :type nodes_info: dict :type node: dict - :type interface_name: str - :return: Return (node, interface info) tuple or None if not found. - :rtype: (dict, dict) + :type iface_key: str + :returns: Return (node, interface_key) tuple or None if not found. + :rtype: (dict, str) """ link_name = None # get link name where the interface belongs to - for port_name, port_data in node['interfaces'].iteritems(): - if port_name == 'mgmt': + for if_key, if_val in node[u"interfaces"].items(): + if if_key == u"mgmt": continue - if port_data['name'] == interface_name: - link_name = port_data['link'] + if if_key == iface_key: + link_name = if_val[u"link"] break if link_name is None: @@ -457,171 +766,238 @@ class Topology(object): # find link for node_data in nodes_info.values(): # skip self - if node_data['host'] == node['host']: + l_hash = node_data[u"host"]+str(node_data[u"port"]) + r_hash = node[u"host"]+str(node[u"port"]) + if l_hash == r_hash: continue - for interface, interface_data \ - in node_data['interfaces'].iteritems(): - if 'link' not in interface_data: + for if_key, if_val \ + in node_data[u"interfaces"].items(): + if u"link" not in if_val: continue - if interface_data['link'] == link_name: - return node_data, node_data['interfaces'][interface] + if if_val[u"link"] == link_name: + return node_data, if_key + return None @staticmethod - def get_interface_pci_addr(node, interface): + def get_interface_pci_addr(node, iface_key): """Get interface PCI address. :param node: Node to get interface PCI address on. - :param interface: Interface name. + :param iface_key: Interface key from topology file. :type node: dict - :type interface: str - :return: Return PCI address or None if not found. + :type iface_key: str + :returns: Return PCI address or None if not found. """ - for port in node['interfaces'].values(): - if interface == port.get('name'): - return port.get('pci_address') - return None + try: + return node[u"interfaces"][iface_key].get(u"pci_address") + except KeyError: + return None @staticmethod - def get_interface_driver(node, interface): + def get_interface_driver(node, iface_key): """Get interface driver. :param node: Node to get interface driver on. - :param interface: Interface name. + :param iface_key: Interface key from topology file. :type node: dict - :type interface: str - :return: Return interface driver or None if not found. + :type iface_key: str + :returns: Return interface driver or None if not found. """ - for port in node['interfaces'].values(): - if interface == port.get('name'): - return port.get('driver') - return None + try: + return node[u"interfaces"][iface_key].get(u"driver") + except KeyError: + return None + + @staticmethod + def get_interface_vlan(node, iface_key): + """Get interface vlan. + + :param node: Node to get interface driver on. + :param iface_key: Interface key from topology file. + :type node: dict + :type iface_key: str + :returns: Return interface vlan or None if not found. + """ + try: + return node[u"interfaces"][iface_key].get(u"vlan") + except KeyError: + return None + + @staticmethod + def get_node_interfaces(node): + """Get all node interfaces. + + :param node: Node to get list of interfaces from. + :type node: dict + :returns: Return list of keys of all interfaces. + :rtype: list + """ + return node[u"interfaces"].keys() @staticmethod def get_node_link_mac(node, link_name): - """Return interface mac address by link name + """Return interface mac address by link name. - :param node: Node to get interface sw_index on - :param link_name: link name + :param node: Node to get interface sw_if_index on. + :param link_name: Link name. :type node: dict - :type link_name: string - :return: mac address string + :type link_name: str + :returns: MAC address string. + :rtype: str """ - for port in node['interfaces'].values(): - if port.get('link') == link_name: - return port.get('mac_address') + for port in node[u"interfaces"].values(): + if port.get(u"link") == link_name: + return port.get(u"mac_address") return None @staticmethod - def _get_node_active_link_names(node): - """Returns list of link names that are other than mgmt links + def _get_node_active_link_names(node, filter_list=None, topo_has_dut=True): + """Return list of link names that are other than mgmt links. - :param node: node topology dictionary - :return: list of strings that represent link names occupied by the node + :param node: Node topology dictionary. + :param filter_list: Link filter criteria. + :param topo_has_dut: Whether we require back-to-back links. + :type node: dict + :type filter_list: list of strings + :type topo_has_dut: bool + :returns: List of link names occupied by the node. + :rtype: None or list of string """ - interfaces = node['interfaces'] + interfaces = node[u"interfaces"] link_names = [] for interface in interfaces.values(): - if 'link' in interface: - link_names.append(interface['link']) - if len(link_names) == 0: + if u"link" in interface: + if (filter_list is not None) and (u"model" in interface): + for filt in filter_list: + if filt == interface[u"model"]: + link_names.append(interface[u"link"]) + elif (filter_list is not None) and (u"model" not in interface): + logger.trace( + f"Cannot apply filter on interface: {str(interface)}" + ) + else: + link_names.append(interface[u"link"]) + if not link_names: link_names = None + if not topo_has_dut: + new_link_names = list() + for link_name in link_names: + count = 0 + for interface in interfaces.values(): + link = interface.get(u"link", None) + if link == link_name: + count += 1 + if count == 2: + new_link_names.append(link_name) + link_names = new_link_names return link_names - @keyword('Get active links connecting "${node1}" and "${node2}"') - def get_active_connecting_links(self, node1, node2): - """Returns list of link names that connect together node1 and node2 + def get_active_connecting_links( + self, node1, node2, filter_list_node1=None, filter_list_node2=None): + """Return list of link names that connect together node1 and node2. - :param node1: node topology dictionary - :param node2: node topology dictionary - :return: list of strings that represent connecting link names + :param node1: Node topology dictionary. + :param node2: Node topology dictionary. + :param filter_list_node1: Link filter criteria for node1. + :param filter_list_node2: Link filter criteria for node2. + :type node1: dict + :type node2: dict + :type filter_list_node1: list of strings + :type filter_list_node2: list of strings + :returns: List of strings that represent connecting link names. + :rtype: list """ - logger.trace("node1: {}".format(str(node1))) - logger.trace("node2: {}".format(str(node2))) - node1_links = self._get_node_active_link_names(node1) - node2_links = self._get_node_active_link_names(node2) - connecting_links = list(set(node1_links).intersection(node2_links)) + if node1 != node2: + node1_links = self._get_node_active_link_names( + node1, filter_list=filter_list_node1 + ) + node2_links = self._get_node_active_link_names( + node2, filter_list=filter_list_node2 + ) + else: + # Looking for back-to-back links. + node1_links = self._get_node_active_link_names( + node1, filter_list=filter_list_node1, topo_has_dut=False + ) + node2_links = node1_links + + connecting_links = None + if node1_links is None: + logger.error(u"Unable to find active links for node1") + elif node2_links is None: + logger.error(u"Unable to find active links for node2") + else: + # Not using set operations, as we need deterministic order. + connecting_links = [ + link for link in node1_links if link in node2_links + ] return connecting_links - @keyword('Get first active connecting link between node "${node1}" and ' - '"${node2}"') def get_first_active_connecting_link(self, node1, node2): - """ + """Get first link connecting the two nodes together. - :param node1: Connected node + :param node1: Connected node. + :param node2: Connected node. :type node1: dict - :param node2: Connected node :type node2: dict - :return: name of link connecting the two nodes together - :raises: RuntimeError + :returns: Name of a link connecting the two nodes together. + :rtype: str + :raises RuntimeError: If no links are found. """ - connecting_links = self.get_active_connecting_links(node1, node2) - if len(connecting_links) == 0: - raise RuntimeError("No links connecting the nodes were found") - else: - return connecting_links[0] + if not connecting_links: + raise RuntimeError(u"No links connecting the nodes were found") + return connecting_links[0] - @keyword('Get egress interfaces on "${node1}" for link with "${node2}"') - def get_egress_interfaces_for_nodes(self, node1, node2): + def get_egress_interfaces_name_for_nodes(self, node1, node2): """Get egress interfaces on node1 for link with node2. :param node1: First node, node to get egress interface on. :param node2: Second node. :type node1: dict :type node2: dict - :return: Engress interfaces. + :returns: Egress interfaces. :rtype: list """ - interfaces = [] + interfaces = list() links = self.get_active_connecting_links(node1, node2) - if len(links) == 0: - raise RuntimeError('No link between nodes') - for interface in node1['interfaces'].values(): - link = interface.get('link') + if not links: + raise RuntimeError(u"No link between nodes") + for interface in node1[u"interfaces"].values(): + link = interface.get(u"link") if link is None: continue if link in links: continue - name = interface.get('name') + name = interface.get(u"name") if name is None: continue interfaces.append(name) return interfaces - @keyword('Get first egress interface on "${node1}" for link with ' - '"${node2}"') def get_first_egress_interface_for_nodes(self, node1, node2): """Get first egress interface on node1 for link with node2. - :param node1: First node, node to get egress interface on. + :param node1: First node, node to get egress interface name on. :param node2: Second node. :type node1: dict :type node2: dict - :return: Engress interface. + :returns: Egress interface name. :rtype: str """ - interfaces = self.get_egress_interfaces_for_nodes(node1, node2) + interfaces = self.get_egress_interfaces_name_for_nodes(node1, node2) if not interfaces: - raise RuntimeError('No engress interface for nodes') + raise RuntimeError(u"No egress interface for nodes") return interfaces[0] - @keyword('Get link data useful in circular topology test from tg "${tgen}"' - ' dut1 "${dut1}" dut2 "${dut2}"') def get_links_dict_from_nodes(self, tgen, dut1, dut2): - """Returns link combinations used in tests in circular topology. + """Return link combinations used in tests in circular topology. For the time being it returns links from the Node path: TG->DUT1->DUT2->TG - :param tg: traffic generator node data - :param dut1: DUT1 node data - :param dut2: DUT2 node data - :type tg: dict - :type dut1: dict - :type dut2: dict - :return: dictionary of possible link combinations - the naming convention until changed to something more general is + The naming convention until changed to something more general is implemented is this: DUT1_DUT2_LINK: link name between DUT! and DUT2 DUT1_TG_LINK: link name between DUT1 and TG @@ -632,6 +1008,15 @@ class Topology(object): domain on DUT1 DUT2_BD_LINKS: list of link names that will be connected by the bridge domain on DUT2 + + :param tgen: Traffic generator node data. + :param dut1: DUT1 node data. + :param dut2: DUT2 node data. + :type tgen: dict + :type dut1: dict + :type dut2: dict + :returns: Dictionary of possible link combinations. + :rtype: dict """ # TODO: replace with generic function. dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2) @@ -640,10 +1025,168 @@ class Topology(object): tg_traffic_links = [dut1_tg_link, dut2_tg_link] dut1_bd_links = [dut1_dut2_link, dut1_tg_link] dut2_bd_links = [dut1_dut2_link, dut2_tg_link] - topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link, - 'DUT1_TG_LINK': dut1_tg_link, - 'DUT2_TG_LINK': dut2_tg_link, - 'TG_TRAFFIC_LINKS': tg_traffic_links, - 'DUT1_BD_LINKS': dut1_bd_links, - 'DUT2_BD_LINKS': dut2_bd_links} + topology_links = { + u"DUT1_DUT2_LINK": dut1_dut2_link, + u"DUT1_TG_LINK": dut1_tg_link, + u"DUT2_TG_LINK": dut2_tg_link, + u"TG_TRAFFIC_LINKS": tg_traffic_links, + u"DUT1_BD_LINKS": dut1_bd_links, + u"DUT2_BD_LINKS": dut2_bd_links + } return topology_links + + @staticmethod + def is_tg_node(node): + """Find out whether the node is TG. + + :param node: Node to examine. + :type node: dict + :returns: True if node is type of TG, otherwise False. + :rtype: bool + """ + return node[u"type"] == NodeType.TG + + @staticmethod + def get_node_hostname(node): + """Return host (hostname/ip address) of the node. + + :param node: Node created from topology. + :type node: dict + :returns: Hostname or IP address. + :rtype: str + """ + return node[u"host"] + + @staticmethod + def get_node_arch(node): + """Return arch of the node. + Default to x86_64 if no arch present + + :param node: Node created from topology. + :type node: dict + :returns: Node architecture + :rtype: str + """ + try: + return node[u"arch"] + except KeyError: + node[u"arch"] = u"x86_64" + return u"x86_64" + + @staticmethod + def get_cryptodev(node): + """Return Crytodev configuration of the node. + + :param node: Node created from topology. + :type node: dict + :returns: Cryptodev configuration string. + :rtype: str + """ + try: + return node[u"cryptodev"] + except KeyError: + return None + + def get_bus(node): + """Return bus configuration of the node. + + :param node: Node created from topology. + :type node: dict + :returns: bus configuration string. + :rtype: str + """ + try: + return node[u"bus"] + except KeyError: + return None + + @staticmethod + def get_uio_driver(node): + """Return uio-driver configuration of the node. + + :param node: Node created from topology. + :type node: dict + :returns: uio-driver configuration string. + :rtype: str + """ + try: + return node[u"uio_driver"] + except KeyError: + return None + + @staticmethod + def set_interface_numa_node(node, iface_key, numa_node_id): + """Set interface numa_node location. + + :param node: Node to set numa_node on. + :param iface_key: Interface key from topology file. + :param numa_node_id: Num_node ID. + :type node: dict + :type iface_key: str + :type numa_node_id: int + :returns: Return iface_key or None if not found. + """ + try: + node[u"interfaces"][iface_key][u"numa_node"] = numa_node_id + return iface_key + except KeyError: + return None + + @staticmethod + def add_new_socket(node, socket_type, socket_id, socket_path): + """Add socket file of specific SocketType and ID to node. + + :param node: Node to add socket on. + :param socket_type: Socket type. + :param socket_id: Socket id, currently equals to unique node key. + :param socket_path: Socket absolute path. + :type node: dict + :type socket_type: SocketType + :type socket_id: str + :type socket_path: str + """ + path = [u"sockets", socket_type, socket_id] + Topology.add_node_item(node, socket_path, path) + + @staticmethod + def del_node_socket_id(node, socket_type, socket_id): + """Delete socket of specific SocketType and ID from node. + + :param node: Node to delete socket from. + :param socket_type: Socket type. + :param socket_id: Socket id, currently equals to unique node key. + :type node: dict + :type socket_type: SocketType + :type socket_id: str + """ + node[u"sockets"][socket_type].pop(socket_id) + + @staticmethod + def get_node_sockets(node, socket_type=None): + """Get node socket files. + + :param node: Node to get sockets from. + :param socket_type: Socket type or None for all sockets. + :type node: dict + :type socket_type: SocketType + :returns: Node sockets or None if not found. + :rtype: dict + """ + try: + if socket_type: + return node[u"sockets"][socket_type] + return node[u"sockets"] + except KeyError: + return None + + @staticmethod + def clean_sockets_on_all_nodes(nodes): + """Remove temporary socket files from topology file. + + :param nodes: SUT nodes. + :type node: dict + """ + for node in nodes.values(): + if u"sockets" in list(node.keys()): + # Containers are disconnected and destroyed already. + node.pop(u"sockets")