VXLAN test with dot1q tagging.
[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_if_index for the interface.
168
169         :param node: Node to get interface sw_if_index on.
170         :param interface: Interface identifier.
171         :type node: dict
172         :type interface: str or int
173         :return: Return sw_if_index or None if not found.
174         """
175         try:
176             return int(interface)
177         except ValueError:
178             for port in node['interfaces'].values():
179                 port_name = port.get('name')
180                 if port_name == interface:
181                     return port.get('vpp_sw_index')
182             return None
183
184     @staticmethod
185     def get_interface_mtu(node, interface):
186         """Get interface MTU.
187
188         Returns physical layer MTU (max. size of Ethernet frame).
189         :param node: Node to get interface MTU on.
190         :param interface: Interface name.
191         :type node: dict
192         :type interface: str
193         :return: MTU or None if not found.
194         :rtype: int
195         """
196         for port in node['interfaces'].values():
197             port_name = port.get('name')
198             if port_name == interface:
199                 return port.get('mtu')
200
201         return None
202
203     @staticmethod
204     def get_interface_mac_by_port_key(node, port_key):
205         """Get MAC address for the interface based on port key.
206
207         :param node: Node to get interface mac on.
208         :param port_key: Dictionary key name of interface.
209         :type node: dict
210         :type port_key: str
211         :return: Return MAC or None if not found.
212         """
213         for port_name, port_data in node['interfaces'].iteritems():
214             if port_name == port_key:
215                 return port_data['mac_address']
216
217         return None
218
219     @staticmethod
220     def get_interface_mac(node, interface):
221         """Get MAC address for the interface.
222
223         :param node: Node to get interface sw_index on.
224         :param interface: Interface name.
225         :type node: dict
226         :type interface: str
227         :return: Return MAC or None if not found.
228         """
229         for port in node['interfaces'].values():
230             port_name = port.get('name')
231             if port_name == interface:
232                 return port.get('mac_address')
233
234         return None
235
236     @staticmethod
237     def get_adjacent_node_and_interface_by_key(nodes_info, node, port_key):
238         """Get node and interface adjacent to specified interface
239         on local network.
240
241         :param nodes_info: Dictionary containing information on all nodes
242         in topology.
243         :param node: Node that contains specified interface.
244         :param port_key: Interface port key.
245         :type nodes_info: dict
246         :type node: dict
247         :type port_key: str
248         :return: Return (node, interface info) tuple or None if not found.
249         :rtype: (dict, dict)
250         """
251         link_name = None
252         # get link name where the interface belongs to
253         for port_name, port_data in node['interfaces'].iteritems():
254             if port_name == 'mgmt':
255                 continue
256             if port_name == port_key:
257                 link_name = port_data['link']
258                 break
259
260         if link_name is None: 
261             return None
262
263         # find link
264         for node_data in nodes_info.values():
265             # skip self
266             if node_data['host'] == node['host']:
267                 continue
268             for interface, interface_data \
269                     in node_data['interfaces'].iteritems():
270                 if 'link' not in interface_data:
271                     continue
272                 if interface_data['link'] == link_name:
273                     return node_data, node_data['interfaces'][interface]
274
275     @staticmethod
276     def get_adjacent_node_and_interface(nodes_info, node, interface_name):
277         """Get node and interface adjacent to specified interface
278         on local network.
279
280         :param nodes_info: Dictionary containing information on all nodes
281         in topology.
282         :param node: Node that contains specified interface.
283         :param interface_name: Interface name.
284         :type nodes_info: dict
285         :type node: dict
286         :type interface_name: str
287         :return: Return (node, interface info) tuple or None if not found.
288         :rtype: (dict, dict)
289         """
290         link_name = None
291         # get link name where the interface belongs to
292         for port_name, port_data in node['interfaces'].iteritems():
293             if port_name == 'mgmt':
294                 continue
295             if port_data['name'] == interface_name:
296                 link_name = port_data['link']
297                 break
298
299         if link_name is None:
300             return None
301
302         # find link
303         for node_data in nodes_info.values():
304             # skip self
305             if node_data['host'] == node['host']:
306                 continue
307             for interface, interface_data \
308                     in node_data['interfaces'].iteritems():
309                 if 'link' not in interface_data:
310                     continue
311                 if interface_data['link'] == link_name:
312                     return node_data, node_data['interfaces'][interface]
313
314     @staticmethod
315     def get_interface_pci_addr(node, interface):
316         """Get interface PCI address.
317
318         :param node: Node to get interface PCI address on.
319         :param interface: Interface name.
320         :type node: dict
321         :type interface: str
322         :return: Return PCI address or None if not found.
323         """
324         for port in node['interfaces'].values():
325             if interface == port.get('name'):
326                 return port.get('pci_address')
327         return None
328
329     @staticmethod
330     def get_interface_driver(node, interface):
331         """Get interface driver.
332
333         :param node: Node to get interface driver on.
334         :param interface: Interface name.
335         :type node: dict
336         :type interface: str
337         :return: Return interface driver or None if not found.
338         """
339         for port in node['interfaces'].values():
340             if interface == port.get('name'):
341                 return port.get('driver')
342         return None
343
344     @staticmethod
345     def get_node_link_mac(node, link_name):
346         """Return interface mac address by link name
347
348         :param node: Node to get interface sw_index on
349         :param link_name: link name
350         :type node: dict
351         :type link_name: string
352         :return: mac address string
353         """
354         for port in node['interfaces'].values():
355             if port.get('link') == link_name:
356                 return port.get('mac_address')
357         return None
358
359     @staticmethod
360     def _get_node_active_link_names(node):
361         """Return list of link names that are other than mgmt links
362
363         :param node: node topology dictionary
364         :return: list of strings that represent link names occupied by the node
365         """
366         interfaces = node['interfaces']
367         link_names = []
368         for interface in interfaces.values():
369             if 'link' in interface:
370                 link_names.append(interface['link'])
371         if len(link_names) == 0:
372             link_names = None
373         return link_names
374
375     @keyword('Get active links connecting "${node1}" and "${node2}"')
376     def get_active_connecting_links(self, node1, node2):
377         """Return list of link names that connect together node1 and node2
378
379         :param node1: node topology dictionary
380         :param node2: node topology dictionary
381         :return: list of strings that represent connecting link names
382         """
383
384         logger.trace("node1: {}".format(str(node1)))
385         logger.trace("node2: {}".format(str(node2)))
386         node1_links = self._get_node_active_link_names(node1)
387         node2_links = self._get_node_active_link_names(node2)
388         connecting_links = list(set(node1_links).intersection(node2_links))
389
390         return connecting_links
391
392     @keyword('Get first active connecting link between node "${node1}" and '
393              '"${node2}"')
394     def get_first_active_connecting_link(self, node1, node2):
395         """
396
397         :param node1: Connected node
398         :type node1: dict
399         :param node2: Connected node
400         :type node2: dict
401         :return: name of link connecting the two nodes together
402         :raises: RuntimeError
403         """
404
405         connecting_links = self.get_active_connecting_links(node1, node2)
406         if len(connecting_links) == 0:
407             raise RuntimeError("No links connecting the nodes were found")
408         else:
409             return connecting_links[0]
410
411     @keyword('Get egress interfaces on "${node1}" for link with "${node2}"')
412     def get_egress_interfaces_for_nodes(self, node1, node2):
413         """Get egress interfaces on node1 for link with node2.
414
415         :param node1: First node, node to get egress interface on.
416         :param node2: Second node.
417         :type node1: dict
418         :type node2: dict
419         :return: Egress interfaces.
420         :rtype: list
421         """
422         interfaces = []
423         links = self.get_active_connecting_links(node1, node2)
424         if len(links) == 0:
425             raise RuntimeError('No link between nodes')
426         for interface in node1['interfaces'].values():
427             link = interface.get('link')
428             if link is None:
429                 continue
430             if link in links:
431                 continue
432             name = interface.get('name')
433             if name is None:
434                 continue
435             interfaces.append(name)
436         return interfaces
437
438     @keyword('Get first egress interface on "${node1}" for link with '
439              '"${node2}"')
440     def get_first_egress_interface_for_nodes(self, node1, node2):
441         """Get first egress interface on node1 for link with node2.
442
443         :param node1: First node, node to get egress interface on.
444         :param node2: Second node.
445         :type node1: dict
446         :type node2: dict
447         :return: Egress interface.
448         :rtype: str
449         """
450         interfaces = self.get_egress_interfaces_for_nodes(node1, node2)
451         if not interfaces:
452             raise RuntimeError('No egress interface for nodes')
453         return interfaces[0]
454
455     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
456              ' dut1 "${dut1}" dut2 "${dut2}"')
457     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
458         """Return link combinations used in tests in circular topology.
459
460         For the time being it returns links from the Node path:
461         TG->DUT1->DUT2->TG
462         :param tgen: traffic generator node data
463         :param dut1: DUT1 node data
464         :param dut2: DUT2 node data
465         :type tgen: dict
466         :type dut1: dict
467         :type dut2: dict
468         :return: dictionary of possible link combinations
469         the naming convention until changed to something more general is
470         implemented is this:
471         DUT1_DUT2_LINK: link name between DUT! and DUT2
472         DUT1_TG_LINK: link name between DUT1 and TG
473         DUT2_TG_LINK: link name between DUT2 and TG
474         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
475         to and from
476         DUT1_BD_LINKS: list of link names that will be connected by the bridge
477         domain on DUT1
478         DUT2_BD_LINKS: list of link names that will be connected by the bridge
479         domain on DUT2
480         """
481         # TODO: replace with generic function.
482         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
483         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
484         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
485         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
486         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
487         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
488         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
489                           'DUT1_TG_LINK': dut1_tg_link,
490                           'DUT2_TG_LINK': dut2_tg_link,
491                           'TG_TRAFFIC_LINKS': tg_traffic_links,
492                           'DUT1_BD_LINKS': dut1_bd_links,
493                           'DUT2_BD_LINKS': dut2_bd_links}
494         return topology_links
495
496     @staticmethod
497     def is_tg_node(node):
498         """Find out whether the node is TG
499
500         :param node: node to examine
501         :return: True if node is type of TG; False otherwise
502         """
503         return node['type'] == NodeType.TG
504
505     @staticmethod
506     def get_node_hostname(node):
507         """Return host (hostname/ip address) of the node.
508
509         :param node: Node created from topology.
510         :type node: dict
511         :return: host as 'str' type
512         """
513         return node['host']