20745eb0a25ff8bcec55fc0655124645f238e931
[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):
363         """Return list of link names that are other than mgmt links.
364
365         :param node: Node topology dictionary.
366         :return: List of strings that represent link names occupied by the node.
367         :rtype: list
368         """
369         interfaces = node['interfaces']
370         link_names = []
371         for interface in interfaces.values():
372             if 'link' in interface:
373                 link_names.append(interface['link'])
374         if len(link_names) == 0:
375             link_names = None
376         return link_names
377
378     @keyword('Get active links connecting "${node1}" and "${node2}"')
379     def get_active_connecting_links(self, node1, node2):
380         """Return list of link names that connect together node1 and node2.
381
382         :param node1: Node topology dictionary.
383         :param node2: Node topology dictionary.
384         :type node1: dict
385         :type node2: dict
386         :return: List of strings that represent connecting link names.
387         :rtype: list
388         """
389
390         logger.trace("node1: {}".format(str(node1)))
391         logger.trace("node2: {}".format(str(node2)))
392         node1_links = self._get_node_active_link_names(node1)
393         node2_links = self._get_node_active_link_names(node2)
394         connecting_links = list(set(node1_links).intersection(node2_links))
395
396         return connecting_links
397
398     @keyword('Get first active connecting link between node "${node1}" and '
399              '"${node2}"')
400     def get_first_active_connecting_link(self, node1, node2):
401         """
402
403         :param node1: Connected node.
404         :param node2: Connected node.
405         :type node1: dict
406         :type node2: dict
407         :return: Name of link connecting the two nodes together.
408         :rtype: str
409         :raises: RuntimeError
410         """
411         connecting_links = self.get_active_connecting_links(node1, node2)
412         if len(connecting_links) == 0:
413             raise RuntimeError("No links connecting the nodes were found")
414         else:
415             return connecting_links[0]
416
417     @keyword('Get egress interfaces on "${node1}" for link with "${node2}"')
418     def get_egress_interfaces_for_nodes(self, node1, node2):
419         """Get egress interfaces on node1 for link with node2.
420
421         :param node1: First node, node to get egress interface on.
422         :param node2: Second node.
423         :type node1: dict
424         :type node2: dict
425         :return: Egress interfaces.
426         :rtype: list
427         """
428         interfaces = []
429         links = self.get_active_connecting_links(node1, node2)
430         if len(links) == 0:
431             raise RuntimeError('No link between nodes')
432         for interface in node1['interfaces'].values():
433             link = interface.get('link')
434             if link is None:
435                 continue
436             if link in links:
437                 continue
438             name = interface.get('name')
439             if name is None:
440                 continue
441             interfaces.append(name)
442         return interfaces
443
444     @keyword('Get first egress interface on "${node1}" for link with '
445              '"${node2}"')
446     def get_first_egress_interface_for_nodes(self, node1, node2):
447         """Get first egress interface 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 interface.
454         :rtype: str
455         """
456         interfaces = self.get_egress_interfaces_for_nodes(node1, node2)
457         if not interfaces:
458             raise RuntimeError('No egress interface for nodes')
459         return interfaces[0]
460
461     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
462              ' dut1 "${dut1}" dut2 "${dut2}"')
463     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
464         """Return link combinations used in tests in circular topology.
465
466         For the time being it returns links from the Node path:
467         TG->DUT1->DUT2->TG
468         The naming convention until changed to something more general is
469         implemented is this:
470         DUT1_DUT2_LINK: link name between DUT! and DUT2
471         DUT1_TG_LINK: link name between DUT1 and TG
472         DUT2_TG_LINK: link name between DUT2 and TG
473         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
474         to and from
475         DUT1_BD_LINKS: list of link names that will be connected by the bridge
476         domain on DUT1
477         DUT2_BD_LINKS: list of link names that will be connected by the bridge
478         domain on DUT2
479
480         :param tgen: Traffic generator node data.
481         :param dut1: DUT1 node data.
482         :param dut2: DUT2 node data.
483         :type tgen: dict
484         :type dut1: dict
485         :type dut2: dict
486         :return: Dictionary of possible link combinations.
487         :rtype: dict
488         """
489         # TODO: replace with generic function.
490         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
491         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
492         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
493         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
494         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
495         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
496         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
497                           'DUT1_TG_LINK': dut1_tg_link,
498                           'DUT2_TG_LINK': dut2_tg_link,
499                           'TG_TRAFFIC_LINKS': tg_traffic_links,
500                           'DUT1_BD_LINKS': dut1_bd_links,
501                           'DUT2_BD_LINKS': dut2_bd_links}
502         return topology_links
503
504     @staticmethod
505     def is_tg_node(node):
506         """Find out whether the node is TG.
507
508         :param node: Node to examine.
509         :type node: dict
510         :return: True if node is type of TG, otherwise False.
511         :rtype: bool
512         """
513         return node['type'] == NodeType.TG
514
515     @staticmethod
516     def get_node_hostname(node):
517         """Return host (hostname/ip address) of the node.
518
519         :param node: Node created from topology.
520         :type node: dict
521         :return: Hostname or IP address.
522         :rtype: str
523         """
524         return node['host']