02f326a1df852517aef6241a8c0f03899e0aeafc
[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 collections import Counter
17
18 from yaml import load
19
20 from robot.api import logger
21 from robot.libraries.BuiltIn import BuiltIn
22 from robot.api.deco import keyword
23
24 __all__ = ["DICT__nodes", 'Topology']
25
26
27 def load_topo_from_yaml():
28     """Load topology from file defined in "${TOPOLOGY_PATH}" variable.
29
30     :return: Nodes from loaded topology.
31     """
32     topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}")
33
34     with open(topo_path) as work_file:
35         return load(work_file.read())['nodes']
36
37
38 # pylint: disable=invalid-name
39 class NodeType(object):
40     """Defines node types used in topology dictionaries."""
41     # Device Under Test (this node has VPP running on it)
42     DUT = 'DUT'
43     # Traffic Generator (this node has traffic generator on it)
44     TG = 'TG'
45     # Virtual Machine (this node running on DUT node)
46     VM = 'VM'
47
48
49 class NodeSubTypeTG(object):
50     """Defines node sub-type TG - traffic generator."""
51     # T-Rex traffic generator
52     TREX = 'TREX'
53     # Moongen
54     MOONGEN = 'MOONGEN'
55     # IxNetwork
56     IXNET = 'IXNET'
57
58 DICT__nodes = load_topo_from_yaml()
59
60
61 class Topology(object):
62     """Topology data manipulation and extraction methods.
63
64     Defines methods used for manipulation and extraction of data from
65     the active topology.
66
67     "Active topology" contains initially data from the topology file and can be
68     extended with additional data from the DUTs like internal interface indexes
69     or names. Additional data which can be filled to the active topology are
70         - additional internal representation (index, name, ...)
71         - operational data (dynamic ports)
72
73     To access the port data it is recommended to use a port key because the key
74     does not rely on the data retrieved from nodes, this allows to call most of
75     the methods without having filled active topology with internal nodes data.
76     """
77
78     @staticmethod
79     def add_new_port(node, ptype):
80         """Add new port to the node to active topology.
81
82         :param node: Node to add new port on.
83         :param ptype: Port type, used as key prefix.
84         :type node: dict
85         :type ptype: str
86         :return: Port key or None
87         :rtype: string or None
88         """
89         max_ports = 1000000
90         iface = None
91         for i in range(1, max_ports):
92             if node['interfaces'].get(str(ptype) + str(i)) is None:
93                 iface = str(ptype) + str(i)
94                 node['interfaces'][iface] = dict()
95                 break
96         return iface
97
98     @staticmethod
99     def remove_all_ports(node, ptype):
100         """Remove all ports with ptype as prefix.
101
102         :param node: Node to remove ports on.
103         :param: ptype: Port type, used as key prefix.
104         :type node: dict
105         :type ptype: str
106         :return: Nothing
107         """
108         for if_key in list(node['interfaces']):
109             if if_key.startswith(str(ptype)):
110                 node['interfaces'].pop(if_key)
111
112     @staticmethod
113     def update_interface_sw_if_index(node, iface_key, sw_if_index):
114         """Update sw_if_index on the interface from the node.
115
116         :param node: Node to update sw_if_index on.
117         :param iface_key: Topology key of the interface.
118         :param sw_if_index: Internal index to store.
119         :type node: dict
120         :type iface_key: str
121         :type sw_if_index: int
122         """
123         node['interfaces'][iface_key]['vpp_sw_index'] = int(sw_if_index)
124
125     @staticmethod
126     def update_interface_mac_address(node, iface_key, mac_address):
127         """Update mac_address on the interface from the node.
128
129         :param node: Node to update MAC on.
130         :param iface_key: Topology key of the interface.
131         :param mac_address: MAC address.
132         :type node: dict
133         :type iface_key: str
134         :type mac_address: str
135         """
136         node['interfaces'][iface_key]['mac_address'] = str(mac_address)
137
138     @staticmethod
139     def update_interface_vhost_socket(node, iface_key, vhost_socket):
140         """Update vhost socket name on the interface from the node.
141
142         :param node: Node to update socket name on.
143         :param iface_key: Topology key of the interface.
144         :param vhost_socket: Path to named socket on node.
145         :type node: dict
146         :type iface_key: str
147         :type vhost_socket: str
148         """
149         node['interfaces'][iface_key]['vhost_socket'] = str(vhost_socket)
150
151     @staticmethod
152     def get_node_by_hostname(nodes, hostname):
153         """Get node from nodes of the topology by hostname.
154
155         :param nodes: Nodes of the test topology.
156         :param hostname: Host name.
157         :type nodes: dict
158         :type hostname: str
159         :return: Node dictionary or None if not found.
160         """
161         for node in nodes.values():
162             if node['host'] == hostname:
163                 return node
164
165         return None
166
167     @staticmethod
168     def get_links(nodes):
169         """Get list of links(networks) in the topology.
170
171         :param nodes: Nodes of the test topology.
172         :type nodes: dict
173         :return: Links in the topology.
174         :rtype: list
175         """
176         links = []
177
178         for node in nodes.values():
179             for interface in node['interfaces'].values():
180                 link = interface.get('link')
181                 if link is not None:
182                     if link not in links:
183                         links.append(link)
184
185         return links
186
187     @staticmethod
188     def _get_interface_by_key_value(node, key, value):
189         """Return node interface key from topology file
190         according to key and value.
191
192         :param node: The node dictionary.
193         :param key: Key by which to select the interface.
194         :param value: Value that should be found using the key.
195         :type node: dict
196         :type key: string
197         :type value: string
198         :return: Interface key from topology file
199         :rtype: string
200         """
201         interfaces = node['interfaces']
202         retval = None
203         for if_key, if_val in interfaces.iteritems():
204             k_val = if_val.get(key)
205             if k_val is not None:
206                 if k_val == value:
207                     retval = if_key
208                     break
209         return retval
210
211     @staticmethod
212     def get_interface_by_name(node, iface_name):
213         """Return interface key based on name from DUT/TG.
214
215         This method returns interface key based on interface name
216         retrieved from the DUT, or TG.
217
218         :param node: The node topology dictionary.
219         :param iface_name: Interface name (string form).
220         :type node: dict
221         :type iface_name: string
222         :return: Interface key.
223         :rtype: str
224         """
225         return Topology._get_interface_by_key_value(node, "name", iface_name)
226
227     @staticmethod
228     def get_interface_by_link_name(node, link_name):
229         """Return interface key of link on node.
230
231         This method returns the interface name associated with a given link
232         for a given node.
233
234         :param node: The node topology dictionary.
235         :param link_name: Name of the link that a interface is connected to.
236         :type node: dict
237         :type link_name: string
238         :return: Interface key of the interface connected to the given link.
239         :rtype: str
240         """
241         return Topology._get_interface_by_key_value(node, "link", link_name)
242
243     def get_interfaces_by_link_names(self, node, link_names):
244         """Return dictionary of dictionaries {"interfaceN", interface name}.
245
246         This method returns the interface names associated with given links
247         for a given node.
248
249         :param node: The node topology directory.
250         :param link_names: List of names of the link that a interface is
251         connected to.
252         :type node: dict
253         :type link_names: list
254         :return: Dictionary of interface names that are connected to the given
255         links.
256         :rtype: dict
257         """
258         retval = {}
259         interface_key_tpl = "interface{}"
260         interface_number = 1
261         for link_name in link_names:
262             interface = self.get_interface_by_link_name(node, link_name)
263             interface_name = self.get_interface_name(node, interface)
264             interface_key = interface_key_tpl.format(str(interface_number))
265             retval[interface_key] = interface_name
266             interface_number += 1
267         return retval
268
269     @staticmethod
270     def get_interface_by_sw_index(node, sw_index):
271         """Return interface name of link on node.
272
273         This method returns the interface name associated with a software
274         interface index assigned to the interface by vpp for a given node.
275
276         :param node: The node topology dictionary.
277         :param sw_index: Sw_index of the link that a interface is connected to.
278         :type node: dict
279         :type sw_index: int
280         :return: Interface name of the interface connected to the given link.
281         :rtype: str
282         """
283         return Topology._get_interface_by_key_value(node, "vpp_sw_index",
284                                                     sw_index)
285
286     @staticmethod
287     def get_interface_sw_index(node, iface_key):
288         """Get VPP sw_if_index for the interface using interface key.
289
290         :param node: Node to get interface sw_if_index on.
291         :param iface_key: Interface key from topology file, or sw_index.
292         :type node: dict
293         :type iface_key: str/int
294         :return: Return sw_if_index or None if not found.
295         """
296         try:
297             if isinstance(iface_key, basestring):
298                 return node['interfaces'][iface_key].get('vpp_sw_index')
299             #FIXME: use only iface_key, do not use integer
300             else:
301                 return int(iface_key)
302         except (KeyError, ValueError):
303             return None
304
305     @staticmethod
306     def get_interface_sw_index_by_name(node, iface_name):
307         """Get VPP sw_if_index for the interface using interface name.
308
309         :param node: Node to get interface sw_if_index on.
310         :param iface_name: Interface name.
311         :type node: dict
312         :type iface_name: str
313         :return: Return sw_if_index or None if not found.
314         :raises TypeError: If provided interface name is not a string.
315         """
316         try:
317             if isinstance(iface_name, basestring):
318                 iface_key = Topology.get_interface_by_name(node, iface_name)
319                 return node['interfaces'][iface_key].get('vpp_sw_index')
320             else:
321                 raise TypeError("Interface name must be a string.")
322         except (KeyError, ValueError):
323             return None
324
325     @staticmethod
326     def get_interface_mtu(node, iface_key):
327         """Get interface MTU.
328
329         Returns physical layer MTU (max. size of Ethernet frame).
330         :param node: Node to get interface MTU on.
331         :param iface_key: Interface key from topology file.
332         :type node: dict
333         :type iface_key: str
334         :return: MTU or None if not found.
335         :rtype: int
336         """
337         try:
338             return node['interfaces'][iface_key].get('mtu')
339         except KeyError:
340             return None
341
342     @staticmethod
343     def get_interface_name(node, iface_key):
344         """Get interface name (retrieved from DUT/TG).
345
346         Returns name in string format, retrieved from the node.
347         :param node: Node to get interface name on.
348         :param iface_key: Interface key from topology file.
349         :type node: dict
350         :type iface_key: str
351         :return: Interface name or None if not found.
352         :rtype: int
353         """
354         try:
355             return node['interfaces'][iface_key].get('name')
356         except KeyError:
357             return None
358
359     @staticmethod
360     def get_interface_numa_node(node, iface_key):
361         """Get interface numa node.
362
363         Returns physical relation to numa node, numa_id.
364
365         :param node: Node to get numa id on.
366         :param iface_key: Interface key from topology file.
367         :type node: dict
368         :type iface_key: str
369         :return: numa node id, None if not available.
370         :rtype: int
371         """
372         try:
373             return node['interfaces'][iface_key].get('numa_node')
374         except KeyError:
375             return None
376
377     @staticmethod
378     def get_interfaces_numa_node(node, *iface_keys):
379         """Get numa node on which are located most of the interfaces.
380
381         Return numa node with highest count of interfaces provided as arguments.
382         Return 0 if the interface does not have numa_node information available.
383         If all interfaces have unknown location (-1), then return 0.
384         If most of interfaces have unknown location (-1), but there are
385         some interfaces with known location, then return the second most
386         location of the provided interfaces.
387
388         :param node: Node from DICT__nodes.
389         :param iface_keys: Interface keys for lookup.
390         :type node: dict
391         :type iface_keys: strings
392         """
393         numa_list = []
394         for if_key in iface_keys:
395             try:
396                 numa_list.append(node['interfaces'][if_key].get('numa_node'))
397             except KeyError:
398                 pass
399
400         numa_cnt_mc = Counter(numa_list).most_common()
401
402         if len(numa_cnt_mc) > 0 and numa_cnt_mc[0][0] != -1:
403             return numa_cnt_mc[0][0]
404         elif len(numa_cnt_mc) > 1 and numa_cnt_mc[0][0] == -1:
405             return numa_cnt_mc[1][0]
406         else:
407             return 0
408
409     @staticmethod
410     def get_interface_mac(node, iface_key):
411         """Get MAC address for the interface.
412
413         :param node: Node to get interface mac on.
414         :param iface_key: Interface key from topology file.
415         :type node: dict
416         :type iface_key: str
417         :return: Return MAC or None if not found.
418         """
419         try:
420             return node['interfaces'][iface_key].get('mac_address')
421         except KeyError:
422             return None
423
424     @staticmethod
425     def get_adjacent_node_and_interface(nodes_info, node, iface_key):
426         """Get node and interface adjacent to specified interface
427         on local network.
428
429         :param nodes_info: Dictionary containing information on all nodes
430         in topology.
431         :param node: Node that contains specified interface.
432         :param iface_key: Interface key from topology file.
433         :type nodes_info: dict
434         :type node: dict
435         :type iface_key: str
436         :return: Return (node, interface_key) tuple or None if not found.
437         :rtype: (dict, str)
438         """
439         link_name = None
440         # get link name where the interface belongs to
441         for if_key, if_val in node['interfaces'].iteritems():
442             if if_key == 'mgmt':
443                 continue
444             if if_key == iface_key:
445                 link_name = if_val['link']
446                 break
447
448         if link_name is None:
449             return None
450
451         # find link
452         for node_data in nodes_info.values():
453             # skip self
454             if node_data['host'] == node['host']:
455                 continue
456             for if_key, if_val \
457                     in node_data['interfaces'].iteritems():
458                 if 'link' not in if_val:
459                     continue
460                 if if_val['link'] == link_name:
461                     return node_data, if_key
462
463     @staticmethod
464     def get_interface_pci_addr(node, iface_key):
465         """Get interface PCI address.
466
467         :param node: Node to get interface PCI address on.
468         :param iface_key: Interface key from topology file.
469         :type node: dict
470         :type iface_key: str
471         :return: Return PCI address or None if not found.
472         """
473         try:
474             return node['interfaces'][iface_key].get('pci_address')
475         except KeyError:
476             return None
477
478     @staticmethod
479     def get_interface_driver(node, iface_key):
480         """Get interface driver.
481
482         :param node: Node to get interface driver on.
483         :param iface_key: Interface key from topology file.
484         :type node: dict
485         :type iface_key: str
486         :return: Return interface driver or None if not found.
487         """
488         try:
489             return node['interfaces'][iface_key].get('driver')
490         except KeyError:
491             return None
492
493     @staticmethod
494     def get_node_interfaces(node):
495         """Get all node interfaces.
496
497         :param node: Node to get list of interfaces from.
498         :type node: dict
499         :return: Return list of keys of all interfaces.
500         :rtype: list
501         """
502         return node['interfaces'].keys()
503
504     @staticmethod
505     def get_node_link_mac(node, link_name):
506         """Return interface mac address by link name.
507
508         :param node: Node to get interface sw_index on.
509         :param link_name: Link name.
510         :type node: dict
511         :type link_name: str
512         :return: MAC address string.
513         :rtype: str
514         """
515         for port in node['interfaces'].values():
516             if port.get('link') == link_name:
517                 return port.get('mac_address')
518         return None
519
520     @staticmethod
521     def _get_node_active_link_names(node, filter_list=None):
522         """Return list of link names that are other than mgmt links.
523
524         :param node: Node topology dictionary.
525         :param filter_list: Link filter criteria.
526         :type node: dict
527         :type filter_list: list of strings
528         :return: List of strings that represent link names occupied by the node.
529         :rtype: list
530         """
531         interfaces = node['interfaces']
532         link_names = []
533         for interface in interfaces.values():
534             if 'link' in interface:
535                 if (filter_list is not None) and ('model' in interface):
536                     for filt in filter_list:
537                         if filt == interface['model']:
538                             link_names.append(interface['link'])
539                 elif (filter_list is not None) and ('model' not in interface):
540                     logger.trace("Cannot apply filter on interface: {}"
541                                  .format(str(interface)))
542                 else:
543                     link_names.append(interface['link'])
544         if len(link_names) == 0:
545             link_names = None
546         return link_names
547
548     @keyword('Get active links connecting "${node1}" and "${node2}"')
549     def get_active_connecting_links(self, node1, node2,
550                                     filter_list_node1=None,
551                                     filter_list_node2=None):
552         """Return list of link names that connect together node1 and node2.
553
554         :param node1: Node topology dictionary.
555         :param node2: Node topology dictionary.
556         :param filter_list_node1: Link filter criteria for node1.
557         :param filter_list_node2: Link filter criteria for node2.
558         :type node1: dict
559         :type node2: dict
560         :type filter_list_node1: list of strings
561         :type filter_list_node2: list of strings
562         :return: List of strings that represent connecting link names.
563         :rtype: list
564         """
565
566         logger.trace("node1: {}".format(str(node1)))
567         logger.trace("node2: {}".format(str(node2)))
568         node1_links = self._get_node_active_link_names(
569             node1,
570             filter_list=filter_list_node1)
571         node2_links = self._get_node_active_link_names(
572             node2,
573             filter_list=filter_list_node2)
574
575         connecting_links = None
576         if node1_links is None:
577             logger.error("Unable to find active links for node1")
578         elif node2_links is None:
579             logger.error("Unable to find active links for node2")
580         else:
581             connecting_links = list(set(node1_links).intersection(node2_links))
582
583         return connecting_links
584
585     @keyword('Get first active connecting link between node "${node1}" and '
586              '"${node2}"')
587     def get_first_active_connecting_link(self, node1, node2):
588         """
589
590         :param node1: Connected node.
591         :param node2: Connected node.
592         :type node1: dict
593         :type node2: dict
594         :return: Name of link connecting the two nodes together.
595         :rtype: str
596         :raises: RuntimeError
597         """
598         connecting_links = self.get_active_connecting_links(node1, node2)
599         if len(connecting_links) == 0:
600             raise RuntimeError("No links connecting the nodes were found")
601         else:
602             return connecting_links[0]
603
604     @keyword('Get egress interfaces name on "${node1}" for link with '
605              '"${node2}"')
606     def get_egress_interfaces_name_for_nodes(self, node1, node2):
607         """Get egress interfaces on node1 for link with node2.
608
609         :param node1: First node, node to get egress interface on.
610         :param node2: Second node.
611         :type node1: dict
612         :type node2: dict
613         :return: Egress interfaces.
614         :rtype: list
615         """
616         interfaces = []
617         links = self.get_active_connecting_links(node1, node2)
618         if len(links) == 0:
619             raise RuntimeError('No link between nodes')
620         for interface in node1['interfaces'].values():
621             link = interface.get('link')
622             if link is None:
623                 continue
624             if link in links:
625                 continue
626             name = interface.get('name')
627             if name is None:
628                 continue
629             interfaces.append(name)
630         return interfaces
631
632     @keyword('Get first egress interface name on "${node1}" for link with '
633              '"${node2}"')
634     def get_first_egress_interface_for_nodes(self, node1, node2):
635         """Get first egress interface on node1 for link with node2.
636
637         :param node1: First node, node to get egress interface name on.
638         :param node2: Second node.
639         :type node1: dict
640         :type node2: dict
641         :return: Egress interface name.
642         :rtype: str
643         """
644         interfaces = self.get_egress_interfaces_name_for_nodes(node1, node2)
645         if not interfaces:
646             raise RuntimeError('No egress interface for nodes')
647         return interfaces[0]
648
649     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
650              ' dut1 "${dut1}" dut2 "${dut2}"')
651     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
652         """Return link combinations used in tests in circular topology.
653
654         For the time being it returns links from the Node path:
655         TG->DUT1->DUT2->TG
656         The naming convention until changed to something more general is
657         implemented is this:
658         DUT1_DUT2_LINK: link name between DUT! and DUT2
659         DUT1_TG_LINK: link name between DUT1 and TG
660         DUT2_TG_LINK: link name between DUT2 and TG
661         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
662         to and from
663         DUT1_BD_LINKS: list of link names that will be connected by the bridge
664         domain on DUT1
665         DUT2_BD_LINKS: list of link names that will be connected by the bridge
666         domain on DUT2
667
668         :param tgen: Traffic generator node data.
669         :param dut1: DUT1 node data.
670         :param dut2: DUT2 node data.
671         :type tgen: dict
672         :type dut1: dict
673         :type dut2: dict
674         :return: Dictionary of possible link combinations.
675         :rtype: dict
676         """
677         # TODO: replace with generic function.
678         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
679         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
680         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
681         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
682         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
683         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
684         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
685                           'DUT1_TG_LINK': dut1_tg_link,
686                           'DUT2_TG_LINK': dut2_tg_link,
687                           'TG_TRAFFIC_LINKS': tg_traffic_links,
688                           'DUT1_BD_LINKS': dut1_bd_links,
689                           'DUT2_BD_LINKS': dut2_bd_links}
690         return topology_links
691
692     @staticmethod
693     def is_tg_node(node):
694         """Find out whether the node is TG.
695
696         :param node: Node to examine.
697         :type node: dict
698         :return: True if node is type of TG, otherwise False.
699         :rtype: bool
700         """
701         return node['type'] == NodeType.TG
702
703     @staticmethod
704     def get_node_hostname(node):
705         """Return host (hostname/ip address) of the node.
706
707         :param node: Node created from topology.
708         :type node: dict
709         :return: Hostname or IP address.
710         :rtype: str
711         """
712         return node['host']
713
714     @staticmethod
715     def set_interface_numa_node(node, iface_key, numa_node_id):
716         """Set interface numa_node location.
717
718         :param node: Node to set numa_node on.
719         :param iface_key: Interface key from topology file.
720         :type node: dict
721         :type iface_key: str
722         :return: Return iface_key or None if not found.
723         """
724         try:
725             node['interfaces'][iface_key]['numa_node'] = numa_node_id
726             return iface_key
727         except KeyError:
728             return None