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