Test VIRL connection.
[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 resources.libraries.python.parsers.JsonParser import JsonParser
17 from resources.libraries.python.VatExecutor import VatExecutor
18 from resources.libraries.python.ssh import SSH
19 from resources.libraries.python.InterfaceSetup import InterfaceSetup
20 from robot.api import logger
21 from robot.libraries.BuiltIn import BuiltIn
22 from robot.api.deco import keyword
23 from yaml import load
24
25 __all__ = ["DICT__nodes", 'Topology']
26
27
28 def load_topo_from_yaml():
29     """Loads topology from file defined in "${TOPOLOGY_PATH}" variable
30
31     :return: nodes from loaded topology
32     """
33     topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}")
34
35     with open(topo_path) as work_file:
36         return load(work_file.read())['nodes']
37
38
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
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 used topology.
62     """
63
64     def __init__(self):
65         pass
66
67     @staticmethod
68     def get_node_by_hostname(nodes, hostname):
69         """Get node from nodes of the topology by hostname.
70
71         :param nodes: Nodes of the test topology.
72         :param hostname: Host name.
73         :type nodes: dict
74         :type hostname: str
75         :return: Node dictionary or None if not found.
76         """
77         for node in nodes.values():
78             if node['host'] == hostname:
79                 return node
80
81         return None
82
83     @staticmethod
84     def get_links(nodes):
85         """Get list of links(networks) in the topology.
86
87         :param nodes: Nodes of the test topology.
88         :type nodes: dict
89         :return: Links in the topology.
90         :rtype: list
91         """
92         links = []
93
94         for node in nodes.values():
95             for interface in node['interfaces'].values():
96                 link = interface.get('link')
97                 if link is not None:
98                     if link not in links:
99                         links.append(link)
100
101         return links
102
103     @staticmethod
104     def _get_interface_by_key_value(node, key, value):
105         """ Return node interface name according to key and value
106
107         :param node: :param node: the node dictionary
108         :param key: key by which to select the interface.
109         :param value: value that should be found using the key.
110         :return:
111         """
112
113         interfaces = node['interfaces']
114         retval = None
115         for interface in interfaces.values():
116             k_val = interface.get(key)
117             if k_val is not None:
118                 if k_val == value:
119                     retval = interface['name']
120                     break
121         return retval
122
123     def get_interface_by_link_name(self, node, link_name):
124         """Return interface name of link on node.
125
126         This method returns the interface name asociated with a given link
127         for a given node.
128         :param link_name: name of the link that a interface is connected to.
129         :param node: the node topology dictionary
130         :return: interface name of the interface connected to the given link
131         """
132
133         return self._get_interface_by_key_value(node, "link", link_name)
134
135     def get_interfaces_by_link_names(self, node, link_names):
136         """Return dictionary of dicitonaries {"interfaceN", interface name}.
137
138         This method returns the interface names asociated with given links
139         for a given node.
140         The resulting dictionary can be then used to with VatConfigGenerator
141         to generate a VAT script with proper interface names.
142         :param link_names: list of names of the link that a interface is
143         connected to.
144         :param node: the node topology directory
145         :return: dictionary of interface names that are connected to the given
146         links
147         """
148
149         retval = {}
150         interface_key_tpl = "interface{}"
151         interface_number = 1
152         for link_name in link_names:
153             interface_name = self.get_interface_by_link_name(node, link_name)
154             interface_key = interface_key_tpl.format(str(interface_number))
155             retval[interface_key] = interface_name
156             interface_number += 1
157         return retval
158
159     def get_interface_by_sw_index(self, node, sw_index):
160         """Return interface name of link on node.
161
162         This method returns the interface name asociated with a software index
163         assigned to the interface by vpp for a given node.
164         :param sw_index: sw_index of the link that a interface is connected to.
165         :param node: the node topology dictionary
166         :return: interface name of the interface connected to the given link
167         """
168
169         return self._get_interface_by_key_value(node, "vpp_sw_index", sw_index)
170
171     @staticmethod
172     def convert_mac_to_number_list(mac_address):
173         """Convert mac address string to list of decimal numbers.
174
175         Converts a : separated mac address to decimal number list as used
176         in json interface dump.
177         :param mac_address: string mac address
178         :return: list representation of mac address
179         """
180
181         list_mac = []
182         for num in mac_address.split(":"):
183             list_mac.append(int(num, 16))
184         return list_mac
185
186     def _extract_vpp_interface_by_mac(self, interfaces_list, mac_address):
187         """Returns interface dictionary from interface_list by mac address.
188
189         Extracts interface dictionary from all of the interfaces in interfaces
190         list parsed from json according to mac_address of the interface
191         :param interfaces_list: dictionary of all interfaces parsed from json
192         :param mac_address: string mac address of interface we are looking for
193         :return: interface dictionary from json
194         """
195
196         interface_dict = {}
197         list_mac_address = self.convert_mac_to_number_list(mac_address)
198         logger.trace(str(list_mac_address))
199         for interface in interfaces_list:
200             # TODO: create vat json integrity checking and move there
201             if "l2_address" not in interface:
202                 raise KeyError(
203                     "key l2_address not found in interface dict."
204                     "Probably input list is not parsed from correct VAT "
205                     "json output.")
206             if "l2_address_length" not in interface:
207                 raise KeyError(
208                     "key l2_address_length not found in interface "
209                     "dict. Probably input list is not parsed from correct "
210                     "VAT json output.")
211             mac_from_json = interface["l2_address"][:6]
212             if mac_from_json == list_mac_address:
213                 if interface["l2_address_length"] != 6:
214                     raise ValueError("l2_address_length value is not 6.")
215                 interface_dict = interface
216                 break
217         return interface_dict
218
219     def vpp_interface_name_from_json_by_mac(self, json_data, mac_address):
220         """Return vpp interface name string from VAT interface dump json output
221
222         Extracts the name given to an interface by VPP.
223         These interface names differ from what you would see if you
224         used the ipconfig or similar command.
225         Required json data can be obtained by calling :
226         VatExecutor.execute_script_json_out("dump_interfaces.vat", node)
227         :param json_data: string json data from sw_interface_dump VAT command
228         :param mac_address: string containing mac address of interface
229         whose vpp name we wish to discover.
230         :return: string vpp interface name
231         """
232
233         interfaces_list = JsonParser().parse_data(json_data)
234         # TODO: checking if json data is parsed correctly
235         interface_dict = self._extract_vpp_interface_by_mac(interfaces_list,
236                                                             mac_address)
237         interface_name = interface_dict["interface_name"]
238         return interface_name
239
240     def _update_node_interface_data_from_json(self, node, interface_dump_json):
241         """ Update node vpp data in node__DICT from json interface dump.
242
243         This method updates vpp interface names and sw indexexs according to
244         interface mac addresses found in interface_dump_json
245         :param node: node dictionary
246         :param interface_dump_json: json output from dump_interface_list VAT
247         command
248         """
249
250         interface_list = JsonParser().parse_data(interface_dump_json)
251         for ifc in node['interfaces'].values():
252             if 'link' not in ifc:
253                 continue
254             if_mac = ifc['mac_address']
255             interface_dict = self._extract_vpp_interface_by_mac(interface_list,
256                                                                 if_mac)
257             if not interface_dict:
258                 raise Exception('Interface {0} not found by MAC {1}'.
259                         format(ifc, if_mac))
260             ifc['name'] = interface_dict["interface_name"]
261             ifc['vpp_sw_index'] = interface_dict["sw_if_index"]
262
263     def update_vpp_interface_data_on_node(self, node):
264         """Update vpp generated interface data for a given node in DICT__nodes
265
266         Updates interface names, software index numbers and any other details
267         generated specifically by vpp that are unknown before testcase run.
268         :param node: Node selected from DICT__nodes
269         """
270
271         vat_executor = VatExecutor()
272         vat_executor.execute_script_json_out("dump_interfaces.vat", node)
273         interface_dump_json = vat_executor.get_script_stdout()
274         self._update_node_interface_data_from_json(node,
275                                                    interface_dump_json)
276
277     @staticmethod
278     def update_tg_interface_data_on_node(node):
279         """Update interface name for TG/linux node in DICT__nodes
280
281         :param node: Node selected from DICT__nodes.
282         :type node: dict
283
284         .. note::
285             # for dev in `ls /sys/class/net/`;
286             > do echo "\"`cat /sys/class/net/$dev/address`\": \"$dev\""; done
287             "52:54:00:9f:82:63": "eth0"
288             "52:54:00:77:ae:a9": "eth1"
289             "52:54:00:e1:8a:0f": "eth2"
290             "00:00:00:00:00:00": "lo"
291
292         .. todo:: parse lshw -json instead
293         """
294         # First setup interface driver specified in yaml file
295         InterfaceSetup.tg_set_interfaces_default_driver(node)
296
297         # Get interface names
298         ssh = SSH()
299         ssh.connect(node)
300
301         cmd = 'for dev in `ls /sys/class/net/`; do echo "\\"`cat ' \
302               '/sys/class/net/$dev/address`\\": \\"$dev\\""; done;'
303
304         (ret_code, stdout, _) = ssh.exec_command(cmd)
305         if int(ret_code) != 0:
306             raise Exception('Get interface name and MAC failed')
307         tmp = "{" + stdout.rstrip().replace('\n', ',') + "}"
308         interfaces = JsonParser().parse_data(tmp)
309         for if_k, if_v in node['interfaces'].items():
310             if if_k == 'mgmt':
311                 continue
312             name = interfaces.get(if_v['mac_address'])
313             if name is None:
314                 continue
315             if_v['name'] = name
316
317         # Set udev rules for interfaces
318         InterfaceSetup.tg_set_interfaces_udev_rules(node)
319
320     def update_all_interface_data_on_all_nodes(self, nodes):
321         """ Update interface names on all nodes in DICT__nodes
322
323         :param nodes: Nodes in the topology.
324         :type nodes: dict
325
326         This method updates the topology dictionary by querying interface lists
327         of all nodes mentioned in the topology dictionary.
328         It does this by dumping interface list to json output from all devices
329         using vpp_api_test, and pairing known information from topology
330         (mac address/pci address of interface) to state from VPP.
331         For TG/linux nodes add interface name only.
332         """
333
334         for node_data in nodes.values():
335             if node_data['type'] == NodeType.DUT:
336                 self.update_vpp_interface_data_on_node(node_data)
337             elif node_data['type'] == NodeType.TG:
338                 self.update_tg_interface_data_on_node(node_data)
339
340     @staticmethod
341     def get_interface_sw_index(node, interface):
342         """Get VPP sw_index for the interface.
343
344         :param node: Node to get interface sw_index on.
345         :param interface: Interface name.
346         :type node: dict
347         :type interface: str
348         :return: Return sw_index or None if not found.
349         """
350         for port in node['interfaces'].values():
351             port_name = port.get('name')
352             if port_name is None:
353                 continue
354             if port_name == interface:
355                 return port.get('vpp_sw_index')
356
357         return None
358
359     @staticmethod
360     def get_interface_mac_by_port_key(node, port_key):
361         """Get MAC address for the interface based on port key.
362
363         :param node: Node to get interface mac on.
364         :param port_key: Dictionary key name of interface.
365         :type node: dict
366         :type port_key: str
367         :return: Return MAC or None if not found.
368         """
369         for port_name, port_data in node['interfaces'].iteritems():
370             if port_name == port_key:
371                 return port_data['mac_address']
372
373         return None
374
375     @staticmethod
376     def get_interface_mac(node, interface):
377         """Get MAC address for the interface.
378
379         :param node: Node to get interface sw_index on.
380         :param interface: Interface name.
381         :type node: dict
382         :type interface: str
383         :return: Return MAC or None if not found.
384         """
385         for port in node['interfaces'].values():
386             port_name = port.get('name')
387             if port_name is None:
388                 continue
389             if port_name == interface:
390                 return port.get('mac_address')
391
392         return None
393
394     @staticmethod
395     def get_adjacent_node_and_interface_by_key(nodes_info, node, port_key):
396         """Get node and interface adjacent to specified interface
397         on local network.
398
399         :param nodes_info: Dictionary containing information on all nodes
400         in topology.
401         :param node: Node that contains specified interface.
402         :param port_key: Interface port key.
403         :type nodes_info: dict
404         :type node: dict
405         :type port_key: str
406         :return: Return (node, interface info) tuple or None if not found.
407         :rtype: (dict, dict)
408         """
409         link_name = None
410        # get link name where the interface belongs to
411         for port_name, port_data in node['interfaces'].iteritems():
412             if port_name == 'mgmt':
413                 continue
414             if port_name == port_key:
415                 link_name = port_data['link']
416                 break
417
418         if link_name is None: 
419             return None
420
421         # find link
422         for node_data in nodes_info.values():
423             # skip self
424             if node_data['host'] == node['host']:
425                 continue
426             for interface, interface_data \
427                     in node_data['interfaces'].iteritems():
428                 if 'link' not in interface_data:
429                     continue
430                 if interface_data['link'] == link_name:
431                     return node_data, node_data['interfaces'][interface]
432
433     @staticmethod
434     def get_adjacent_node_and_interface(nodes_info, node, interface_name):
435         """Get node and interface adjacent to specified interface
436         on local network.
437
438         :param nodes_info: Dictionary containing information on all nodes
439         in topology.
440         :param node: Node that contains specified interface.
441         :param interface_name: Interface name.
442         :type nodes_info: dict
443         :type node: dict
444         :type interface_name: str
445         :return: Return (node, interface info) tuple or None if not found.
446         :rtype: (dict, dict)
447         """
448         link_name = None
449         # get link name where the interface belongs to
450         for port_name, port_data in node['interfaces'].iteritems():
451             if port_name == 'mgmt':
452                 continue
453             if port_data['name'] == interface_name:
454                 link_name = port_data['link']
455                 break
456
457         if link_name is None:
458             return None
459
460         # find link
461         for node_data in nodes_info.values():
462             # skip self
463             if node_data['host'] == node['host']:
464                 continue
465             for interface, interface_data \
466                     in node_data['interfaces'].iteritems():
467                 if 'link' not in interface_data:
468                     continue
469                 if interface_data['link'] == link_name:
470                     return node_data, node_data['interfaces'][interface]
471
472     @staticmethod
473     def get_interface_pci_addr(node, interface):
474         """Get interface PCI address.
475
476         :param node: Node to get interface PCI address on.
477         :param interface: Interface name.
478         :type node: dict
479         :type interface: str
480         :return: Return PCI address or None if not found.
481         """
482         for port in node['interfaces'].values():
483             if interface == port.get('name'):
484                 return port.get('pci_address')
485         return None
486
487     @staticmethod
488     def get_interface_driver(node, interface):
489         """Get interface driver.
490
491         :param node: Node to get interface driver on.
492         :param interface: Interface name.
493         :type node: dict
494         :type interface: str
495         :return: Return interface driver or None if not found.
496         """
497         for port in node['interfaces'].values():
498             if interface == port.get('name'):
499                 return port.get('driver')
500         return None
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: string
510         :return: mac address string
511         """
512         for port in node['interfaces'].values():
513             if port.get('link') == link_name:
514                 return port.get('mac_address')
515         return None
516
517     @staticmethod
518     def _get_node_active_link_names(node):
519         """Returns list of link names that are other than mgmt links
520
521         :param node: node topology dictionary
522         :return: list of strings that represent link names occupied by the node
523         """
524         interfaces = node['interfaces']
525         link_names = []
526         for interface in interfaces.values():
527             if 'link' in interface:
528                 link_names.append(interface['link'])
529         if len(link_names) == 0:
530             link_names = None
531         return link_names
532
533     @keyword('Get active links connecting "${node1}" and "${node2}"')
534     def get_active_connecting_links(self, node1, node2):
535         """Returns list of link names that connect together node1 and node2
536
537         :param node1: node topology dictionary
538         :param node2: node topology dictionary
539         :return: list of strings that represent connecting link names
540         """
541
542         logger.trace("node1: {}".format(str(node1)))
543         logger.trace("node2: {}".format(str(node2)))
544         node1_links = self._get_node_active_link_names(node1)
545         node2_links = self._get_node_active_link_names(node2)
546         connecting_links = list(set(node1_links).intersection(node2_links))
547
548         return connecting_links
549
550     @keyword('Get first active connecting link between node "${node1}" and '
551              '"${node2}"')
552     def get_first_active_connecting_link(self, node1, node2):
553         """
554
555         :param node1: Connected node
556         :type node1: dict
557         :param node2: Connected node
558         :type node2: dict
559         :return: name of link connecting the two nodes together
560         :raises: RuntimeError
561         """
562
563         connecting_links = self.get_active_connecting_links(node1, node2)
564         if len(connecting_links) == 0:
565             raise RuntimeError("No links connecting the nodes were found")
566         else:
567             return connecting_links[0]
568
569     @keyword('Get egress interfaces on "${node1}" for link with "${node2}"')
570     def get_egress_interfaces_for_nodes(self, node1, node2):
571         """Get egress interfaces on node1 for link with node2.
572
573         :param node1: First node, node to get egress interface on.
574         :param node2: Second node.
575         :type node1: dict
576         :type node2: dict
577         :return: Engress interfaces.
578         :rtype: list
579         """
580         interfaces = []
581         links = self.get_active_connecting_links(node1, node2)
582         if len(links) == 0:
583             raise RuntimeError('No link between nodes')
584         for interface in node1['interfaces'].values():
585             link = interface.get('link')
586             if link is None:
587                 continue
588             if link in links:
589                 continue
590             name = interface.get('name')
591             if name is None:
592                 continue
593             interfaces.append(name)
594         return interfaces
595
596     @keyword('Get first egress interface on "${node1}" for link with '
597              '"${node2}"')
598     def get_first_egress_interface_for_nodes(self, node1, node2):
599         """Get first egress interface on node1 for link with node2.
600
601         :param node1: First node, node to get egress interface on.
602         :param node2: Second node.
603         :type node1: dict
604         :type node2: dict
605         :return: Engress interface.
606         :rtype: str
607         """
608         interfaces = self.get_egress_interfaces_for_nodes(node1, node2)
609         if not interfaces:
610             raise RuntimeError('No engress interface for nodes')
611         return interfaces[0]
612
613     @keyword('Get link data useful in circular topology test from tg "${tgen}"'
614              ' dut1 "${dut1}" dut2 "${dut2}"')
615     def get_links_dict_from_nodes(self, tgen, dut1, dut2):
616         """Returns link combinations used in tests in circular topology.
617
618         For the time being it returns links from the Node path:
619         TG->DUT1->DUT2->TG
620         :param tg: traffic generator node data
621         :param dut1: DUT1 node data
622         :param dut2: DUT2 node data
623         :type tg: dict
624         :type dut1: dict
625         :type dut2: dict
626         :return: dictionary of possible link combinations
627         the naming convention until changed to something more general is
628         implemented is this:
629         DUT1_DUT2_LINK: link name between DUT! and DUT2
630         DUT1_TG_LINK: link name between DUT1 and TG
631         DUT2_TG_LINK: link name between DUT2 and TG
632         TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
633         to and from
634         DUT1_BD_LINKS: list of link names that will be connected by the bridge
635         domain on DUT1
636         DUT2_BD_LINKS: list of link names that will be connected by the bridge
637         domain on DUT2
638         """
639         # TODO: replace with generic function.
640         dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
641         dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
642         dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
643         tg_traffic_links = [dut1_tg_link, dut2_tg_link]
644         dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
645         dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
646         topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
647                           'DUT1_TG_LINK': dut1_tg_link,
648                           'DUT2_TG_LINK': dut2_tg_link,
649                           'TG_TRAFFIC_LINKS': tg_traffic_links,
650                           'DUT1_BD_LINKS': dut1_bd_links,
651                           'DUT2_BD_LINKS': dut2_bd_links}
652         return topology_links
653
654     @staticmethod
655     def is_tg_node(node):
656         """Find out whether the node is TG
657
658         :param node: node to examine
659         :return: True if node is type of TG; False otherwise
660         """
661         return node['type'] == NodeType.TG
662
663     @staticmethod
664     def get_node_hostname(node):
665         """
666         :param node: node dictionary
667         :return: host name as 'str' type
668         """
669         return node['host']