fix(tgtest): filter by TG NIC model
[csit.git] / resources / libraries / python / NodePath.py
1 # Copyright (c) 2022 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Path utilities library for nodes in the topology."""
15
16 from resources.libraries.python.topology import Topology
17
18
19 class NodePath:
20     """Path utilities for nodes in the topology.
21
22     :Example:
23
24     node1--link1-->node2--link2-->node3--link3-->node2--link4-->node1
25     RobotFramework:
26     | Library | resources/libraries/python/NodePath.py
27
28     | Path test
29     | | [Arguments] | ${node1} | ${node2} | ${node3}
30     | | Append Node | ${nodes1}
31     | | Append Node | ${nodes2}
32     | | Append Nodes | ${nodes3} | ${nodes2}
33     | | Append Node | ${nodes1}
34     | | Compute Path | ${FALSE}
35     | | ${first_int} | ${node}= | First Interface
36     | | ${last_int} | ${node}= | Last Interface
37     | | ${first_ingress} | ${node}= | First Ingress Interface
38     | | ${last_egress} | ${node}= | Last Egress Interface
39     | | ${next} | ${node}= | Next Interface
40
41     Python:
42     >>> from NodePath import NodePath
43     >>> path = NodePath()
44     >>> path.append_node(node1)
45     >>> path.append_node(node2)
46     >>> path.append_nodes(node3, node2)
47     >>> path.append_node(node1)
48     >>> path.compute_path()
49     >>> (interface, node) = path.first_interface()
50     >>> (interface, node) = path.last_interface()
51     >>> (interface, node) = path.first_ingress_interface()
52     >>> (interface, node) = path.last_egress_interface()
53     >>> (interface, node) = path.next_interface()
54     """
55
56     def __init__(self):
57         self._nodes = []
58         self._nodes_filter = []
59         self._links = []
60         self._path = []
61         self._path_iter = []
62
63     def append_node(self, node, filter_list=None):
64         """Append node to the path.
65
66         :param node: Node to append to the path.
67         :param filter_list: Filter criteria list.
68         :type node: dict
69         :type filter_list: list of strings
70         """
71         self._nodes_filter.append(filter_list)
72         self._nodes.append(node)
73
74     def append_nodes(self, *nodes, filter_list=None):
75         """Append nodes to the path.
76
77         :param nodes: Nodes to append to the path.
78         :param filter_list: Filter criteria list.
79         :type nodes: dict
80         :type filter_list: list of strings
81
82         .. note:: Node order does matter.
83         """
84         for node in nodes:
85             self.append_node(node, filter_list=filter_list)
86
87     def clear_path(self):
88         """Clear path."""
89         self._nodes = []
90         self._nodes_filter = []
91         self._links = []
92         self._path = []
93         self._path_iter = []
94
95     def compute_path(self, always_same_link=True, topo_has_dut=True):
96         """Compute path for added nodes.
97
98         .. note:: First add at least two nodes to the topology.
99
100         :param always_same_link: If True use always same link between two nodes
101             in path. If False use different link (if available)
102             between two nodes if one link was used before.
103         :param topo_has_dut: If False we want to test back to back test on TG.
104         :type always_same_link: bool
105         :type topo_has_dut: bool
106         :raises RuntimeError: If not enough nodes for path.
107         """
108         nodes = self._nodes
109         if len(nodes) < 2 and topo_has_dut:
110             raise RuntimeError(u"Not enough nodes to compute path")
111
112         for idx in range(0, len(nodes) - 1):
113             topo = Topology()
114             node1 = nodes[idx]
115             n1_list = self._nodes_filter[idx]
116             if topo_has_dut:
117                 node2 = nodes[idx + 1]
118                 n2_list = self._nodes_filter[idx + 1]
119             else:
120                 node2 = node1
121                 n2_list = n1_list
122
123             links = topo.get_active_connecting_links(
124                 node1, node2, filter_list_node1=n1_list,
125                 filter_list_node2=n2_list
126             )
127             if not links:
128                 raise RuntimeError(
129                     f"No link between {node1[u'host']} and {node2[u'host']}"
130                 )
131
132             # Not using set operations, as we need deterministic order.
133             if always_same_link:
134                 l_set = [link for link in links if link in self._links]
135             else:
136                 l_set = [link for link in links if link not in self._links]
137                 if not l_set:
138                     raise RuntimeError(
139                         f"No free link between {node1[u'host']} and "
140                         f"{node2[u'host']}, all links already used"
141                     )
142
143             if not l_set:
144                 link = links[0]
145             else:
146                 link = l_set[0]
147
148             self._links.append(link)
149
150             use_subsequent = not topo_has_dut
151             interface1 = topo.get_interface_by_link_name(node1, link)
152             interface2 = topo.get_interface_by_link_name(node2, link,
153                                                          use_subsequent)
154             self._path.append((interface1, node1))
155             self._path.append((interface2, node2))
156
157         self._path_iter.extend(self._path)
158         self._path_iter.reverse()
159
160     def next_interface(self):
161         """Path interface iterator.
162
163         :returns: Interface and node or None if not next interface.
164         :rtype: tuple (str, dict)
165
166         .. note:: Call compute_path before.
167         """
168         if not self._path_iter:
169             return None, None
170         return self._path_iter.pop()
171
172     def first_interface(self):
173         """Return first interface on the path.
174
175         :returns: Interface and node.
176         :rtype: tuple (str, dict)
177
178         .. note:: Call compute_path before.
179         """
180         if not self._path:
181             raise RuntimeError(u"No path for topology")
182         return self._path[0]
183
184     def last_interface(self):
185         """Return last interface on the path.
186
187         :returns: Interface and node.
188         :rtype: tuple (str, dict)
189
190         .. note:: Call compute_path before.
191         """
192         if not self._path:
193             raise RuntimeError(u"No path for topology")
194         return self._path[-1]
195
196     def first_ingress_interface(self):
197         """Return first ingress interface on the path.
198
199         :returns: Interface and node.
200         :rtype: tuple (str, dict)
201
202         .. note:: Call compute_path before.
203         """
204         if not self._path:
205             raise RuntimeError(u"No path for topology")
206         return self._path[1]
207
208     def last_egress_interface(self):
209         """Return last egress interface on the path.
210
211         :returns: Interface and node.
212         :rtype: tuple (str, dict)
213
214         .. note:: Call compute_path before.
215         """
216         if not self._path:
217             raise RuntimeError(u"No path for topology")
218         return self._path[-2]
219
220     def compute_circular_topology(
221             self, nodes, filter_list=None, nic_pfs=1,
222             always_same_link=False, topo_has_tg=True, topo_has_dut=True):
223         """Return computed circular path.
224
225         :param nodes: Nodes to append to the path.
226         :param filter_list: Filter criteria list.
227         :param nic_pfs: Number of PF of NIC.
228         :param always_same_link: If True use always same link between two nodes
229             in path. If False use different link (if available)
230             between two nodes if one link was used before.
231         :param topo_has_tg: If True, the topology has a TG node. If False,
232             the topology consists entirely of DUT nodes.
233         :param topo_has_dut: If True, the topology has a DUT node(s). If False,
234             the topology consists entirely of TG nodes.
235         :type nodes: dict
236         :type filter_list: list of strings
237         :type nic_pfs: int
238         :type always_same_link: bool
239         :type topo_has_tg: bool
240         :type topo_has_dut: bool
241         :returns: Topology information dictionary.
242         :rtype: dict
243         :raises RuntimeError: If unsupported combination of parameters.
244         """
245         t_dict = dict()
246         if topo_has_dut:
247             duts = [key for key in nodes if u"DUT" in key]
248             t_dict[u"duts"] = duts
249             t_dict[u"duts_count"] = len(duts)
250             t_dict[u"int"] = u"pf"
251
252         for _ in range(0, nic_pfs // 2):
253             if topo_has_tg:
254                 if topo_has_dut:
255                     self.append_node(nodes[u"TG"])
256                 else:
257                     self.append_node(nodes[u"TG"], filter_list=filter_list)
258             if topo_has_dut:
259                 for dut in duts:
260                     self.append_node(nodes[dut], filter_list=filter_list)
261         if topo_has_tg:
262             if topo_has_dut:
263                 self.append_node(nodes[u"TG"])
264             else:
265                 self.append_node(nodes[u"TG"], filter_list=filter_list)
266         self.compute_path(always_same_link, topo_has_dut)
267
268         n_idx = 0 # node index
269         t_idx = 1 # TG interface index
270         d_idx = 0 # DUT interface index
271         prev_host = None
272         while True:
273             interface, node = self.next_interface()
274             if not interface:
275                 break
276             if topo_has_tg and node.get(u"type") == u"TG":
277                 n_pfx = f"TG" # node prefix
278                 p_pfx = f"pf{t_idx}" # physical interface prefix
279                 i_pfx = f"if{t_idx}" # [backwards compatible] interface prefix
280                 n_idx = 0
281                 t_idx = t_idx + 1
282             elif topo_has_tg and topo_has_dut:
283                 # Each node has 2 interfaces, starting with 1
284                 # Calculate prefixes appropriately for current
285                 # path topology nomenclature:
286                 #   tg1_if1 -> dut1_if1 -> dut1_if2 ->
287                 #        [dut2_if1 -> dut2_if2 ...] -> tg1_if2
288                 n_pfx = f"DUT{n_idx // 2 + 1}"
289                 p_pfx = f"pf{d_idx % 2 + t_idx - 1}"
290                 i_pfx = f"if{d_idx % 2 + t_idx - 1}"
291                 n_idx = n_idx + 1
292                 d_idx = d_idx + 1
293             elif not topo_has_tg and always_same_link:
294                 this_host = node.get(u"host")
295                 if prev_host != this_host:
296                     # When moving to a new host in the path,
297                     # increment the node index (n_idx) and
298                     # reset DUT interface index (d_idx) to 1.
299                     n_idx = n_idx + 1
300                     d_idx = 1
301                 n_pfx = f"DUT{n_idx}"
302                 p_pfx = f"pf{d_idx}"
303                 i_pfx = f"if{d_idx}"
304                 d_idx = d_idx + 1
305             else:
306                 raise RuntimeError(u"Unsupported combination of paramters")
307
308             t_dict[f"{n_pfx}"] = node
309             t_dict[f"{n_pfx}_{p_pfx}"] = [interface]
310             t_dict[f"{n_pfx}_{p_pfx}_mac"] = \
311                 [Topology.get_interface_mac(node, interface)]
312             t_dict[f"{n_pfx}_{p_pfx}_vlan"] = \
313                 [Topology.get_interface_vlan(node, interface)]
314             t_dict[f"{n_pfx}_{p_pfx}_pci"] = \
315                 [Topology.get_interface_pci_addr(node, interface)]
316             t_dict[f"{n_pfx}_{p_pfx}_ip4_addr"] = \
317                 [Topology.get_interface_ip4(node, interface)]
318             t_dict[f"{n_pfx}_{p_pfx}_ip4_prefix"] = \
319                 [Topology.get_interface_ip4_prefix_length(node, interface)]
320             if f"{n_pfx}_pf_pci" not in t_dict:
321                 t_dict[f"{n_pfx}_pf_pci"] = []
322             t_dict[f"{n_pfx}_pf_pci"].append(
323                 Topology.get_interface_pci_addr(node, interface))
324             if f"{n_pfx}_pf_keys" not in t_dict:
325                 t_dict[f"{n_pfx}_pf_keys"] = []
326             t_dict[f"{n_pfx}_pf_keys"].append(interface)
327             # Backward compatibility below
328             t_dict[f"{n_pfx.lower()}_{i_pfx}"] = interface
329             t_dict[f"{n_pfx.lower()}_{i_pfx}_mac"] = \
330                 Topology.get_interface_mac(node, interface)
331             t_dict[f"{n_pfx.lower()}_{i_pfx}_pci"] = \
332                 Topology.get_interface_pci_addr(node, interface)
333             t_dict[f"{n_pfx.lower()}_{i_pfx}_ip4_addr"] = \
334                 Topology.get_interface_ip4(node, interface)
335             t_dict[f"{n_pfx.lower()}_{i_pfx}_ip4_prefix"] = \
336                 Topology.get_interface_ip4_prefix_length(node, interface)
337
338         self.clear_path()
339         return t_dict