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