Extend host topology with NIC type filtering
[csit.git] / resources / libraries / python / topology.py
1 # Copyright (c) 2016 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 """Defines nodes and topology structure."""
15
16 from yaml import load
17
18 from robot.api import logger
19 from robot.libraries.BuiltIn import BuiltIn
20 from robot.api.deco import keyword
21
22 __all__ = ["DICT__nodes", 'Topology']
23
24
25 def load_topo_from_yaml():
26     """Load topology from file defined in "${TOPOLOGY_PATH}" variable.
27
28     :return: Nodes from loaded topology.
29     """
30     topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}")
31
32     with open(topo_path) as work_file:
33         return load(work_file.read())['nodes']
34
35
36 class NodeType(object):
37     """Defines node types used in topology dictionaries."""
38     # Device Under Test (this node has VPP running on it)
39     DUT = 'DUT'
40     # Traffic Generator (this node has traffic generator on it)
41     TG = 'TG'
42     # Virtual Machine (this node running on DUT node)
43     VM = 'VM'
44
45
46 class NodeSubTypeTG(object):
47     # T-Rex traffic generator
48     TREX = 'TREX'
49     # Moongen
50     MOONGEN = 'MOONGEN'
51     # IxNetwork
52     IXNET = 'IXNET'
53
54 DICT__nodes = load_topo_from_yaml()
55
56
57 class Topology(object):
58     """Topology data manipulation and extraction methods.
59
60     Defines methods used for manipulation and extraction of data from
61     the used topology.
62     """
63
64     @staticmethod
65     def get_node_by_hostname(nodes, hostname):
66         """Get node from nodes of the topology by hostname.
67
68         :param nodes: Nodes of the test topology.
69         :param hostname: Host name.
70         :type nodes: dict
71         :type hostname: str
72         :return: Node dictionary or None if not found.
73         """
74         for node in nodes.values():
75             if node['host'] == hostname:
76                 return node
77
78         return None
79
80     @staticmethod
81     def get_links(nodes):
82         """Get list of links(networks) in the topology.
83
84         :param nodes: Nodes of the test topology.
85         :type nodes: dict
86         :return: Links in the topology.
87         :rtype: list
88         """
89         links = []
90
91         for node in nodes.values():
92             for interface in node['interfaces'].values():
93                 link = interface.get('link')
94                 if link is not None:
95                     if link not in links:
96                         links.append(link)
97
98         return links
99
100     @staticmethod
101     def _get_interface_by_key_value(node, key, value):
102         """Return node interface name according to key and value.
103
104         :param node: The node dictionary.
105         :param key: Key by which to select the interface.
106         :param value: Value that should be found using the key.
107         :return:
108         """
109         interfaces = node['interfaces']
110         retval = None
111         for interface in interfaces.values():
112             k_val = interface.get(key)
113             if k_val is not None:
114                 if k_val == value:
115                     retval = interface['name']
116                     break
117         return retval
118
119     def get_interface_by_link_name(self, node, link_name):
120         """Return interface name of link on node.
121
122         This method returns the interface name associated with a given link
123         for a given node.
124
125         :param link_name: Name of the link that a interface is connected to.
126         :param node: The node topology dictionary.
127         :return: Interface name of the interface connected to the given link.
128         :rtype: str
129         """
130         return self._get_interface_by_key_value(node, "link", link_name)
131
132     def get_interfaces_by_link_names(self, node, link_names):
133         """Return dictionary of dictionaries {"interfaceN", interface name}.
134
135         This method returns the interface names associated with given links
136         for a given node.
137
138         :param link_names: List of names of the link that a interface is
139         connected to.
140         :param node: The node topology directory.
141         :return: Dictionary of interface names that are connected to the given
142         links.
143         :rtype: dict
144         """
145         retval = {}
146         interface_key_tpl = "interface{}"
147         interface_number = 1
148         for link_name in link_names:
149             interface_name = self.get_interface_by_link_name(node, link_name)
150             interface_key = interface_key_tpl.format(str(interface_number))
151             retval[interface_key] = interface_name
152             interface_number += 1
153         return retval
154
155     def get_interface_by_sw_index(self, node, sw_index):
156         """Return interface name of link on node.
157
158         This method returns the interface name associated with a software
159         interface index assigned to the interface by vpp for a given node.
160
161         :param sw_index: Sw_index of the link that a interface is connected to.
162         :param node: The node topology dictionary.
163         :return: Interface name of the interface connected to the given link.
164         :rtype: str
165         """
166         return self._get_interface_by_key_value(node, "vpp_sw_index", sw_index)
167
168     @staticmethod
169     def get_interface_sw_index(node, interface):
170         """Get VPP sw_if_index for the interface.
171
172         :param node: Node to get interface sw_if_index on.
173         :param interface: Interface identifier.
174         :type node: dict
175         :type interface: str or int
176         :return: Return sw_if_index or None if not found.
177         """
178         try:
179             return int(interface)
180         except ValueError:
181             for port in node['interfaces'].values():
182                 port_name = port.get('name')
183                 if port_name == interface:
184                     return port.get('vpp_sw_index')
185             return None
186
187     @staticmethod
188     def get_interface_mtu(node, interface):
189         """Get interface MTU.
190
191         Returns physical layer MTU (max. size of Ethernet frame).
192         :param node: Node to get interface MTU on.
193         :param interface: Interface name.
194         :type node: dict
195         :type interface: str
196         :return: MTU or None if not found.
197         :rtype: int
198         """
199         for port in node['interfaces'].values():
200             port_name = port.get('name')
201             if port_name == interface:
202                 return port.get('mtu')
203
204         return None
205
206     @staticmethod
207     def get_interface_mac_by_port_key(node, port_key):
208         """Get MAC address for the interface based on port key.
209
210         :param node: Node to get interface mac on.
211         :param port_key: Dictionary key name of interface.
212         :type node: dict
213         :type port_key: str
214         :return: Return MAC or None if not found.
215         """
216         for port_name, port_data in node['interfaces'].iteritems():
217             if port_name == port_key:
218                 return port_data['mac_address']
219
220         return None
221
222     @staticmethod
223     def get_interface_mac(node, interface):
224         """Get MAC address for the interface.
225
226         :param node: Node to get interface sw_index on.
227         :param interface: Interface name.
228         :type node: dict
229         :type interface: str
230         :return: Return MAC or None if not found.
231         """
232         for port in node['interfaces'].values():
233             port_name = port.get('name')
234             if port_name == interface:
235                 return port.get('mac_address')
236
237         return None
238
239     @staticmethod
240     def get_adjacent_node_and_interface_by_key(nodes_info, node, port_key):
241         """Get node and interface adjacent to specified interface
242         on local network.
243
244         :param nodes_info: Dictionary containing information on all nodes
245         in topology.
246         :param node: Node that contains specified interface.
247         :param port_key: Interface port key.
248         :type nodes_info: dict
249         :type node: dict
250         :type port_key: str
251         :return: Return (node, interface info) tuple or None if not found.
252         :rtype: (dict, dict)
253         """
254         link_name = None
255         # get link name where the interface belongs to
256         for port_name, port_data in node['interfaces'].iteritems():
257             if port_name == 'mgmt':
258                 continue
259             if port_name == port_key:
260                 link_name = port_data['link']
261                 break
262
263         if link_name is None:
264             return None
265
266         # find link
267         for node_data in nodes_info.values():
268             # skip self
269             if node_data['host'] == node['host']:
270                 continue
271             for interface, interface_data \
272                     in node_data['interfaces'].iteritems():
273                 if 'link' not in interface_data:
274                     continue
275                 if interface_data['link'] == link_name:
276                     return node_data, node_data['interfaces'][interface]
277
278     @staticmethod
279     def get_adjacent_node_and_interface(nodes_info, node, interface_name):
280         """Get node and interface adjacent to specified interface
281         on local network.
282
283         :param nodes_info: Dictionary containing information on all nodes
284         in topology.
285         :param node: Node that contains specified interface.
286         :param interface_name: Interface name.
287         :type nodes_info: dict
288         :type node: dict
289         :type interface_name: str
290         :return: Return (node, interface info) tuple or None if not found.
291         :rtype: (dict, dict)
292         """
293         link_name = None
294         # get link name where the interface belongs to
295         for port_name, port_data in node['interfaces'].iteritems():
296             if port_data['name'] == interface_name:
297                 link_name = port_data['link']
298                 break
299
300         if link_name is None:
301             return None
302
303         # find link
304         for node_data in nodes_info.values():
305             # skip self
306             if node_data['host'] == node['host']:
307                 continue
308             for interface, interface_data \
309                     in node_data['interfaces'].iteritems():
310                 if 'link' not in interface_data:
311                     continue
312                 if interface_data['link'] == link_name:
313                     return node_data, node_data['interfaces'][interface]
314
315     @staticmethod
316     def get_interface_pci_addr(node, interface):
317         """Get interface PCI address.
318
319         :param node: Node to get interface PCI address on.
320         :param interface: Interface name.
321         :type node: dict
322         :type interface: str
323         :return: Return PCI address or None if not found.
324         """
325         for port in node['interfaces'].values():
326             if interface == port.get('name'):
327                 return port.get('pci_address')
328         return None
329
330     @staticmethod
331     def get_interface_driver(node, interface):
332         """Get interface driver.
333
334         :param node: Node to get interface driver on.
335         :param interface: Interface name.
336         :type node: dict
337         :type interface: str
338         :return: Return interface driver or None if not found.
339         """
340         for port in node['interfaces'].values():
341             if interface == port.get('name'):
342                 return port.get('driver')
343         return None
344
345     @staticmethod
346     def get_node_link_mac(node, link_name):
347         """Return interface mac address by link name.
348
349         :param node: Node to get interface sw_index on.
350         :param link_name: Link name.
351         :type node: dict
352         :type link_name: str
353         :return: MAC address string.
354         :rtype: str
355         """
356         for port in node['interfaces'].values():
357             if port.get('link') == link_name:
358                 return port.get('mac_address')
359         return None
360
361     @staticmethod
362     def _get_node_active_link_names(node, filter_list=None):
363         """Return list of link names that are other than mgmt links.
364
365         :param node: Node topology dictionary.
366         :param filter_list: Link filter criteria.
367         :type node: dict
368         :type filter_list: list of strings
369         :return: List of strings that represent link names occupied by the node.
370         :rtype: list
371         """
372         interfaces = node['interfaces']
373         link_names = []
374         for interface in interfaces.values():
375             if 'link' in interface:
376                 if (filter_list is not None) and ('model' in interface):
377                     for filt in filter_list:
378                         if filt == interface['model']:
379                             link_names.append(interface['link'])
380                 elif (filter_list is not None) and ('model' not in interface):
381                     logger.trace("Cannot apply filter on interface: {}" \
382                                  .format(str(interface)))
383                 else:
384                     link_names.append(interface['link'])
385         if len(link_names) == 0:
386             link_names = None
387         return link_names
388
389     @keyword('Get active links connecting "${node1}" and "${node2}"')
390     def get_active_connecting_links(self, node1, node2,
391                                     filter_list_node1=None,
392                                     filter_list_node2=None):
393         """Return list of link names that connect together node1 and node2.
394
395         :param node1: Node topology dictionary.
396         :param node2: Node topology dictionary.
397         :param filter_list_node1: Link filter criteria for node1.
398         :param filter_list_node2: Link filter criteria for node2.
399         :type node1: dict
400         :type node2: dict
401         :type filter_list1: list of strings
402         :type filter_list2: list of strings
403         :return: List of strings that represent connecting link names.
404         :rtype: list
405         """
406
407         logger.trace("node1: {}".format(str(node1)))
408         logger.trace("node2: {}".format(str(node2)))
409         node1_links = self._get_node_active_link_names(
410             node1,
411             filter_list=filter_list_node1)
412         node2_links = self._get_node_active_link_names(
413             node2,
414             filter_list=filter_list_node2)
415
416         connecting_links = None
417         if node1_links is None:
418             logger.error("Unable to find active links for node1")
419         elif node2_links is None:
420             logger.error("Unable to find active links for node2")
421         else:
422             connecting_links = list(set(node1_links).intersection(node2_links))
423
424         return connecting_links
425
426     @keyword('Get first active connecting link between node "${node1}" and '
427              '"${node2}"')
428     def get_first_active_connecting_link(self, node1, node2):
429         """
430
431         :param node1: Connected node.
432         :param node2: Connected node.
433         :type node1: dict
434         :type node2: dict
435         :return: Name of link connecting the two nodes together.
436         :rtype: str
437         :raises: RuntimeError
438         """
439         connecting_links = self.get_active_connecting_links(node1, node2)
440         if len(connecting_links) == 0:
441             raise RuntimeError("No links connecting the nodes were found")
442         else:
443             return connecting_links[0]
444
445     @keyword('Get egress interfaces on "${node1}" for link with "${node2}"')
446     def get_egress_interfaces_for_nodes(self, node1, node2):
447         """Get egress interfaces on node1 for link with node2.
448
449         :param node1: First node, node to get egress interface on.
450         :param node2: Second node.
451         :type node1: dict
452         :type node2: dict
453         :return: Egress interfaces.
454         :rtype: list
455         """
456         interfaces = []
457         links = self.get_active_connecting_links(node1, node2)
458         if len(links) == 0:
459             raise RuntimeError('No link between nodes')
460         for interface in node1['interfaces'].values():
461             link = interface.get('link')
462             if link is None:
463                 continue
464             if link in links:
465                 continue
466             name = interface.get('name')
467             if name is None:
468                 continue
469             interfaces.append(name)
470         return interfaces
471
472     @keyword('Get first egress interface on "${node1}" for link with '
473              '"${node2}"')
474     def get_first_egress_interface_for_nodes(self, node1, node2):
475         """Get first egress interface on node1 for link with node2.
476
477         :param node1: First node, node to get egress interface on.
478         :param node2: Second node.
479         :type node1: dict
480         :type node2: dict
481         :return: Egress interface.
482         :rtype: str
483         """
484         interfaces = self.get_egress_interfaces_for_nodes(node1, node2)
485         if not interfaces:
486             raise RuntimeError('No egress interface for nodes')
487         return interfaces[0]
488
489     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
490              ' dut1 "${dut1}" dut2 "${dut2}"')
491     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
492         """Return link combinations used in tests in circular topology.
493
494         For the time being it returns links from the Node path:
495         TG->DUT1->DUT2->TG
496         The naming convention until changed to something more general is
497         implemented is this:
498         DUT1_DUT2_LINK: link name between DUT! and DUT2
499         DUT1_TG_LINK: link name between DUT1 and TG
500         DUT2_TG_LINK: link name between DUT2 and TG
501         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
502         to and from
503         DUT1_BD_LINKS: list of link names that will be connected by the bridge
504         domain on DUT1
505         DUT2_BD_LINKS: list of link names that will be connected by the bridge
506         domain on DUT2
507
508         :param tgen: Traffic generator node data.
509         :param dut1: DUT1 node data.
510         :param dut2: DUT2 node data.
511         :type tgen: dict
512         :type dut1: dict
513         :type dut2: dict
514         :return: Dictionary of possible link combinations.
515         :rtype: dict
516         """
517         # TODO: replace with generic function.
518         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
519         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
520         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
521         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
522         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
523         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
524         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
525                           'DUT1_TG_LINK': dut1_tg_link,
526                           'DUT2_TG_LINK': dut2_tg_link,
527                           'TG_TRAFFIC_LINKS': tg_traffic_links,
528                           'DUT1_BD_LINKS': dut1_bd_links,
529                           'DUT2_BD_LINKS': dut2_bd_links}
530         return topology_links
531
532     @staticmethod
533     def is_tg_node(node):
534         """Find out whether the node is TG.
535
536         :param node: Node to examine.
537         :type node: dict
538         :return: True if node is type of TG, otherwise False.
539         :rtype: bool
540         """
541         return node['type'] == NodeType.TG
542
543     @staticmethod
544     def get_node_hostname(node):
545         """Return host (hostname/ip address) of the node.
546
547         :param node: Node created from topology.
548         :type node: dict
549         :return: Hostname or IP address.
550         :rtype: str
551         """
552         return node['host']