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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
14 """Defines nodes and topology structure."""
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
25 __all__ = ["DICT__nodes", 'Topology']
28 def load_topo_from_yaml():
29 """Loads topology from file defined in "${TOPOLOGY_PATH}" variable
31 :return: nodes from loaded topology
33 topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}")
35 with open(topo_path) as work_file:
36 return load(work_file.read())['nodes']
39 class NodeType(object):
40 """Defines node types used in topology dictionaries"""
41 # Device Under Test (this node has VPP running on it)
43 # Traffic Generator (this node has traffic generator on it)
46 DICT__nodes = load_topo_from_yaml()
49 class Topology(object):
50 """Topology data manipulation and extraction methods
52 Defines methods used for manipulation and extraction of data from
60 def get_node_by_hostname(nodes, hostname):
61 """Get node from nodes of the topology by hostname.
63 :param nodes: Nodes of the test topology.
64 :param hostname: Host name.
67 :return: Node dictionary or None if not found.
69 for node in nodes.values():
70 if node['host'] == hostname:
77 """Get list of links(networks) in the topology.
79 :param nodes: Nodes of the test topology.
81 :return: Links in the topology.
86 for node in nodes.values():
87 for interface in node['interfaces'].values():
88 link = interface.get('link')
96 def _get_interface_by_key_value(node, key, value):
97 """ Return node interface name according to key and value
99 :param node: :param node: the node dictionary
100 :param key: key by which to select the interface.
101 :param value: value that should be found using the key.
105 interfaces = node['interfaces']
107 for interface in interfaces.values():
108 k_val = interface.get(key)
109 if k_val is not None:
111 retval = interface['name']
115 def get_interface_by_link_name(self, node, link_name):
116 """Return interface name of link on node.
118 This method returns the interface name asociated with a given link
120 :param link_name: name of the link that a interface is connected to.
121 :param node: the node topology dictionary
122 :return: interface name of the interface connected to the given link
125 return self._get_interface_by_key_value(node, "link", link_name)
127 def get_interfaces_by_link_names(self, node, link_names):
128 """Return dictionary of dicitonaries {"interfaceN", interface name}.
130 This method returns the interface names asociated with given links
132 The resulting dictionary can be then used to with VatConfigGenerator
133 to generate a VAT script with proper interface names.
134 :param link_names: list of names of the link that a interface is
136 :param node: the node topology directory
137 :return: dictionary of interface names that are connected to the given
142 interface_key_tpl = "interface{}"
144 for link_name in link_names:
145 interface_name = self.get_interface_by_link_name(node, link_name)
146 interface_key = interface_key_tpl.format(str(interface_number))
147 retval[interface_key] = interface_name
148 interface_number += 1
151 def get_interface_by_sw_index(self, node, sw_index):
152 """Return interface name of link on node.
154 This method returns the interface name asociated with a software index
155 assigned to the interface by vpp for a given node.
156 :param sw_index: sw_index of the link that a interface is connected to.
157 :param node: the node topology dictionary
158 :return: interface name of the interface connected to the given link
161 return self._get_interface_by_key_value(node, "vpp_sw_index", sw_index)
164 def convert_mac_to_number_list(mac_address):
165 """Convert mac address string to list of decimal numbers.
167 Converts a : separated mac address to decimal number list as used
168 in json interface dump.
169 :param mac_address: string mac address
170 :return: list representation of mac address
174 for num in mac_address.split(":"):
175 list_mac.append(int(num, 16))
178 def _extract_vpp_interface_by_mac(self, interfaces_list, mac_address):
179 """Returns interface dictionary from interface_list by mac address.
181 Extracts interface dictionary from all of the interfaces in interfaces
182 list parsed from json according to mac_address of the interface
183 :param interfaces_list: dictionary of all interfaces parsed from json
184 :param mac_address: string mac address of interface we are looking for
185 :return: interface dictionary from json
189 list_mac_address = self.convert_mac_to_number_list(mac_address)
190 logger.trace(list_mac_address.__str__())
191 for interface in interfaces_list:
192 # TODO: create vat json integrity checking and move there
193 if "l2_address" not in interface:
195 "key l2_address not found in interface dict."
196 "Probably input list is not parsed from correct VAT "
198 if "l2_address_length" not in interface:
200 "key l2_address_length not found in interface "
201 "dict. Probably input list is not parsed from correct "
203 mac_from_json = interface["l2_address"][:6]
204 if mac_from_json == list_mac_address:
205 if interface["l2_address_length"] != 6:
206 raise ValueError("l2_address_length value is not 6.")
207 interface_dict = interface
209 return interface_dict
211 def vpp_interface_name_from_json_by_mac(self, json_data, mac_address):
212 """Return vpp interface name string from VAT interface dump json output
214 Extracts the name given to an interface by VPP.
215 These interface names differ from what you would see if you
216 used the ipconfig or similar command.
217 Required json data can be obtained by calling :
218 VatExecutor.execute_script_json_out("dump_interfaces.vat", node)
219 :param json_data: string json data from sw_interface_dump VAT command
220 :param mac_address: string containing mac address of interface
221 whose vpp name we wish to discover.
222 :return: string vpp interface name
225 interfaces_list = JsonParser().parse_data(json_data)
226 # TODO: checking if json data is parsed correctly
227 interface_dict = self._extract_vpp_interface_by_mac(interfaces_list,
229 interface_name = interface_dict["interface_name"]
230 return interface_name
232 def _update_node_interface_data_from_json(self, node, interface_dump_json):
233 """ Update node vpp data in node__DICT from json interface dump.
235 This method updates vpp interface names and sw indexexs according to
236 interface mac addresses found in interface_dump_json
237 :param node: node dictionary
238 :param interface_dump_json: json output from dump_interface_list VAT
242 interface_list = JsonParser().parse_data(interface_dump_json)
243 for ifc in node['interfaces'].values():
244 if 'link' not in ifc:
246 if_mac = ifc['mac_address']
247 interface_dict = self._extract_vpp_interface_by_mac(interface_list,
249 ifc['name'] = interface_dict["interface_name"]
250 ifc['vpp_sw_index'] = interface_dict["sw_if_index"]
252 def update_vpp_interface_data_on_node(self, node):
253 """Update vpp generated interface data for a given node in DICT__nodes
255 Updates interface names, software index numbers and any other details
256 generated specifically by vpp that are unknown before testcase run.
257 :param node: Node selected from DICT__nodes
260 vat_executor = VatExecutor()
261 vat_executor.execute_script_json_out("dump_interfaces.vat", node)
262 interface_dump_json = vat_executor.get_script_stdout()
263 self._update_node_interface_data_from_json(node,
267 def update_tg_interface_data_on_node(node):
268 """Update interface name for TG/linux node in DICT__nodes
270 :param node: Node selected from DICT__nodes.
274 # for dev in `ls /sys/class/net/`;
275 > do echo "\"`cat /sys/class/net/$dev/address`\": \"$dev\""; done
276 "52:54:00:9f:82:63": "eth0"
277 "52:54:00:77:ae:a9": "eth1"
278 "52:54:00:e1:8a:0f": "eth2"
279 "00:00:00:00:00:00": "lo"
281 .. todo:: parse lshw -json instead
283 # First setup interface driver specified in yaml file
284 InterfaceSetup.tg_set_interfaces_default_driver(node)
286 # Get interface names
290 cmd = 'for dev in `ls /sys/class/net/`; do echo "\\"`cat ' \
291 '/sys/class/net/$dev/address`\\": \\"$dev\\""; done;'
293 (ret_code, stdout, _) = ssh.exec_command(cmd)
294 if int(ret_code) != 0:
295 raise Exception('Get interface name and MAC failed')
296 tmp = "{" + stdout.rstrip().replace('\n', ',') + "}"
297 interfaces = JsonParser().parse_data(tmp)
298 for if_k, if_v in node['interfaces'].items():
301 name = interfaces.get(if_v['mac_address'])
306 # Set udev rules for interfaces
307 InterfaceSetup.tg_set_interfaces_udev_rules(node)
309 def update_all_interface_data_on_all_nodes(self, nodes):
310 """ Update interface names on all nodes in DICT__nodes
312 :param nodes: Nodes in the topology.
315 This method updates the topology dictionary by querying interface lists
316 of all nodes mentioned in the topology dictionary.
317 It does this by dumping interface list to json output from all devices
318 using vpe_api_test, and pairing known information from topology
319 (mac address/pci address of interface) to state from VPP.
320 For TG/linux nodes add interface name only.
323 for node_data in nodes.values():
324 if node_data['type'] == NodeType.DUT:
325 self.update_vpp_interface_data_on_node(node_data)
326 elif node_data['type'] == NodeType.TG:
327 self.update_tg_interface_data_on_node(node_data)
330 def get_interface_sw_index(node, interface):
331 """Get VPP sw_index for the interface.
333 :param node: Node to get interface sw_index on.
334 :param interface: Interface name.
337 :return: Return sw_index or None if not found.
339 for port in node['interfaces'].values():
340 port_name = port.get('name')
341 if port_name is None:
343 if port_name == interface:
344 return port.get('vpp_sw_index')
349 def get_interface_mac(node, interface):
350 """Get MAC address for the interface.
352 :param node: Node to get interface sw_index on.
353 :param interface: Interface name.
356 :return: Return MAC or None if not found.
358 for port in node['interfaces'].values():
359 port_name = port.get('name')
360 if port_name is None:
362 if port_name == interface:
363 return port.get('mac_address')
368 def get_adjacent_node_and_interface(nodes_info, node, interface_name):
369 """Get node and interface adjacent to specified interface
372 :param nodes_info: Dictionary containing information on all nodes
374 :param node: Node that contains specified interface.
375 :param interface_name: Interface name.
376 :type nodes_info: dict
378 :type interface_name: str
379 :return: Return (node, interface info) tuple or None if not found.
383 # get link name where the interface belongs to
384 for port_name, port_data in node['interfaces'].iteritems():
385 if port_name == 'mgmt':
387 if port_data['name'] == interface_name:
388 link_name = port_data['link']
391 if link_name is None:
395 for node_data in nodes_info.values():
397 if node_data['host'] == node['host']:
399 for interface, interface_data \
400 in node_data['interfaces'].iteritems():
401 if 'link' not in interface_data:
403 if interface_data['link'] == link_name:
404 return node_data, node_data['interfaces'][interface]
407 def get_interface_pci_addr(node, interface):
408 """Get interface PCI address.
410 :param node: Node to get interface PCI address on.
411 :param interface: Interface name.
414 :return: Return PCI address or None if not found.
416 for port in node['interfaces'].values():
417 if interface == port.get('name'):
418 return port.get('pci_address')
422 def get_interface_driver(node, interface):
423 """Get interface driver.
425 :param node: Node to get interface driver on.
426 :param interface: Interface name.
429 :return: Return interface driver or None if not found.
431 for port in node['interfaces'].values():
432 if interface == port.get('name'):
433 return port.get('driver')
437 def get_node_link_mac(node, link_name):
438 """Return interface mac address by link name
440 :param node: Node to get interface sw_index on
441 :param link_name: link name
443 :type link_name: string
444 :return: mac address string
446 for port in node['interfaces'].values():
447 if port.get('link') == link_name:
448 return port.get('mac_address')
452 def _get_node_active_link_names(node):
453 """Returns list of link names that are other than mgmt links
455 :param node: node topology dictionary
456 :return: list of strings that represent link names occupied by the node
458 interfaces = node['interfaces']
460 for interface in interfaces.values():
461 if 'link' in interface:
462 link_names.append(interface['link'])
463 if len(link_names) == 0:
467 @keyword('Get active links connecting "${node1}" and "${node2}"')
468 def get_active_connecting_links(self, node1, node2):
469 """Returns list of link names that connect together node1 and node2
471 :param node1: node topology dictionary
472 :param node2: node topology dictionary
473 :return: list of strings that represent connecting link names
476 logger.trace("node1: {}".format(str(node1)))
477 logger.trace("node2: {}".format(str(node2)))
478 node1_links = self._get_node_active_link_names(node1)
479 node2_links = self._get_node_active_link_names(node2)
480 connecting_links = list(set(node1_links).intersection(node2_links))
482 return connecting_links
484 @keyword('Get first active connecting link between node "${node1}" and '
486 def get_first_active_connecting_link(self, node1, node2):
489 :param node1: Connected node
491 :param node2: Connected node
493 :return: name of link connecting the two nodes together
494 :raises: RuntimeError
497 connecting_links = self.get_active_connecting_links(node1, node2)
498 if len(connecting_links) == 0:
499 raise RuntimeError("No links connecting the nodes were found")
501 return connecting_links[0]
503 @keyword('Get egress interfaces on "${node1}" for link with "${node2}"')
504 def get_egress_interfaces_for_nodes(self, node1, node2):
505 """Get egress interfaces on node1 for link with node2.
507 :param node1: First node, node to get egress interface on.
508 :param node2: Second node.
511 :return: Engress interfaces.
515 links = self.get_active_connecting_links(node1, node2)
517 raise RuntimeError('No link between nodes')
518 for interface in node1['interfaces'].values():
519 link = interface.get('link')
524 name = interface.get('name')
527 interfaces.append(name)
530 @keyword('Get first egress interface on "${node1}" for link with '
532 def get_first_egress_interface_for_nodes(self, node1, node2):
533 """Get first egress interface on node1 for link with node2.
535 :param node1: First node, node to get egress interface on.
536 :param node2: Second node.
539 :return: Engress interface.
542 interfaces = self.get_egress_interfaces_for_nodes(node1, node2)
544 raise RuntimeError('No engress interface for nodes')
547 @keyword('Get link data useful in circular topology test from tg "${tgen}"'
548 ' dut1 "${dut1}" dut2 "${dut2}"')
549 def get_links_dict_from_nodes(self, tgen, dut1, dut2):
550 """Returns link combinations used in tests in circular topology.
552 For the time being it returns links from the Node path:
554 :param tg: traffic generator node data
555 :param dut1: DUT1 node data
556 :param dut2: DUT2 node data
560 :return: dictionary of possible link combinations
561 the naming convention until changed to something more general is
563 DUT1_DUT2_LINK: link name between DUT! and DUT2
564 DUT1_TG_LINK: link name between DUT1 and TG
565 DUT2_TG_LINK: link name between DUT2 and TG
566 TG_TRAFFIC_LINKS: list of link names that generated traffic is sent
568 DUT1_BD_LINKS: list of link names that will be connected by the bridge
570 DUT2_BD_LINKS: list of link names that will be connected by the bridge
573 # TODO: replace with generic function.
574 dut1_dut2_link = self.get_first_active_connecting_link(dut1, dut2)
575 dut1_tg_link = self.get_first_active_connecting_link(dut1, tgen)
576 dut2_tg_link = self.get_first_active_connecting_link(dut2, tgen)
577 tg_traffic_links = [dut1_tg_link, dut2_tg_link]
578 dut1_bd_links = [dut1_dut2_link, dut1_tg_link]
579 dut2_bd_links = [dut1_dut2_link, dut2_tg_link]
580 topology_links = {'DUT1_DUT2_LINK': dut1_dut2_link,
581 'DUT1_TG_LINK': dut1_tg_link,
582 'DUT2_TG_LINK': dut2_tg_link,
583 'TG_TRAFFIC_LINKS': tg_traffic_links,
584 'DUT1_BD_LINKS': dut1_bd_links,
585 'DUT2_BD_LINKS': dut2_bd_links}
586 return topology_links