Use interface key instead of interface name.
[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 active topology.
62
63     "Active topology" contains initially data from the topology file and can be
64     extended with additional data from the DUTs like internal interface indexes
65     or names. Additional data which can be filled to the active topology are
66         - additional internal representation (index, name, ...)
67         - operational data (dynamic ports)
68
69     To access the port data it is recommended to use a port key because the key
70     does not rely on the data retrieved from nodes, this allows to call most of
71     the methods without having filled active topology with internal nodes data.
72     """
73
74     @staticmethod
75     def add_new_port(node, ptype):
76         """Add new port to the node to active topology.
77
78         :param node: Node to add new port on.
79         :param ptype: Port type, used as key prefix.
80         :type node: dict
81         :type ptype: str
82         :return: Port key or None
83         :rtype: string or None
84         """
85         max_ports = 1000000
86         iface = None
87         for i in range(1, max_ports):
88             if node['interfaces'].get(str(ptype) + str(i)) is None:
89                 iface = str(ptype) + str(i)
90                 node['interfaces'][iface] = dict()
91                 break
92         return iface
93
94     @staticmethod
95     def remove_all_ports(node, ptype):
96         """Remove all ports with ptype as prefix.
97
98         :param node: Node to remove ports on.
99         :param: ptype: Port type, used as key prefix.
100         :type node: dict
101         :type ptype: str
102         :return: Nothing
103         """
104         for if_key in list(node['interfaces']):
105             if if_key.startswith(str(ptype)):
106                 node['interfaces'].pop(if_key)
107
108     @staticmethod
109     def update_interface_sw_if_index(node, iface_key, sw_if_index):
110         """Update sw_if_index on the interface from the node.
111
112         :param node: Node to update sw_if_index on.
113         :param iface_key: Topology key of the interface.
114         :param sw_if_index: Internal index to store.
115         :type node: dict
116         :type iface_key: str
117         :type sw_if_index: int
118         """
119         node['interfaces'][iface_key]['vpp_sw_index'] = int(sw_if_index)
120
121     @staticmethod
122     def update_interface_mac_address(node, iface_key, mac_address):
123         """Update mac_address on the interface from the node.
124
125         :param node: Node to update MAC on.
126         :param iface_key: Topology key of the interface.
127         :param mac_address: MAC address.
128         :type node: dict
129         :type iface_key: str
130         :type mac_address: str
131         """
132         node['interfaces'][iface_key]['mac_address'] = str(mac_address)
133
134     @staticmethod
135     def update_interface_vhost_socket(node, iface_key, vhost_socket):
136         """Update vhost socket name on the interface from the node.
137
138         :param node: Node to update socket name on.
139         :param iface_key: Topology key of the interface.
140         :param vhost_socket: Path to named socket on node.
141         :type node: dict
142         :type iface_key: str
143         :type vhost_socket: str
144         """
145         node['interfaces'][iface_key]['vhost_socket'] = str(vhost_socket)
146
147     @staticmethod
148     def get_node_by_hostname(nodes, hostname):
149         """Get node from nodes of the topology by hostname.
150
151         :param nodes: Nodes of the test topology.
152         :param hostname: Host name.
153         :type nodes: dict
154         :type hostname: str
155         :return: Node dictionary or None if not found.
156         """
157         for node in nodes.values():
158             if node['host'] == hostname:
159                 return node
160
161         return None
162
163     @staticmethod
164     def get_links(nodes):
165         """Get list of links(networks) in the topology.
166
167         :param nodes: Nodes of the test topology.
168         :type nodes: dict
169         :return: Links in the topology.
170         :rtype: list
171         """
172         links = []
173
174         for node in nodes.values():
175             for interface in node['interfaces'].values():
176                 link = interface.get('link')
177                 if link is not None:
178                     if link not in links:
179                         links.append(link)
180
181         return links
182
183     @staticmethod
184     def _get_interface_by_key_value(node, key, value):
185         """Return node interface key from topology file
186         according to key and value.
187
188         :param node: The node dictionary.
189         :param key: Key by which to select the interface.
190         :param value: Value that should be found using the key.
191         :type node: dict
192         :type key: string
193         :type value: string
194         :return: Interface key from topology file
195         :rtype: string
196         """
197         interfaces = node['interfaces']
198         retval = None
199         for if_key, if_val in interfaces.iteritems():
200             k_val = if_val.get(key)
201             if k_val is not None:
202                 if k_val == value:
203                     retval = if_key
204                     break
205         return retval
206
207     @staticmethod
208     def get_interface_by_name(node, iface_name):
209         """Return interface key based on name from DUT/TG.
210
211         This method returns interface key based on interface name
212         retrieved from the DUT, or TG.
213
214         :param node: The node topology dictionary.
215         :param iface_name: Interface name (string form).
216         :type node: dict
217         :type iface_name: string
218         :return: Interface key.
219         :rtype: str
220         """
221         return Topology._get_interface_by_key_value(node, "name", iface_name)
222
223     @staticmethod
224     def get_interface_by_link_name(node, link_name):
225         """Return interface key of link on node.
226
227         This method returns the interface name associated with a given link
228         for a given node.
229
230         :param node: The node topology dictionary.
231         :param link_name: Name of the link that a interface is connected to.
232         :type node: dict
233         :type link_name: string
234         :return: Interface key of the interface connected to the given link.
235         :rtype: str
236         """
237         return Topology._get_interface_by_key_value(node, "link", link_name)
238
239     def get_interfaces_by_link_names(self, node, link_names):
240         """Return dictionary of dictionaries {"interfaceN", interface name}.
241
242         This method returns the interface names associated with given links
243         for a given node.
244
245         :param node: The node topology directory.
246         :param link_names: List of names of the link that a interface is
247         connected to.
248         :type node: dict
249         :type link_names: list
250         :return: Dictionary of interface names that are connected to the given
251         links.
252         :rtype: dict
253         """
254         retval = {}
255         interface_key_tpl = "interface{}"
256         interface_number = 1
257         for link_name in link_names:
258             interface = self.get_interface_by_link_name(node, link_name)
259             interface_name = self.get_interface_name(node, interface)
260             interface_key = interface_key_tpl.format(str(interface_number))
261             retval[interface_key] = interface_name
262             interface_number += 1
263         return retval
264
265     @staticmethod
266     def get_interface_by_sw_index(node, sw_index):
267         """Return interface name of link on node.
268
269         This method returns the interface name associated with a software
270         interface index assigned to the interface by vpp for a given node.
271
272         :param node: The node topology dictionary.
273         :param sw_index: Sw_index of the link that a interface is connected to.
274         :type node: dict
275         :type sw_index: int
276         :return: Interface name of the interface connected to the given link.
277         :rtype: str
278         """
279         return Topology._get_interface_by_key_value(node, "vpp_sw_index", sw_index)
280
281     @staticmethod
282     def get_interface_sw_index(node, iface_key):
283         """Get VPP sw_if_index for the interface.
284
285         :param node: Node to get interface sw_if_index on.
286         :param iface_key: Interface key from topology file, or sw_index.
287         :type node: dict
288         :type iface_key: str/int
289         :return: Return sw_if_index or None if not found.
290         """
291         try:
292             if isinstance(iface_key, basestring):
293                 return node['interfaces'][iface_key].get('vpp_sw_index')
294             #FIXME: use only iface_key, do not use integer
295             else:
296                 return int(iface_key)
297         except (KeyError, ValueError):
298             return None
299
300     @staticmethod
301     def get_interface_mtu(node, iface_key):
302         """Get interface MTU.
303
304         Returns physical layer MTU (max. size of Ethernet frame).
305         :param node: Node to get interface MTU on.
306         :param iface_key: Interface key from topology file.
307         :type node: dict
308         :type iface_key: str
309         :return: MTU or None if not found.
310         :rtype: int
311         """
312         try:
313             return node['interfaces'][iface_key].get('mtu')
314         except KeyError:
315             return None
316
317     @staticmethod
318     def get_interface_name(node, iface_key):
319         """Get interface name (retrieved from DUT/TG).
320
321         Returns name in string format, retrieved from the node.
322         :param node: Node to get interface name on.
323         :param iface_key: Interface key from topology file.
324         :type node: dict
325         :type iface_key: str
326         :return: Interface name or None if not found.
327         :rtype: int
328         """
329         try:
330             return node['interfaces'][iface_key].get('name')
331         except KeyError:
332             return None
333
334     @staticmethod
335     def get_interface_mac(node, iface_key):
336         """Get MAC address for the interface.
337
338         :param node: Node to get interface mac on.
339         :param iface_key: Interface key from topology file.
340         :type node: dict
341         :type iface_key: str
342         :return: Return MAC or None if not found.
343         """
344         try:
345             return node['interfaces'][iface_key].get('mac_address')
346         except KeyError:
347             return None
348
349     @staticmethod
350     def get_adjacent_node_and_interface(nodes_info, node, iface_key):
351         """Get node and interface adjacent to specified interface
352         on local network.
353
354         :param nodes_info: Dictionary containing information on all nodes
355         in topology.
356         :param node: Node that contains specified interface.
357         :param iface_key: Interface key from topology file.
358         :type nodes_info: dict
359         :type node: dict
360         :type iface_key: str
361         :return: Return (node, interface_key) tuple or None if not found.
362         :rtype: (dict, str)
363         """
364         link_name = None
365         # get link name where the interface belongs to
366         for if_key, if_val in node['interfaces'].iteritems():
367             if if_key == 'mgmt':
368                 continue
369             if if_key == iface_key:
370                 link_name = if_val['link']
371                 break
372
373         if link_name is None:
374             return None
375
376         # find link
377         for node_data in nodes_info.values():
378             # skip self
379             if node_data['host'] == node['host']:
380                 continue
381             for if_key, if_val \
382                     in node_data['interfaces'].iteritems():
383                 if 'link' not in if_val:
384                     continue
385                 if if_val['link'] == link_name:
386                     return node_data, if_key
387
388     @staticmethod
389     def get_interface_pci_addr(node, iface_key):
390         """Get interface PCI address.
391
392         :param node: Node to get interface PCI address on.
393         :param iface_key: Interface key from topology file.
394         :type node: dict
395         :type iface_key: str
396         :return: Return PCI address or None if not found.
397         """
398         try:
399             return node['interfaces'][iface_key].get('pci_address')
400         except KeyError:
401             return None
402
403     @staticmethod
404     def get_interface_driver(node, iface_key):
405         """Get interface driver.
406
407         :param node: Node to get interface driver on.
408         :param iface_key: Interface key from topology file.
409         :type node: dict
410         :type iface_key: str
411         :return: Return interface driver or None if not found.
412         """
413         try:
414             return node['interfaces'][iface_key].get('driver')
415         except KeyError:
416             return None
417
418     @staticmethod
419     def get_node_link_mac(node, link_name):
420         """Return interface mac address by link name.
421
422         :param node: Node to get interface sw_index on.
423         :param link_name: Link name.
424         :type node: dict
425         :type link_name: str
426         :return: MAC address string.
427         :rtype: str
428         """
429         for port in node['interfaces'].values():
430             if port.get('link') == link_name:
431                 return port.get('mac_address')
432         return None
433
434     @staticmethod
435     def _get_node_active_link_names(node, filter_list=None):
436         """Return list of link names that are other than mgmt links.
437
438         :param node: Node topology dictionary.
439         :param filter_list: Link filter criteria.
440         :type node: dict
441         :type filter_list: list of strings
442         :return: List of strings that represent link names occupied by the node.
443         :rtype: list
444         """
445         interfaces = node['interfaces']
446         link_names = []
447         for interface in interfaces.values():
448             if 'link' in interface:
449                 if (filter_list is not None) and ('model' in interface):
450                     for filt in filter_list:
451                         if filt == interface['model']:
452                             link_names.append(interface['link'])
453                 elif (filter_list is not None) and ('model' not in interface):
454                     logger.trace("Cannot apply filter on interface: {}" \
455                                  .format(str(interface)))
456                 else:
457                     link_names.append(interface['link'])
458         if len(link_names) == 0:
459             link_names = None
460         return link_names
461
462     @keyword('Get active links connecting "${node1}" and "${node2}"')
463     def get_active_connecting_links(self, node1, node2,
464                                     filter_list_node1=None,
465                                     filter_list_node2=None):
466         """Return list of link names that connect together node1 and node2.
467
468         :param node1: Node topology dictionary.
469         :param node2: Node topology dictionary.
470         :param filter_list_node1: Link filter criteria for node1.
471         :param filter_list_node2: Link filter criteria for node2.
472         :type node1: dict
473         :type node2: dict
474         :type filter_list1: list of strings
475         :type filter_list2: list of strings
476         :return: List of strings that represent connecting link names.
477         :rtype: list
478         """
479
480         logger.trace("node1: {}".format(str(node1)))
481         logger.trace("node2: {}".format(str(node2)))
482         node1_links = self._get_node_active_link_names(
483             node1,
484             filter_list=filter_list_node1)
485         node2_links = self._get_node_active_link_names(
486             node2,
487             filter_list=filter_list_node2)
488
489         connecting_links = None
490         if node1_links is None:
491             logger.error("Unable to find active links for node1")
492         elif node2_links is None:
493             logger.error("Unable to find active links for node2")
494         else:
495             connecting_links = list(set(node1_links).intersection(node2_links))
496
497         return connecting_links
498
499     @keyword('Get first active connecting link between node "${node1}" and '
500              '"${node2}"')
501     def get_first_active_connecting_link(self, node1, node2):
502         """
503
504         :param node1: Connected node.
505         :param node2: Connected node.
506         :type node1: dict
507         :type node2: dict
508         :return: Name of link connecting the two nodes together.
509         :rtype: str
510         :raises: RuntimeError
511         """
512         connecting_links = self.get_active_connecting_links(node1, node2)
513         if len(connecting_links) == 0:
514             raise RuntimeError("No links connecting the nodes were found")
515         else:
516             return connecting_links[0]
517
518     @keyword('Get egress interfaces name on "${node1}" for link with "${node2}"')
519     def get_egress_interfaces_name_for_nodes(self, node1, node2):
520         """Get egress interfaces on node1 for link with node2.
521
522         :param node1: First node, node to get egress interface on.
523         :param node2: Second node.
524         :type node1: dict
525         :type node2: dict
526         :return: Egress interfaces.
527         :rtype: list
528         """
529         interfaces = []
530         links = self.get_active_connecting_links(node1, node2)
531         if len(links) == 0:
532             raise RuntimeError('No link between nodes')
533         for interface in node1['interfaces'].values():
534             link = interface.get('link')
535             if link is None:
536                 continue
537             if link in links:
538                 continue
539             name = interface.get('name')
540             if name is None:
541                 continue
542             interfaces.append(name)
543         return interfaces
544
545     @keyword('Get first egress interface name on "${node1}" for link with '
546              '"${node2}"')
547     def get_first_egress_interface_for_nodes(self, node1, node2):
548         """Get first egress interface on node1 for link with node2.
549
550         :param node1: First node, node to get egress interface name on.
551         :param node2: Second node.
552         :type node1: dict
553         :type node2: dict
554         :return: Egress interface name.
555         :rtype: str
556         """
557         interfaces = self.get_egress_interfaces_name_for_nodes(node1, node2)
558         if not interfaces:
559             raise RuntimeError('No egress interface for nodes')
560         return interfaces[0]
561
562     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
563              ' dut1 "${dut1}" dut2 "${dut2}"')
564     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
565         """Return link combinations used in tests in circular topology.
566
567         For the time being it returns links from the Node path:
568         TG->DUT1->DUT2->TG
569         The naming convention until changed to something more general is
570         implemented is this:
571         DUT1_DUT2_LINK: link name between DUT! and DUT2
572         DUT1_TG_LINK: link name between DUT1 and TG
573         DUT2_TG_LINK: link name between DUT2 and TG
574         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
575         to and from
576         DUT1_BD_LINKS: list of link names that will be connected by the bridge
577         domain on DUT1
578         DUT2_BD_LINKS: list of link names that will be connected by the bridge
579         domain on DUT2
580
581         :param tgen: Traffic generator node data.
582         :param dut1: DUT1 node data.
583         :param dut2: DUT2 node data.
584         :type tgen: dict
585         :type dut1: dict
586         :type dut2: dict
587         :return: Dictionary of possible link combinations.
588         :rtype: dict
589         """
590         # TODO: replace with generic function.
591         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
592         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
593         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
594         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
595         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
596         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
597         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
598                           'DUT1_TG_LINK': dut1_tg_link,
599                           'DUT2_TG_LINK': dut2_tg_link,
600                           'TG_TRAFFIC_LINKS': tg_traffic_links,
601                           'DUT1_BD_LINKS': dut1_bd_links,
602                           'DUT2_BD_LINKS': dut2_bd_links}
603         return topology_links
604
605     @staticmethod
606     def is_tg_node(node):
607         """Find out whether the node is TG.
608
609         :param node: Node to examine.
610         :type node: dict
611         :return: True if node is type of TG, otherwise False.
612         :rtype: bool
613         """
614         return node['type'] == NodeType.TG
615
616     @staticmethod
617     def get_node_hostname(node):
618         """Return host (hostname/ip address) of the node.
619
620         :param node: Node created from topology.
621         :type node: dict
622         :return: Hostname or IP address.
623         :rtype: str
624         """
625         return node['host']