CSIT-338 PCI numa_node discovery
[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", sw_index)
282
283     @staticmethod
284     def get_interface_sw_index(node, iface_key):
285         """Get VPP sw_if_index for the interface.
286
287         :param node: Node to get interface sw_if_index on.
288         :param iface_key: Interface key from topology file, or sw_index.
289         :type node: dict
290         :type iface_key: str/int
291         :return: Return sw_if_index or None if not found.
292         """
293         try:
294             if isinstance(iface_key, basestring):
295                 return node['interfaces'][iface_key].get('vpp_sw_index')
296             #FIXME: use only iface_key, do not use integer
297             else:
298                 return int(iface_key)
299         except (KeyError, ValueError):
300             return None
301
302     @staticmethod
303     def get_interface_mtu(node, iface_key):
304         """Get interface MTU.
305
306         Returns physical layer MTU (max. size of Ethernet frame).
307         :param node: Node to get interface MTU on.
308         :param iface_key: Interface key from topology file.
309         :type node: dict
310         :type iface_key: str
311         :return: MTU or None if not found.
312         :rtype: int
313         """
314         try:
315             return node['interfaces'][iface_key].get('mtu')
316         except KeyError:
317             return None
318
319     @staticmethod
320     def get_interface_name(node, iface_key):
321         """Get interface name (retrieved from DUT/TG).
322
323         Returns name in string format, retrieved from the node.
324         :param node: Node to get interface name on.
325         :param iface_key: Interface key from topology file.
326         :type node: dict
327         :type iface_key: str
328         :return: Interface name or None if not found.
329         :rtype: int
330         """
331         try:
332             return node['interfaces'][iface_key].get('name')
333         except KeyError:
334             return None
335
336     @staticmethod
337     def get_interface_numa_node(node, iface_key):
338         """Get interface numa node.
339
340         Returns physical relation to numa node, numa_id.
341
342         :param node: Node to get numa id on.
343         :param iface_key: Interface key from topology file.
344         :type node: dict
345         :type iface_key: str
346         :return: numa node id, None if not available.
347         :rtype: int
348         """
349         try:
350             return node['interfaces'][iface_key].get('numa_node')
351         except KeyError:
352             return None
353
354     @staticmethod
355     def get_interfaces_numa_node(node, *iface_keys):
356         """Get numa node on which are located most of the interfaces.
357
358         Return numa node with highest count of interfaces provided as arguments.
359         Return 0 if the interface does not have numa_node information available.
360         If all interfaces have unknown location (-1), then return 0.
361         If most of interfaces have unknown location (-1), but there are
362         some interfaces with known location, then return the second most
363         location of the provided interfaces.
364
365         :param node: Node from DICT__nodes.
366         :param iface_keys: Interface keys for lookup.
367         :type node: dict
368         :type iface_keys: strings
369         """
370         numa_list = []
371         for if_key in iface_keys:
372             try:
373                 numa_list.append(node['interfaces'][if_key].get('numa_node'))
374             except KeyError:
375                 pass
376
377         numa_cnt_mc = Counter(numa_list).most_common()
378
379         if len(numa_cnt_mc) > 0 and numa_cnt_mc[0][0] != -1:
380             return numa_cnt_mc[0][0]
381         elif len(numa_cnt_mc) > 1 and numa_cnt_mc[0][0] == -1:
382             return numa_cnt_mc[1][0]
383         else:
384             return 0
385
386     @staticmethod
387     def get_interface_mac(node, iface_key):
388         """Get MAC address for the interface.
389
390         :param node: Node to get interface mac on.
391         :param iface_key: Interface key from topology file.
392         :type node: dict
393         :type iface_key: str
394         :return: Return MAC or None if not found.
395         """
396         try:
397             return node['interfaces'][iface_key].get('mac_address')
398         except KeyError:
399             return None
400
401     @staticmethod
402     def get_adjacent_node_and_interface(nodes_info, node, iface_key):
403         """Get node and interface adjacent to specified interface
404         on local network.
405
406         :param nodes_info: Dictionary containing information on all nodes
407         in topology.
408         :param node: Node that contains specified interface.
409         :param iface_key: Interface key from topology file.
410         :type nodes_info: dict
411         :type node: dict
412         :type iface_key: str
413         :return: Return (node, interface_key) tuple or None if not found.
414         :rtype: (dict, str)
415         """
416         link_name = None
417         # get link name where the interface belongs to
418         for if_key, if_val in node['interfaces'].iteritems():
419             if if_key == 'mgmt':
420                 continue
421             if if_key == iface_key:
422                 link_name = if_val['link']
423                 break
424
425         if link_name is None:
426             return None
427
428         # find link
429         for node_data in nodes_info.values():
430             # skip self
431             if node_data['host'] == node['host']:
432                 continue
433             for if_key, if_val \
434                     in node_data['interfaces'].iteritems():
435                 if 'link' not in if_val:
436                     continue
437                 if if_val['link'] == link_name:
438                     return node_data, if_key
439
440     @staticmethod
441     def get_interface_pci_addr(node, iface_key):
442         """Get interface PCI address.
443
444         :param node: Node to get interface PCI address on.
445         :param iface_key: Interface key from topology file.
446         :type node: dict
447         :type iface_key: str
448         :return: Return PCI address or None if not found.
449         """
450         try:
451             return node['interfaces'][iface_key].get('pci_address')
452         except KeyError:
453             return None
454
455     @staticmethod
456     def get_interface_driver(node, iface_key):
457         """Get interface driver.
458
459         :param node: Node to get interface driver on.
460         :param iface_key: Interface key from topology file.
461         :type node: dict
462         :type iface_key: str
463         :return: Return interface driver or None if not found.
464         """
465         try:
466             return node['interfaces'][iface_key].get('driver')
467         except KeyError:
468             return None
469
470     @staticmethod
471     def get_node_interfaces(node):
472         """Get all node interfaces.
473
474         :param node: Node to get list of interfaces from.
475         :type node: dict
476         :return: Return list of keys of all interfaces.
477         :rtype: list
478         """
479         return node['interfaces'].keys()
480
481     @staticmethod
482     def get_node_link_mac(node, link_name):
483         """Return interface mac address by link name.
484
485         :param node: Node to get interface sw_index on.
486         :param link_name: Link name.
487         :type node: dict
488         :type link_name: str
489         :return: MAC address string.
490         :rtype: str
491         """
492         for port in node['interfaces'].values():
493             if port.get('link') == link_name:
494                 return port.get('mac_address')
495         return None
496
497     @staticmethod
498     def _get_node_active_link_names(node, filter_list=None):
499         """Return list of link names that are other than mgmt links.
500
501         :param node: Node topology dictionary.
502         :param filter_list: Link filter criteria.
503         :type node: dict
504         :type filter_list: list of strings
505         :return: List of strings that represent link names occupied by the node.
506         :rtype: list
507         """
508         interfaces = node['interfaces']
509         link_names = []
510         for interface in interfaces.values():
511             if 'link' in interface:
512                 if (filter_list is not None) and ('model' in interface):
513                     for filt in filter_list:
514                         if filt == interface['model']:
515                             link_names.append(interface['link'])
516                 elif (filter_list is not None) and ('model' not in interface):
517                     logger.trace("Cannot apply filter on interface: {}" \
518                                  .format(str(interface)))
519                 else:
520                     link_names.append(interface['link'])
521         if len(link_names) == 0:
522             link_names = None
523         return link_names
524
525     @keyword('Get active links connecting "${node1}" and "${node2}"')
526     def get_active_connecting_links(self, node1, node2,
527                                     filter_list_node1=None,
528                                     filter_list_node2=None):
529         """Return list of link names that connect together node1 and node2.
530
531         :param node1: Node topology dictionary.
532         :param node2: Node topology dictionary.
533         :param filter_list_node1: Link filter criteria for node1.
534         :param filter_list_node2: Link filter criteria for node2.
535         :type node1: dict
536         :type node2: dict
537         :type filter_list1: list of strings
538         :type filter_list2: list of strings
539         :return: List of strings that represent connecting link names.
540         :rtype: list
541         """
542
543         logger.trace("node1: {}".format(str(node1)))
544         logger.trace("node2: {}".format(str(node2)))
545         node1_links = self._get_node_active_link_names(
546             node1,
547             filter_list=filter_list_node1)
548         node2_links = self._get_node_active_link_names(
549             node2,
550             filter_list=filter_list_node2)
551
552         connecting_links = None
553         if node1_links is None:
554             logger.error("Unable to find active links for node1")
555         elif node2_links is None:
556             logger.error("Unable to find active links for node2")
557         else:
558             connecting_links = list(set(node1_links).intersection(node2_links))
559
560         return connecting_links
561
562     @keyword('Get first active connecting link between node "${node1}" and '
563              '"${node2}"')
564     def get_first_active_connecting_link(self, node1, node2):
565         """
566
567         :param node1: Connected node.
568         :param node2: Connected node.
569         :type node1: dict
570         :type node2: dict
571         :return: Name of link connecting the two nodes together.
572         :rtype: str
573         :raises: RuntimeError
574         """
575         connecting_links = self.get_active_connecting_links(node1, node2)
576         if len(connecting_links) == 0:
577             raise RuntimeError("No links connecting the nodes were found")
578         else:
579             return connecting_links[0]
580
581     @keyword('Get egress interfaces name on "${node1}" for link with "${node2}"')
582     def get_egress_interfaces_name_for_nodes(self, node1, node2):
583         """Get egress interfaces on node1 for link with node2.
584
585         :param node1: First node, node to get egress interface on.
586         :param node2: Second node.
587         :type node1: dict
588         :type node2: dict
589         :return: Egress interfaces.
590         :rtype: list
591         """
592         interfaces = []
593         links = self.get_active_connecting_links(node1, node2)
594         if len(links) == 0:
595             raise RuntimeError('No link between nodes')
596         for interface in node1['interfaces'].values():
597             link = interface.get('link')
598             if link is None:
599                 continue
600             if link in links:
601                 continue
602             name = interface.get('name')
603             if name is None:
604                 continue
605             interfaces.append(name)
606         return interfaces
607
608     @keyword('Get first egress interface name on "${node1}" for link with '
609              '"${node2}"')
610     def get_first_egress_interface_for_nodes(self, node1, node2):
611         """Get first egress interface on node1 for link with node2.
612
613         :param node1: First node, node to get egress interface name on.
614         :param node2: Second node.
615         :type node1: dict
616         :type node2: dict
617         :return: Egress interface name.
618         :rtype: str
619         """
620         interfaces = self.get_egress_interfaces_name_for_nodes(node1, node2)
621         if not interfaces:
622             raise RuntimeError('No egress interface for nodes')
623         return interfaces[0]
624
625     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
626              ' dut1 "${dut1}" dut2 "${dut2}"')
627     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
628         """Return link combinations used in tests in circular topology.
629
630         For the time being it returns links from the Node path:
631         TG->DUT1->DUT2->TG
632         The naming convention until changed to something more general is
633         implemented is this:
634         DUT1_DUT2_LINK: link name between DUT! and DUT2
635         DUT1_TG_LINK: link name between DUT1 and TG
636         DUT2_TG_LINK: link name between DUT2 and TG
637         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
638         to and from
639         DUT1_BD_LINKS: list of link names that will be connected by the bridge
640         domain on DUT1
641         DUT2_BD_LINKS: list of link names that will be connected by the bridge
642         domain on DUT2
643
644         :param tgen: Traffic generator node data.
645         :param dut1: DUT1 node data.
646         :param dut2: DUT2 node data.
647         :type tgen: dict
648         :type dut1: dict
649         :type dut2: dict
650         :return: Dictionary of possible link combinations.
651         :rtype: dict
652         """
653         # TODO: replace with generic function.
654         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
655         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
656         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
657         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
658         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
659         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
660         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
661                           'DUT1_TG_LINK': dut1_tg_link,
662                           'DUT2_TG_LINK': dut2_tg_link,
663                           'TG_TRAFFIC_LINKS': tg_traffic_links,
664                           'DUT1_BD_LINKS': dut1_bd_links,
665                           'DUT2_BD_LINKS': dut2_bd_links}
666         return topology_links
667
668     @staticmethod
669     def is_tg_node(node):
670         """Find out whether the node is TG.
671
672         :param node: Node to examine.
673         :type node: dict
674         :return: True if node is type of TG, otherwise False.
675         :rtype: bool
676         """
677         return node['type'] == NodeType.TG
678
679     @staticmethod
680     def get_node_hostname(node):
681         """Return host (hostname/ip address) of the node.
682
683         :param node: Node created from topology.
684         :type node: dict
685         :return: Hostname or IP address.
686         :rtype: str
687         """
688         return node['host']
689
690     @staticmethod
691     def set_interface_numa_node(node, iface_key, numa_node_id):
692         """Set interface numa_node location.
693
694         :param node: Node to set numa_node on.
695         :param iface_key: Interface key from topology file.
696         :type node: dict
697         :type iface_key: str
698         :return: Return iface_key or None if not found.
699         """
700         try:
701             node['interfaces'][iface_key]['numa_node'] = numa_node_id
702             return iface_key
703         except KeyError:
704             return None