diff options
author | Stefan Kobza <skobza@cisco.com> | 2016-01-11 18:03:25 +0100 |
---|---|---|
committer | Stefan Kobza <skobza@cisco.com> | 2016-02-08 22:38:32 +0100 |
commit | 33499c81c94c2d3baef9d3e9f061cd76ef86fa74 (patch) | |
tree | 2d000ba17b821339a05e0c039f71e48e09553de9 /resources/libraries/python | |
parent | 5cbeca02602061d32212e14f289d65cf648920e4 (diff) |
New version of RF tests.
Change-Id: I241a2b7a7706e65f71cfd4a62e2a40f053fc5d07
Signed-off-by: Stefan Kobza <skobza@cisco.com>
Diffstat (limited to 'resources/libraries/python')
23 files changed, 3209 insertions, 0 deletions
diff --git a/resources/libraries/python/DUTSetup.py b/resources/libraries/python/DUTSetup.py new file mode 100644 index 0000000000..76f76aef7e --- /dev/null +++ b/resources/libraries/python/DUTSetup.py @@ -0,0 +1,41 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from robot.api import logger +from topology import NodeType +from ssh import SSH +from constants import Constants + +class DUTSetup(object): + + def __init__(self): + pass + + def setup_all_duts(self, nodes): + """Prepare all DUTs in given topology for test execution.""" + for node in nodes.values(): + if node['type'] == NodeType.DUT: + self.setup_dut(node) + + def setup_dut(self, node): + ssh = SSH() + ssh.connect(node) + + (ret_code, stdout, stderr) = \ + ssh.exec_command('sudo -Sn bash {0}/{1}/dut_setup.sh'.format( + Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH)) + logger.trace(stdout) + if 0 != int(ret_code): + logger.error('DUT {0} setup script failed: "{1}"'. + format(node['host'], stdout + stderr)) + raise Exception('DUT test setup script failed at node {}'. + format(node['host'])) diff --git a/resources/libraries/python/IPUtil.py b/resources/libraries/python/IPUtil.py new file mode 100644 index 0000000000..3e002b3495 --- /dev/null +++ b/resources/libraries/python/IPUtil.py @@ -0,0 +1,43 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common IP utilities library.""" + +from ssh import SSH +from constants import Constants + + +class IPUtil(object): + """Common IP utilities""" + + def __init__(self): + pass + + @staticmethod + def vpp_ip_probe(node, interface, addr): + """Run ip probe on VPP node. + + Args: + node (Dict): VPP node. + interface (str): Interface name + addr (str): IPv4/IPv6 address + """ + ssh = SSH() + ssh.connect(node) + + cmd = "{c}".format(c=Constants.VAT_BIN_NAME) + cmd_input = 'exec ip probe {dev} {ip}'.format(dev=interface, ip=addr) + (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input) + if int(ret_code) != 0: + raise Exception('VPP ip probe {dev} {ip} failed on {h}'.format( + dev=interface, ip=addr, h=node['host'])) diff --git a/resources/libraries/python/IPv4NodeAddress.py b/resources/libraries/python/IPv4NodeAddress.py new file mode 100644 index 0000000000..0f2c1d9cd3 --- /dev/null +++ b/resources/libraries/python/IPv4NodeAddress.py @@ -0,0 +1,104 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Robot framework variable file. + + Create dictionary variable nodes_ipv4_addr of IPv4 addresses from + available networks. +""" +from ipaddress import IPv4Network + +# Default list of IPv4 subnets +IPV4_NETWORKS = ['192.168.1.0/24', + '192.168.2.0/24', + '192.168.3.0/24'] + + +class IPv4NetworkGenerator(object): + """IPv4 network generator.""" + def __init__(self, networks): + """ + :param networks: list of strings containing IPv4 subnet + with prefix length + """ + self._networks = list() + for network in networks: + net = IPv4Network(unicode(network)) + subnet, _ = network.split('/') + self._networks.append((net, subnet)) + if len(self._networks) == 0: + raise Exception('No IPv4 networks') + + def next_network(self): + """ + :return: next network in form (IPv4Network, subnet) + """ + if len(self._networks): + return self._networks.pop() + else: + raise StopIteration() + + +def get_variables(networks=IPV4_NETWORKS[:]): + """ + Create dictionary of IPv4 addresses generated from provided subnet list. + + Example of returned dictionary: + network = { + 'NET1': { + 'subnet': '192.168.1.0', + 'prefix': 24, + 'port1': { + 'addr': '192.168.1.1', + }, + 'port2': { + 'addr': '192.168.1.0', + }, + }, + 'NET2': { + 'subnet': '192.168.2.0', + 'prefix': 24, + 'port1': { + 'addr': '192.168.2.1', + }, + 'port2': { + 'addr': '192.168.2.2', + }, + }, + } + + This function is called by RobotFramework automatically. + + :param networks: list of subnets in form a.b.c.d/length + :return: Dictionary of IPv4 addresses + """ + net_object = IPv4NetworkGenerator(networks) + + network = {} + interface_count_per_node = 2 + + for subnet_num in range(len(networks)): + net, net_str = net_object.next_network() + key = 'NET{}'.format(subnet_num + 1) + network[key] = { + 'subnet': net_str, + 'prefix': net.prefixlen, + } + hosts = net.hosts() + for port_num in range(interface_count_per_node): + port = 'port{}'.format(port_num + 1) + network[key][port] = { + 'addr': str(next(hosts)), + } + + return {'DICT__nodes_ipv4_addr': network} diff --git a/resources/libraries/python/IPv4Util.py b/resources/libraries/python/IPv4Util.py new file mode 100644 index 0000000000..5480bfcea3 --- /dev/null +++ b/resources/libraries/python/IPv4Util.py @@ -0,0 +1,499 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Implements IPv4 RobotFramework keywords""" + +from socket import inet_ntoa +from struct import pack +from abc import ABCMeta, abstractmethod +import copy + +from robot.api import logger as log +from robot.api.deco import keyword +from robot.utils.asserts import assert_not_equal + +import resources.libraries.python.ssh as ssh +from resources.libraries.python.topology import Topology +from resources.libraries.python.topology import NodeType +from resources.libraries.python.VatExecutor import VatExecutor +from resources.libraries.python.TrafficScriptExecutor\ + import TrafficScriptExecutor + + +class IPv4Node(object): + """Abstract class of a node in a topology.""" + __metaclass__ = ABCMeta + + def __init__(self, node_info): + self.node_info = node_info + + @staticmethod + def _get_netmask(prefix_length): + bits = 0xffffffff ^ (1 << 32 - prefix_length) - 1 + return inet_ntoa(pack('>I', bits)) + + @abstractmethod + def set_ip(self, interface, address, prefix_length): + """Configure IPv4 address on interface + :param interface: interface name + :param address: + :param prefix_length: + :type interface: str + :type address: str + :type prefix_length: int + :return: nothing + """ + pass + + @abstractmethod + def set_interface_state(self, interface, state): + """Set interface state + :param interface: interface name string + :param state: one of following values: "up" or "down" + :return: nothing + """ + pass + + @abstractmethod + def set_route(self, network, prefix_length, gateway, interface): + """Configure IPv4 route + :param network: network IPv4 address + :param prefix_length: mask length + :param gateway: IPv4 address of the gateway + :param interface: interface name + :type network: str + :type prefix_length: int + :type gateway: str + :type interface: str + :return: nothing + """ + pass + + @abstractmethod + def unset_route(self, network, prefix_length, gateway, interface): + """Remove specified IPv4 route + :param network: network IPv4 address + :param prefix_length: mask length + :param gateway: IPv4 address of the gateway + :param interface: interface name + :type network: str + :type prefix_length: int + :type gateway: str + :type interface: str + :return: nothing + """ + pass + + @abstractmethod + def flush_ip_addresses(self, interface): + """Flush all IPv4 addresses from specified interface + :param interface: interface name + :type interface: str + :return: nothing + """ + pass + + @abstractmethod + def ping(self, destination_address, source_interface): + """Send an ICMP request to destination node + :param destination_address: address to send the ICMP request + :param source_interface: + :type destination_address: str + :type source_interface: str + :return: nothing + """ + pass + + +class Tg(IPv4Node): + """Traffic generator node""" + def __init__(self, node_info): + super(Tg, self).__init__(node_info) + + def _execute(self, cmd): + return ssh.exec_cmd_no_error(self.node_info, cmd) + + def _sudo_execute(self, cmd): + return ssh.exec_cmd_no_error(self.node_info, cmd, sudo=True) + + def set_ip(self, interface, address, prefix_length): + cmd = 'ip -4 addr flush dev {}'.format(interface) + self._sudo_execute(cmd) + cmd = 'ip addr add {}/{} dev {}'.format(address, prefix_length, + interface) + self._sudo_execute(cmd) + + # TODO: not ipv4-specific, move to another class + def set_interface_state(self, interface, state): + cmd = 'ip link set {} {}'.format(interface, state) + self._sudo_execute(cmd) + + def set_route(self, network, prefix_length, gateway, interface): + netmask = self._get_netmask(prefix_length) + cmd = 'route add -net {} netmask {} gw {}'.\ + format(network, netmask, gateway) + self._sudo_execute(cmd) + + def unset_route(self, network, prefix_length, gateway, interface): + self._sudo_execute('ip route delete {}/{}'. + format(network, prefix_length)) + + def arp_ping(self, destination_address, source_interface): + self._sudo_execute('arping -c 1 -I {} {}'.format(source_interface, + destination_address)) + + def ping(self, destination_address, source_interface): + self._execute('ping -c 1 -w 5 -I {} {}'.format(source_interface, + destination_address)) + + def flush_ip_addresses(self, interface): + self._sudo_execute('ip addr flush dev {}'.format(interface)) + + +class Dut(IPv4Node): + """Device under test""" + def __init__(self, node_info): + super(Dut, self).__init__(node_info) + + def get_sw_if_index(self, interface): + """Get sw_if_index of specified interface from current node + :param interface: interface name + :type interface: str + :return: sw_if_index of 'int' type + """ + return Topology().get_interface_sw_index(self.node_info, interface) + + def exec_vat(self, script, **args): + """Wrapper for VAT executor. + :param script: script to execute + :param args: parameters to the script + :type script: str + :type args: dict + :return: nothing + """ + # TODO: check return value + VatExecutor.cmd_from_template(self.node_info, script, **args) + + def set_ip(self, interface, address, prefix_length): + self.exec_vat('add_ip_address.vat', + sw_if_index=self.get_sw_if_index(interface), + address=address, prefix_length=prefix_length) + + def set_interface_state(self, interface, state): + if state == 'up': + state = 'admin-up link-up' + elif state == 'down': + state = 'admin-down link-down' + else: + raise Exception('Unexpected interface state: {}'.format(state)) + + self.exec_vat('set_if_state.vat', + sw_if_index=self.get_sw_if_index(interface), state=state) + + def set_route(self, network, prefix_length, gateway, interface): + sw_if_index = self.get_sw_if_index(interface) + self.exec_vat('add_route.vat', + network=network, prefix_length=prefix_length, + gateway=gateway, sw_if_index=sw_if_index) + + def unset_route(self, network, prefix_length, gateway, interface): + self.exec_vat('del_route.vat', network=network, + prefix_length=prefix_length, gateway=gateway, + sw_if_index=self.get_sw_if_index(interface)) + + def arp_ping(self, destination_address, source_interface): + pass + + def flush_ip_addresses(self, interface): + self.exec_vat('flush_ip_addresses.vat', + sw_if_index=self.get_sw_if_index(interface)) + + def ping(self, destination_address, source_interface): + pass + + +def get_node(node_info): + """Creates a class instance derived from Node based on type. + :param node_info: dictionary containing information on nodes in topology + :return: Class instance that is derived from Node + """ + if node_info['type'] == NodeType.TG: + return Tg(node_info) + elif node_info['type'] == NodeType.DUT: + return Dut(node_info) + else: + raise NotImplementedError('Node type "{}" unsupported!'. + format(node_info['type'])) + + +def get_node_hostname(node_info): + """Get string identifying specifed node. + :param node_info: Node in the topology. + :type node_info: Dict + :return: String identifying node. + """ + return node_info['host'] + + +class IPv4Util(object): + """Implements keywords for IPv4 tests.""" + + ADDRESSES = {} # holds configured IPv4 addresses + PREFIXES = {} # holds configured IPv4 addresses' prefixes + SUBNETS = {} # holds configured IPv4 addresses' subnets + + """ + Helper dictionary used when setting up ipv4 addresses in topology + + Example value: + 'link1': { b'port1': {b'addr': b'192.168.3.1'}, + b'port2': {b'addr': b'192.168.3.2'}, + b'prefix': 24, + b'subnet': b'192.168.3.0'} + """ + topology_helper = None + + @staticmethod + def next_address(subnet): + """Get next unused IPv4 address from a subnet + :param subnet: holds available IPv4 addresses + :return: tuple (ipv4_address, prefix_length) + """ + for i in range(1, 4): + # build a key and try to get it from address dictionary + interface = 'port{}'.format(i) + if interface in subnet: + addr = subnet[interface]['addr'] + del subnet[interface] + return addr, subnet['prefix'] + raise Exception('Not enough ipv4 addresses in subnet') + + @staticmethod + def next_network(nodes_addr): + """Get next unused network from dictionary + :param nodes_addr: dictionary of available networks + :return: dictionary describing an IPv4 subnet with addresses + """ + assert_not_equal(len(nodes_addr), 0, 'Not enough networks') + _, subnet = nodes_addr.popitem() + return subnet + + @staticmethod + def configure_ipv4_addr_on_node(node, nodes_addr): + """Configure IPv4 address for all interfaces on a node in topology + :param node: dictionary containing information about node + :param nodes_addr: dictionary containing IPv4 addresses + :return: + """ + for interface, interface_data in node['interfaces'].iteritems(): + if interface == 'mgmt': + continue + if interface_data['link'] not in IPv4Util.topology_helper: + IPv4Util.topology_helper[interface_data['link']] = \ + IPv4Util.next_network(nodes_addr) + + network = IPv4Util.topology_helper[interface_data['link']] + address, prefix = IPv4Util.next_address(network) + + get_node(node).set_ip(interface_data['name'], address, prefix) + key = (get_node_hostname(node), interface_data['name']) + IPv4Util.ADDRESSES[key] = address + IPv4Util.PREFIXES[key] = prefix + IPv4Util.SUBNETS[key] = network['subnet'] + + @staticmethod + def nodes_setup_ipv4_addresses(nodes_info, nodes_addr): + """Configure IPv4 addresses on all non-management interfaces for each + node in nodes_info + :param nodes_info: dictionary containing information on all nodes + in topology + :param nodes_addr: dictionary containing IPv4 addresses + :return: nothing + """ + IPv4Util.topology_helper = {} + # make a deep copy of nodes_addr because of modifications + nodes_addr_copy = copy.deepcopy(nodes_addr) + for _, node in nodes_info.iteritems(): + IPv4Util.configure_ipv4_addr_on_node(node, nodes_addr_copy) + + @staticmethod + def nodes_clear_ipv4_addresses(nodes): + """Clear all addresses from all nodes in topology + :param nodes: dictionary containing information on all nodes + :return: nothing + """ + for _, node in nodes.iteritems(): + for interface, interface_data in node['interfaces'].iteritems(): + if interface == 'mgmt': + continue + IPv4Util.flush_ip_addresses(interface_data['name'], node) + + # TODO: not ipv4-specific, move to another class + @staticmethod + @keyword('Node "${node}" interface "${interface}" is in "${state}" state') + def set_interface_state(node, interface, state): + """See IPv4Node.set_interface_state for more information. + :param node: + :param interface: + :param state: + :return: + """ + log.debug('Node {} interface {} is in {} state'.format( + get_node_hostname(node), interface, state)) + get_node(node).set_interface_state(interface, state) + + @staticmethod + @keyword('Node "${node}" interface "${port}" has IPv4 address ' + '"${address}" with prefix length "${prefix_length}"') + def set_interface_address(node, interface, address, length): + """See IPv4Node.set_ip for more information. + :param node: + :param interface: + :param address: + :param length: + :return: + """ + log.debug('Node {} interface {} has IPv4 address {} with prefix ' + 'length {}'.format(get_node_hostname(node), interface, + address, length)) + get_node(node).set_ip(interface, address, int(length)) + hostname = get_node_hostname(node) + IPv4Util.ADDRESSES[hostname, interface] = address + IPv4Util.PREFIXES[hostname, interface] = int(length) + # TODO: Calculate subnet from ip address and prefix length. + # IPv4Util.SUBNETS[hostname, interface] = + + @staticmethod + @keyword('From node "${node}" interface "${port}" ARP-ping ' + 'IPv4 address "${ip_address}"') + def arp_ping(node, interface, ip_address): + log.debug('From node {} interface {} ARP-ping IPv4 address {}'. + format(get_node_hostname(node), interface, ip_address)) + get_node(node).arp_ping(ip_address, interface) + + @staticmethod + @keyword('Node "${node}" routes to IPv4 network "${network}" with prefix ' + 'length "${prefix_length}" using interface "${interface}" via ' + '"${gateway}"') + def set_route(node, network, prefix_length, interface, gateway): + """See IPv4Node.set_route for more information. + :param node: + :param network: + :param prefix_length: + :param interface: + :param gateway: + :return: + """ + log.debug('Node {} routes to network {} with prefix length {} ' + 'via {} interface {}'.format(get_node_hostname(node), + network, prefix_length, + gateway, interface)) + get_node(node).set_route(network, int(prefix_length), + gateway, interface) + + @staticmethod + @keyword('Remove IPv4 route from "${node}" to network "${network}" with ' + 'prefix length "${prefix_length}" interface "${interface}" via ' + '"${gateway}"') + def unset_route(node, network, prefix_length, interface, gateway): + """See IPv4Node.unset_route for more information. + :param node: + :param network: + :param prefix_length: + :param interface: + :param gateway: + :return: + """ + get_node(node).unset_route(network, prefix_length, gateway, interface) + + @staticmethod + @keyword('After ping is sent from node "${src_node}" interface ' + '"${src_port}" with destination IPv4 address of node ' + '"${dst_node}" interface "${dst_port}" a ping response arrives ' + 'and TTL is decreased by "${ttl_dec}"') + def send_ping(src_node, src_port, dst_node, dst_port, hops): + """Send IPv4 ping and wait for response. + :param src_node: Source node. + :param src_port: Source interface. + :param dst_node: Destination node. + :param dst_port: Destination interface. + :param hops: Number of hops between src_node and dst_node. + """ + log.debug('After ping is sent from node "{}" interface "{}" ' + 'with destination IPv4 address of node "{}" interface "{}" ' + 'a ping response arrives and TTL is decreased by "${}"'. + format(get_node_hostname(src_node), src_port, + get_node_hostname(dst_node), dst_port, hops)) + node = src_node + src_mac = Topology.get_interface_mac(src_node, src_port) + if dst_node['type'] == NodeType.TG: + dst_mac = Topology.get_interface_mac(src_node, src_port) + adj_int = Topology.get_adjacent_interface(src_node, src_port) + first_hop_mac = adj_int['mac_address'] + src_ip = IPv4Util.get_ip_addr(src_node, src_port) + dst_ip = IPv4Util.get_ip_addr(dst_node, dst_port) + args = '--src_if "{}" --src_mac "{}" --first_hop_mac "{}" ' \ + '--src_ip "{}" --dst_ip "{}" --hops "{}"'\ + .format(src_port, src_mac, first_hop_mac, src_ip, dst_ip, hops) + if dst_node['type'] == NodeType.TG: + args += ' --dst_if "{}" --dst_mac "{}"'.format(dst_port, dst_mac) + TrafficScriptExecutor.run_traffic_script_on_node( + "ipv4_ping_ttl_check.py", node, args) + + @staticmethod + @keyword('Get IPv4 address of node "${node}" interface "${port}"') + def get_ip_addr(node, port): + """Get IPv4 address configured on specified interface + :param node: node dictionary + :param port: interface name + :return: IPv4 address of specified interface as a 'str' type + """ + log.debug('Get IPv4 address of node {} interface {}'. + format(get_node_hostname(node), port)) + return IPv4Util.ADDRESSES[(get_node_hostname(node), port)] + + @staticmethod + @keyword('Get IPv4 address prefix of node "${node}" interface "${port}"') + def get_ip_addr_prefix(node, port): + """ Get IPv4 address prefix for specified interface. + :param node: Node dictionary. + :param port: Interface name. + """ + log.debug('Get IPv4 address prefix of node {} interface {}'. + format(get_node_hostname(node), port)) + return IPv4Util.PREFIXES[(get_node_hostname(node), port)] + + @staticmethod + @keyword('Get IPv4 subnet of node "${node}" interface "${port}"') + def get_ip_addr_subnet(node, port): + """ Get IPv4 subnet of specified interface. + :param node: Node dictionary. + :param port: Interface name. + """ + log.debug('Get IPv4 subnet of node {} interface {}'. + format(get_node_hostname(node), port)) + return IPv4Util.SUBNETS[(get_node_hostname(node), port)] + + @staticmethod + @keyword('Flush IPv4 addresses "${port}" "${node}"') + def flush_ip_addresses(port, node): + """See IPv4Node.flush_ip_addresses for more information. + :param port: + :param node: + :return: + """ + key = (get_node_hostname(node), port) + del IPv4Util.ADDRESSES[key] + del IPv4Util.PREFIXES[key] + del IPv4Util.SUBNETS[key] + get_node(node).flush_ip_addresses(port) diff --git a/resources/libraries/python/IPv6NodesAddr.py b/resources/libraries/python/IPv6NodesAddr.py new file mode 100644 index 0000000000..33192b878f --- /dev/null +++ b/resources/libraries/python/IPv6NodesAddr.py @@ -0,0 +1,67 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Robot framework variable file. + + Create dictionary variable nodes_ipv6_addr with IPv6 adresses from available + networks. +""" + +from IPv6Setup import IPv6Networks +from topology import Topology + +# Default list of available IPv6 networks +IPV6_NETWORKS = ['db01::/64', 'db02::/64', 'db03::/64'] + + +def get_variables(nodes, networks=IPV6_NETWORKS): + """Special robot framework method that returns dictionary nodes_ipv6_addr, + mapping of node and interface name to IPv6 adddress. + + :param nodes: Nodes of the test topology. + :param networks: list of available IPv6 networks + :type nodes: dict + :type networks: list + + .. note:: + Robot framework calls it automatically. + """ + topo = Topology() + links = topo.get_links(nodes) + + if len(links) > len(networks): + raise Exception('Not enough available IPv6 networks for topology.') + + ip6_n = IPv6Networks(networks) + + nets = {} + + for link in links: + ip6_net = ip6_n.next_network() + net_hosts = ip6_net.hosts() + port_idx = 0 + ports = {} + for node in nodes.values(): + if_name = topo.get_interface_by_link_name(node, link) + if if_name is not None: + port = {'addr': str(next(net_hosts)), + 'node': node['host'], + 'if': if_name} + port_idx += 1 + port_id = 'port{0}'.format(port_idx) + ports.update({port_id: port}) + nets.update({link: {'net_addr': str(ip6_net.network_address), + 'prefix': ip6_net.prefixlen, + 'ports': ports}}) + + return {'DICT__nodes_ipv6_addr': nets} diff --git a/resources/libraries/python/IPv6Setup.py b/resources/libraries/python/IPv6Setup.py new file mode 100644 index 0000000000..45a8eba58d --- /dev/null +++ b/resources/libraries/python/IPv6Setup.py @@ -0,0 +1,289 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Library to set up IPv6 in topology.""" + +from ssh import SSH +from ipaddress import IPv6Network +from topology import NodeType +from topology import Topology +from constants import Constants + + +class IPv6Networks(object): + """IPv6 network iterator. + + :param networks: List of the available IPv6 networks. + :type networks: list + """ + def __init__(self, networks): + self._networks = list() + for network in networks: + net = IPv6Network(unicode(network)) + self._networks.append(net) + num = len(self._networks) + if num == 0: + raise Exception('No IPv6 networks') + + def next_network(self): + """Get the next elemnt of the iterator. + + :return: IPv6 network. + :rtype: IPv6Network object + :raises: StopIteration if there is no more elements. + """ + if len(self._networks): + return self._networks.pop() + else: + raise StopIteration() + + +class IPv6Setup(object): + """IPv6 setup in topology.""" + + def __init__(self): + pass + + def nodes_setup_ipv6_addresses(self, nodes, nodes_addr): + """Setup IPv6 addresses on all VPP nodes in topology. + + :param nodes: Nodes of the test topology. + :param nodes_addr: Available nodes IPv6 adresses. + :type nodes: dict + :type nodes_addr: dict + """ + for net in nodes_addr.values(): + for port in net['ports'].values(): + host = port.get('node') + if host is None: + continue + topo = Topology() + node = topo.get_node_by_hostname(nodes, host) + if node is None: + continue + if node['type'] == NodeType.DUT: + self.vpp_set_if_ipv6_addr(node, port['if'], port['addr'], + net['prefix']) + + def nodes_clear_ipv6_addresses(self, nodes, nodes_addr): + """Remove IPv6 addresses from all VPP nodes in topology. + + :param nodes: Nodes of the test topology. + :param nodes_addr: Available nodes IPv6 adresses. + :type nodes: dict + :type nodes_addr: dict + """ + for net in nodes_addr.values(): + for port in net['ports'].values(): + host = port.get('node') + if host is None: + continue + topo = Topology() + node = topo.get_node_by_hostname(nodes, host) + if node is None: + continue + if node['type'] == NodeType.DUT: + self.vpp_del_if_ipv6_addr(node, port['if'], port['addr'], + net['prefix']) + + @staticmethod + def linux_set_if_ipv6_addr(node, interface, addr, prefix): + """Set IPv6 address on linux host. + + :param node: Linux node. + :param interface: Node interface. + :param addr: IPv6 address. + :param prefix: IPv6 address prefix. + :type node: dict + :type interface: str + :type addr: str + :type prefix: str + """ + ssh = SSH() + ssh.connect(node) + + cmd = "ifconfig {dev} inet6 add {ip}/{p} up".format(dev=interface, + ip=addr, p=prefix) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception('TG ifconfig failed') + + @staticmethod + def linux_del_if_ipv6_addr(node, interface, addr, prefix): + """Delete IPv6 address on linux host. + + :param node: Linux node. + :param interface: Node interface. + :param addr: IPv6 address. + :param prefix: IPv6 address prefix. + :type node: dict + :type interface: str + :type addr: str + :type prefix: str + """ + ssh = SSH() + ssh.connect(node) + + cmd = "ifconfig {dev} inet6 del {ip}/{p}".format(dev=interface, + ip=addr, + p=prefix) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception('TG ifconfig failed') + + cmd = "ifconfig {dev} down".format(dev=interface) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception('TG ifconfig failed') + + @staticmethod + def vpp_set_if_ipv6_addr(node, interface, addr, prefix): + """Set IPv6 address on VPP. + + :param node: VPP node. + :param interface: Node interface. + :param addr: IPv6 address. + :param prefix: IPv6 address prefix. + :type node: dict + :type interface: str + :type addr: str + :type prefix: str + """ + ssh = SSH() + ssh.connect(node) + + cmd = '{c}'.format(c=Constants.VAT_BIN_NAME) + cmd_input = 'sw_interface_add_del_address {dev} {ip}/{p}'.format( + dev=interface, ip=addr, p=prefix) + (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input) + if int(ret_code) != 0: + raise Exception('VPP sw_interface_add_del_address failed on {h}' + .format(h=node['host'])) + + cmd_input = 'sw_interface_set_flags {dev} admin-up'.format( + dev=interface) + (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input) + if int(ret_code) != 0: + raise Exception('VPP sw_interface_set_flags failed on {h}'.format( + h=node['host'])) + + @staticmethod + def vpp_del_if_ipv6_addr(node, interface, addr, prefix): + """Delete IPv6 address on VPP. + + :param node: VPP node. + :param interface: Node interface. + :param addr: IPv6 address. + :param prefix: IPv6 address prefix. + :type node: dict + :type interface: str + :type addr: str + :type prefix: str + """ + ssh = SSH() + ssh.connect(node) + + cmd = '{c}'.format(c=Constants.VAT_BIN_NAME) + cmd_input = 'sw_interface_add_del_address {dev} {ip}/{p} del'.format( + dev=interface, ip=addr, p=prefix) + (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input) + if int(ret_code) != 0: + raise Exception( + 'sw_interface_add_del_address failed on {h}'. + format(h=node['host'])) + + cmd_input = 'sw_interface_set_flags {dev} admin-down'.format( + dev=interface) + (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input) + if int(ret_code) != 0: + raise Exception('VPP sw_interface_set_flags failed on {h}'.format( + h=node['host'])) + + @staticmethod + def vpp_ra_supress_link_layer(node, interface): + """Supress ICMPv6 router advertisement message for link scope address + + :param node: VPP node. + :param interface: Interface name. + :type node: dict + :type interface: str + """ + ssh = SSH() + ssh.connect(node) + + cmd = '{c}'.format(c=Constants.VAT_BIN_NAME) + cmd_input = 'exec ip6 nd {0} ra-surpress-link-layer'.format( + interface) + (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input) + if int(ret_code) != 0: + raise Exception("'{0}' failed on {1}".format(cmd_input, + node['host'])) + + def vpp_all_ra_supress_link_layer(self, nodes): + """Supress ICMPv6 router advertisement message for link scope address + on all VPP nodes in the topology + + :param nodes: Nodes of the test topology. + :type nodes: dict + """ + for node in nodes.values(): + if node['type'] == NodeType.TG: + continue + for port_k, port_v in node['interfaces'].items(): + if port_k == 'mgmt': + continue + if_name = port_v.get('name') + if if_name is None: + continue + self.vpp_ra_supress_link_layer(node, if_name) + + @staticmethod + def vpp_ipv6_route_add(node, link, interface, nodes_addr): + """Setup IPv6 route on the VPP node. + + :param node: Node to add route on. + :param link: Route to following link. + :param interface: Route output interface. + :param nodes_addr: Available nodes IPv6 adresses. + :type node: dict + :type link: str + :type interface: str + :type nodes_addr: dict + """ + ssh = SSH() + ssh.connect(node) + + # Get route destination address from link name + net = nodes_addr.get(link) + if net is None: + raise ValueError('No network for link "{0}"'.format(link)) + dst_net = '{0}/{1}'.format(net['net_addr'], net['prefix']) + + # Get next-hop address + nh_addr = None + for net in nodes_addr.values(): + for port in net['ports'].values(): + if port['if'] == interface and port['node'] == node['host']: + for nh in net['ports'].values(): + if nh['if'] != interface and nh['node'] != node['host']: + nh_addr = nh['addr'] + if nh_addr is None: + raise Exception('next-hop not found') + + cmd_input = 'ip_add_del_route {0} via {1} {2} resolve-attempts 10'. \ + format(dst_net, nh_addr, interface) + (ret_code, _, _) = ssh.exec_command_sudo(Constants.VAT_BIN_NAME, + cmd_input) + if int(ret_code) != 0: + raise Exception("'{0}' failed on {1}".format(cmd_input, + node['host'])) diff --git a/resources/libraries/python/IPv6Util.py b/resources/libraries/python/IPv6Util.py new file mode 100644 index 0000000000..a96683b164 --- /dev/null +++ b/resources/libraries/python/IPv6Util.py @@ -0,0 +1,101 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""IPv6 utilities library.""" + +import re +from ssh import SSH + + +class IPv6Util(object): + """IPv6 utilities""" + + def __init__(self): + pass + + @staticmethod + def ipv6_ping(src_node, dst_addr, count=3, data_size=56, timeout=1): + """IPv6 ping. + + Args: + src_node (Dict): Node where ping run. + dst_addr (str): Destination IPv6 address. + count (Optional[int]): Number of echo requests. + data_size (Optional[int]): Number of the data bytes. + timeout (Optional[int]): Time to wait for a response, in seconds. + + Returns: + Number of lost packets. + """ + ssh = SSH() + ssh.connect(src_node) + + cmd = "ping6 -c {c} -s {s} -W {W} {dst}".format(c=count, s=data_size, + W=timeout, + dst=dst_addr) + (ret_code, stdout, _) = ssh.exec_command(cmd) + + regex = re.compile(r'(\d+) packets transmitted, (\d+) received') + match = regex.search(stdout) + sent, received = match.groups() + packet_lost = int(sent) - int(received) + + return packet_lost + + @staticmethod + def ipv6_ping_port(nodes_ip, src_node, dst_node, port, cnt=3, + size=56, timeout=1): + """Send IPv6 ping to the node port. + + Args: + nodes_ip (Dict): Nodes IPv6 adresses. + src_node (Dict): Node where ping run. + dst_node (Dict): Destination node. + port (str): Port on the destination node. + cnt (Optional[int]): Number of echo requests. + size (Optional[int]): Number of the data bytes. + timeout (Optional[int]): Time to wait for a response, in seconds. + + Returns: + Number of lost packets. + """ + dst_ip = IPv6Util.get_node_port_ipv6_address(dst_node, port, nodes_ip) + return IPv6Util.ipv6_ping(src_node, dst_ip, cnt, size, timeout) + + @staticmethod + def get_node_port_ipv6_address(node, interface, nodes_addr): + """Return IPv6 address of the node port. + + Args: + node (Dict): Node in the topology. + interface (str): Interface name of the node. + nodes_addr (Dict): Nodes IPv6 adresses. + + Returns: + IPv6 address string. + """ + for net in nodes_addr.values(): + for port in net['ports'].values(): + host = port.get('node') + dev = port.get('if') + if host == node['host'] and dev == interface: + ip = port.get('addr') + if ip is not None: + return ip + else: + raise Exception( + 'Node {n} port {p} IPv6 address is not set'.format( + n=node['host'], p=interface)) + + raise Exception('Node {n} port {p} IPv6 address not found.'.format( + n=node['host'], p=interface)) diff --git a/resources/libraries/python/InterfaceSetup.py b/resources/libraries/python/InterfaceSetup.py new file mode 100644 index 0000000000..9b6043545a --- /dev/null +++ b/resources/libraries/python/InterfaceSetup.py @@ -0,0 +1,152 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Interface setup library.""" + +from ssh import SSH + + +class InterfaceSetup(object): + """Interface setup utilities.""" + + __UDEV_IF_RULES_FILE = '/etc/udev/rules.d/10-network.rules' + + def __init__(self): + pass + + @staticmethod + def tg_set_interface_driver(node, pci_addr, driver): + """Set interface driver on the TG node. + + :param node: Node to set interface driver on (must be TG node). + :param interface: Interface name. + :param driver: Driver name. + :type node: dict + :type interface: str + :type driver: str + """ + old_driver = InterfaceSetup.tg_get_interface_driver(node, pci_addr) + if old_driver == driver: + return + + ssh = SSH() + ssh.connect(node) + + # Unbind from current driver + if old_driver is not None: + cmd = 'sh -c "echo {0} > /sys/bus/pci/drivers/{1}/unbind"'.format( + pci_addr, old_driver) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception("'{0}' failed on '{1}'".format(cmd, + node['host'])) + + # Bind to the new driver + cmd = 'sh -c "echo {0} > /sys/bus/pci/drivers/{1}/bind"'.format( + pci_addr, driver) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception("'{0}' failed on '{1}'".format(cmd, node['host'])) + + @staticmethod + def tg_get_interface_driver(node, pci_addr): + """Get interface driver from the TG node. + + :param node: Node to get interface driver on (must be TG node). + :param interface: Interface name. + :type node: dict + :type interface: str + :return: Interface driver or None if not found. + :rtype: str + + .. note:: + # lspci -vmmks 0000:00:05.0 + Slot: 00:05.0 + Class: Ethernet controller + Vendor: Red Hat, Inc + Device: Virtio network device + SVendor: Red Hat, Inc + SDevice: Device 0001 + PhySlot: 5 + Driver: virtio-pci + """ + ssh = SSH() + ssh.connect(node) + + cmd = 'lspci -vmmks {0}'.format(pci_addr) + + (ret_code, stdout, _) = ssh.exec_command(cmd) + if int(ret_code) != 0: + raise Exception("'{0}' failed on '{1}'".format(cmd, node['host'])) + + for line in stdout.splitlines(): + if len(line) == 0: + continue + (name, value) = line.split("\t", 1) + if name == 'Driver:': + return value + + return None + + @staticmethod + def tg_set_interfaces_udev_rules(node): + """Set udev rules for interfaces. + + Create udev rules file in /etc/udev/rules.d where are rules for each + interface used by TG node, based on MAC interface has specific name. + So after unbind and bind again to kernel driver interface has same + name as before. This must be called after TG has set name for each + port in topology dictionary. + udev rule example + SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="52:54:00:e1:8a:0f", + NAME="eth1" + + :param node: Node to set udev rules on (must be TG node). + :type node: dict + """ + ssh = SSH() + ssh.connect(node) + + cmd = 'rm -f {0}'.format(InterfaceSetup.__UDEV_IF_RULES_FILE) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception("'{0}' failed on '{1}'".format(cmd, node['host'])) + + for if_k, if_v in node['interfaces'].items(): + if if_k == 'mgmt': + continue + rule = 'SUBSYSTEM==\\"net\\", ACTION==\\"add\\", ATTR{address}' + \ + '==\\"' + if_v['mac_address'] + '\\", NAME=\\"' + \ + if_v['name'] + '\\"' + cmd = 'sh -c "echo \'{0}\' >> {1}"'.format( + rule, InterfaceSetup.__UDEV_IF_RULES_FILE) + (ret_code, _, _) = ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + raise Exception("'{0}' failed on '{1}'".format(cmd, + node['host'])) + + cmd = '/etc/init.d/udev restart' + ssh.exec_command_sudo(cmd) + + @staticmethod + def tg_set_interfaces_default_driver(node): + """Set interfaces default driver specified in topology yaml file. + + :param node: Node to setup interfaces driver on (must be TG node). + :type node: dict + """ + for if_k, if_v in node['interfaces'].items(): + if if_k == 'mgmt': + continue + InterfaceSetup.tg_set_interface_driver(node, if_v['pci_address'], + if_v['driver']) diff --git a/resources/libraries/python/PacketVerifier.py b/resources/libraries/python/PacketVerifier.py new file mode 100644 index 0000000000..81798e1f68 --- /dev/null +++ b/resources/libraries/python/PacketVerifier.py @@ -0,0 +1,310 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""PacketVerifier module. + + :Example: + + >>> from scapy.all import * + >>> from PacketVerifier import * + >>> rxq = RxQueue('eth1') + >>> txq = TxQueue('eth1') + >>> src_mac = "AA:BB:CC:DD:EE:FF" + >>> dst_mac = "52:54:00:ca:5d:0b" + >>> src_ip = "11.11.11.10" + >>> dst_ip = "11.11.11.11" + >>> sent_packets = [] + >>> pkt_send = Ether(src=src_mac, dst=dst_mac) / + ... IP(src=src_ip, dst=dst_ip) / + ... ICMP() + >>> sent_packets.append(pkt_send) + >>> txq.send(pkt_send) + >>> pkt_send = Ether(src=src_mac, dst=dst_mac) / + ... ARP(hwsrc=src_mac, psrc=src_ip, hwdst=dst_mac, pdst=dst_ip, op=2) + >>> sent_packets.append(pkt_send) + >>> txq.send(pkt_send) + >>> rxq.recv(100, sent_packets).show() + ###[ Ethernet ]### + dst = aa:bb:cc:dd:ee:ff + src = 52:54:00:ca:5d:0b + type = 0x800 + ###[ IP ]### + version = 4L + ihl = 5L + tos = 0x0 + len = 28 + id = 43183 + flags = + frag = 0L + ttl = 64 + proto = icmp + chksum = 0xa607 + src = 11.11.11.11 + dst = 11.11.11.10 + \options \ + ###[ ICMP ]### + type = echo-reply + code = 0 + chksum = 0xffff + id = 0x0 + seq = 0x0 + ###[ Padding ]### + load = 'RT\x00\xca]\x0b\xaa\xbb\xcc\xdd\xee\xff\x08\x06\x00\x01\x08\x00' + >>> rxq._proc.terminate() +""" + + +import socket +import os +import time +from multiprocessing import Queue, Process +from scapy.all import ETH_P_IP, ETH_P_IPV6, ETH_P_ALL, ETH_P_ARP +from scapy.all import Ether, ARP, Packet +from scapy.layers.inet6 import IPv6 + +__all__ = ['RxQueue', 'TxQueue', 'Interface', 'create_gratuitous_arp_request', + 'auto_pad'] + +# TODO: http://stackoverflow.com/questions/320232/ensuring-subprocesses-are-dead-on-exiting-python-program + +class PacketVerifier(object): + """Base class for TX and RX queue objects for packet verifier.""" + def __init__(self, interface_name): + os.system('sudo echo 1 > /proc/sys/net/ipv6/conf/{0}/disable_ipv6' + .format(interface_name)) + os.system('sudo ip link set {0} up promisc on'.format(interface_name)) + self._sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, + ETH_P_ALL) + self._sock.bind((interface_name, ETH_P_ALL)) + + +def extract_one_packet(buf): + """Extract one packet from the incoming buf buffer. + + Takes string as input and looks for first whole packet in it. + If it finds one, it returns substring from the buf parameter. + + :param buf: string representation of incoming packet buffer. + :type buf: string + :return: String representation of first packet in buf. + :rtype: string + """ + pkt_len = 0 + + if len(buf) < 60: + return None + + # print + # print buf.__repr__() + # print Ether(buf).__repr__() + # print len(Ether(buf)) + # print + try: + ether_type = Ether(buf[0:14]).type + except AttributeError: + raise RuntimeError( + 'No EtherType in packet {0}'.format(buf.__repr__())) + + if ether_type == ETH_P_IP: + # 14 is Ethernet fame header size. + # 4 bytes is just enough to look for length in ip header. + # ip total length contains just the IP packet length so add the Ether + # header. + pkt_len = Ether(buf[0:14+4]).len + 14 + if len(buf) < 60: + return None + elif ether_type == ETH_P_IPV6: + if not Ether(buf[0:14+6]).haslayer(IPv6): + raise RuntimeError( + 'Invalid IPv6 packet {0}'.format(buf.__repr__())) + # ... to add to the above, 40 bytes is the length of IPV6 header. + # The ipv6.len only contains length of the payload and not the header + pkt_len = Ether(buf)['IPv6'].plen + 14 + 40 + if len(buf) < 60: + return None + elif ether_type == ETH_P_ARP: + pkt = Ether(buf[:20]) + if not pkt.haslayer(ARP): + raise RuntimeError('Incomplete ARP packet') + # len(eth) + arp(2 hw addr type + 2 proto addr type + # + 1b len + 1b len + 2b operation) + + pkt_len = 14 + 8 + pkt_len += 2 * pkt.getlayer(ARP).hwlen + pkt_len += 2 * pkt.getlayer(ARP).plen + + del pkt + elif ether_type == 32821: # RARP (Reverse ARP) + pkt = Ether(buf[:20]) + pkt.type = ETH_P_ARP # Change to ARP so it works with scapy + pkt = Ether(str(pkt)) + if not pkt.haslayer(ARP): + pkt.show() + raise RuntimeError('Incomplete RARP packet') + + # len(eth) + arp(2 hw addr type + 2 proto addr type + # + 1b len + 1b len + 2b operation) + pkt_len = 14 + 8 + pkt_len += 2 * pkt.getlayer(ARP).hwlen + pkt_len += 2 * pkt.getlayer(ARP).plen + + del pkt + else: + raise RuntimeError('Unknown protocol {0}'.format(ether_type)) + + if pkt_len < 60: + pkt_len = 60 + + if len(buf) < pkt_len: + return None + + return buf[0:pkt_len] + + +def packet_reader(interface_name, queue): + """Sub-process routine that reads packets and puts them to queue. + + This function is meant to be run in separate subprocess and is in tight + loop reading raw packets from interface passed as parameter. + + :param interace_name: Name of interface to read packets from. + :param queue: Queue in which this function will push incoming packets. + :type interface_name: string + :type queue: multiprocessing.Queue + :return: None + """ + sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL) + sock.bind((interface_name, ETH_P_ALL)) + + buf = "" + while True: + recvd = sock.recv(1500) + buf = buf + recvd + + pkt = extract_one_packet(buf) + while pkt is not None: + if pkt is None: + break + queue.put(pkt) + buf = buf[len(pkt):] + pkt = extract_one_packet(buf) + + +class RxQueue(PacketVerifier): + """Receive queue object. + + This object creates raw socket, reads packets from it and provides + function to access them. + + :param interface_name: Which interface to bind to. + :type interface_name: string + """ + + def __init__(self, interface_name): + PacketVerifier.__init__(self, interface_name) + + self._queue = Queue() + self._proc = Process(target=packet_reader, args=(interface_name, + self._queue)) + self._proc.daemon = True + self._proc.start() + time.sleep(2) + + def recv(self, timeout=3, ignore=None): + """Read next received packet. + + Returns scapy's Ether() object created from next packet in the queue. + Queue is being filled in parallel in subprocess. If no packet + arrives in given timeout queue.Empty exception will be risen. + + :param timeout: How many seconds to wait for next packet. + :type timeout: int + + :return: Ether() initialized object from packet data. + :rtype: scapy.Ether + """ + + pkt = self._queue.get(True, timeout=timeout) + + if ignore is not None: + for i, ig_pkt in enumerate(ignore): + # Auto pad all packets in ignore list + ignore[i] = auto_pad(ig_pkt) + for ig_pkt in ignore: + if ig_pkt == pkt: + # Found the packet in ignore list, get another one + # TODO: subtract timeout - time_spent in here + ignore.remove(ig_pkt) + return self.recv(timeout, ignore) + + return Ether(pkt) + + +class TxQueue(PacketVerifier): + """Transmission queue object. + + This object is used to send packets over RAW socket on a interface. + + :param interface_name: Which interface to send packets from. + :type interface_name: string + """ + def __init__(self, interface_name): + PacketVerifier.__init__(self, interface_name) + + def send(self, pkt): + """Send packet out of the bound interface. + + :param pkt: Packet to send. + :type pkt: string or scapy Packet derivative. + """ + if isinstance(pkt, Packet): + pkt = str(pkt) + pkt = auto_pad(pkt) + self._sock.send(pkt) + + +class Interface(object): + def __init__(self, if_name): + self.if_name = if_name + self.sent_packets = [] + self.txq = TxQueue(if_name) + self.rxq = RxQueue(if_name) + + def send_pkt(self, pkt): + self.sent_packets.append(pkt) + self.txq.send(pkt) + + def recv_pkt(self, timeout=3): + while True: + pkt = self.rxq.recv(timeout, self.sent_packets) + # TODO: FIX FOLLOWING: DO NOT SKIP RARP IN ALL TESTS!!! + if pkt.type != 32821: # Skip RARP packets + return pkt + + def close(self): + self.rxq._proc.terminate() + + +def create_gratuitous_arp_request(src_mac, src_ip): + """Creates scapy representation of gratuitous ARP request""" + return (Ether(src=src_mac, dst='ff:ff:ff:ff:ff:ff') / + ARP(psrc=src_ip, hwsrc=src_mac, pdst=src_ip)) + + +def auto_pad(packet): + """Pads zeroes at the end of the packet if the total len < 60 bytes.""" + padded = str(packet) + if len(padded) < 60: + padded += ('\0' * (60 - len(padded))) + return padded + diff --git a/resources/libraries/python/SetupFramework.py b/resources/libraries/python/SetupFramework.py new file mode 100644 index 0000000000..47c609fada --- /dev/null +++ b/resources/libraries/python/SetupFramework.py @@ -0,0 +1,137 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import shlex +from subprocess import Popen, PIPE, call +from multiprocessing import Pool +from tempfile import NamedTemporaryFile +from os.path import basename +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn +from ssh import SSH +from constants import Constants as con +from topology import NodeType + +__all__ = ["SetupFramework"] + + +def pack_framework_dir(): + """Pack the testing WS into temp file, return its name.""" + + tmpfile = NamedTemporaryFile(suffix=".tgz", prefix="openvpp-testing-") + file_name = tmpfile.name + tmpfile.close() + + proc = Popen( + shlex.split("tar --exclude-vcs -zcf {0} .".format(file_name)), + stdout=PIPE, stderr=PIPE) + (stdout, stderr) = proc.communicate() + + logger.debug(stdout) + logger.debug(stderr) + + return_code = proc.wait() + if 0 != return_code: + raise Exception("Could not pack testing framework.") + + return file_name + + +def copy_tarball_to_node(tarball, node): + logger.console('Copying tarball to {0}'.format(node['host'])) + ssh = SSH() + ssh.connect(node) + + ssh.scp(tarball, "/tmp/") + + +def extract_tarball_at_node(tarball, node): + logger.console('Extracting tarball to {0} on {1}'.format( + con.REMOTE_FW_DIR, node['host'])) + ssh = SSH() + ssh.connect(node) + + cmd = 'sudo rm -rf {1}; mkdir {1} ; tar -zxf {0} -C {1}; ' \ + 'rm -f {0}'.format(tarball, con.REMOTE_FW_DIR) + (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=30) + if 0 != ret_code: + logger.error('Unpack error: {0}'.format(stderr)) + raise Exception('Failed to unpack {0} at node {1}'.format( + tarball, node['host'])) + + +def create_env_directory_at_node(node): + """Create fresh virtualenv to a directory, install pip requirements.""" + logger.console('Extracting virtualenv, installing requirements.txt ' + 'on {0}'.format(node['host'])) + ssh = SSH() + ssh.connect(node) + (ret_code, stdout, stderr) = ssh.exec_command( + 'cd {0} && rm -rf env && virtualenv env && ' + '. env/bin/activate && ' + 'pip install -r requirements.txt'.format(con.REMOTE_FW_DIR)) + if 0 != ret_code: + logger.error('Virtualenv creation error: {0}'.format(stdout + stderr)) + raise Exception('Virtualenv setup failed') + + +def setup_node(args): + tarball, remote_tarball, node = args + copy_tarball_to_node(tarball, node) + extract_tarball_at_node(remote_tarball, node) + if node['type'] == NodeType.TG: + create_env_directory_at_node(node) + + +def delete_local_tarball(tarball): + call(shlex.split('sh -c "rm {0} > /dev/null 2>&1"'.format(tarball))) + + +class SetupFramework(object): + """Setup suite run on topology nodes. + + Many VAT/CLI based tests need the scripts at remote hosts before executing + them. This class packs the whole testing directory and copies it over + to all nodes in topology under /tmp/ + """ + + def __init__(self): + pass + + def setup_framework(self, nodes): + """Pack the whole directory and extract in temp on each node.""" + + tarball = pack_framework_dir() + msg = 'Framework packed to {0}'.format(tarball) + logger.console(msg) + logger.trace(msg) + remote_tarball = "/tmp/{0}".format(basename(tarball)) + + # Turn off loggining since we use multiprocessing + log_level = BuiltIn().set_log_level('NONE') + params = ((tarball, remote_tarball, node) for node in nodes.values()) + pool = Pool(processes=len(nodes)) + result = pool.map_async(setup_node, params) + pool.close() + pool.join() + + logger.info( + 'Executed node setups in parallel, waiting for processes to end') + result.wait() + + logger.info('Results: {0}'.format(result.get())) + + # Turn on loggining + BuiltIn().set_log_level(log_level) + logger.trace('Test framework copied to all topology nodes') + delete_local_tarball(tarball) diff --git a/resources/libraries/python/TGSetup.py b/resources/libraries/python/TGSetup.py new file mode 100644 index 0000000000..3e372e9464 --- /dev/null +++ b/resources/libraries/python/TGSetup.py @@ -0,0 +1,32 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""TG Setup library.""" + +from topology import NodeType +from InterfaceSetup import InterfaceSetup + + +class TGSetup(object): + """TG setup before test.""" + + @staticmethod + def all_tgs_set_interface_default_driver(nodes): + """Setup interfaces default driver for all TGs in given topology. + + :param nodes: Nodes in topology. + :type nodes: dict + """ + for node in nodes.values(): + if node['type'] == NodeType.TG: + InterfaceSetup.tg_set_interfaces_default_driver(node) diff --git a/resources/libraries/python/TrafficGenerator.py b/resources/libraries/python/TrafficGenerator.py new file mode 100644 index 0000000000..d86917a181 --- /dev/null +++ b/resources/libraries/python/TrafficGenerator.py @@ -0,0 +1,57 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from ssh import SSH +from robot.api import logger + +__all__ = ['TrafficGenerator'] + +class TrafficGenerator(object): + + def __init__(self): + self._result = None + self._loss = None + self._sent = None + self._received = None + + + def send_traffic_on(self, node, tx_port, rx_port, duration, rate, + framesize): + ssh = SSH() + ssh.connect(node) + + (ret, stdout, stderr) = ssh.exec_command( + "sh -c 'cd MoonGen && sudo -S build/MoonGen " + "rfc2544/benchmarks/vpp-frameloss.lua --txport 0 --rxport 1 " + "--duration {0} --rate {1} --framesize {2}'".format( + duration, rate, framesize), + timeout=int(duration)+60) + + logger.trace(ret) + logger.trace(stdout) + logger.trace(stderr) + + for line in stdout.splitlines(): + pass + + self._result = line + logger.info('TrafficGen result: {0}'.format(self._result)) + + self._loss = self._result.split(', ')[3].split('=')[1] + + return self._result + + def no_traffic_loss_occured(self): + if self._loss is None: + raise Exception('The traffic generation has not been issued') + if self._loss != '0': + raise Exception('Traffic loss occured: {0}'.format(self._loss)) diff --git a/resources/libraries/python/TrafficScriptArg.py b/resources/libraries/python/TrafficScriptArg.py new file mode 100644 index 0000000000..ab76f29b8e --- /dev/null +++ b/resources/libraries/python/TrafficScriptArg.py @@ -0,0 +1,60 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Traffic scripts argument parser library.""" + +import argparse + + +class TrafficScriptArg(object): + """Traffic scripts argument parser. + + Parse arguments for traffic script. Default has two arguments '--tx_if' + and '--rx_if'. You can provide more arguments. All arguments have string + representation of the value. + + :param more_args: List of aditional arguments (optional). + :type more_args: list + + :Example: + + >>> from TrafficScriptArg import TrafficScriptArg + >>> args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_ip', 'dst_ip']) + """ + + def __init__(self, more_args=None): + parser = argparse.ArgumentParser() + parser.add_argument("--tx_if", help="interface that sends traffic") + parser.add_argument("--rx_if", help="interface that receives traffic") + + if more_args is not None: + for arg in more_args: + arg_name = '--{0}'.format(arg) + parser.add_argument(arg_name) + + self._parser = parser + self._args = vars(parser.parse_args()) + + def get_arg(self, arg_name): + """Get argument value. + + :param arg_name: Argument name. + :type arg_name: str + :return: Argument value. + :rtype: str + """ + arg_val = self._args.get(arg_name) + if arg_val is None: + raise Exception("Argument '{0}' not found".format(arg_name)) + + return arg_val diff --git a/resources/libraries/python/TrafficScriptExecutor.py b/resources/libraries/python/TrafficScriptExecutor.py new file mode 100644 index 0000000000..2e65a520d0 --- /dev/null +++ b/resources/libraries/python/TrafficScriptExecutor.py @@ -0,0 +1,91 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Traffic script executor library.""" + +from constants import Constants +from ssh import SSH +from robot.api import logger + +__all__ = ['TrafficScriptExecutor'] + + +class TrafficScriptExecutor(object): + """Traffic script executor utilities.""" + + @staticmethod + def _escape(string): + """Escape quotation mark and dollar mark for shell command. + + :param string: String to escape. + :type string: str + :return: Escaped string. + :rtype: str + """ + return string.replace('"', '\\"').replace("$", "\\$") + + @staticmethod + def run_traffic_script_on_node(script_file_name, node, script_args, + timeout=10): + """Run traffic script on the TG node. + + :param script_file_name: Traffic script name + :param node: Node to run traffic script on. + :param script_args: Traffic scripts arguments. + :param timeout: Timeout (optional). + :type script_file_name: str + :type node: dict + :type script_args: str + :type timeout: int + """ + logger.trace("{}".format(timeout)) + ssh = SSH() + ssh.connect(node) + cmd = ("cd {}; virtualenv env && " + + "export PYTHONPATH=${{PWD}}; " + + ". ${{PWD}}/env/bin/activate; " + + "resources/traffic_scripts/{} {}") \ + .format(Constants.REMOTE_FW_DIR, script_file_name, + script_args) + (ret_code, stdout, stderr) = ssh.exec_command_sudo( + 'sh -c "{}"'.format(TrafficScriptExecutor._escape(cmd)), + timeout=timeout) + logger.debug("stdout: {}".format(stdout)) + logger.debug("stderr: {}".format(stderr)) + logger.debug("ret_code: {}".format(ret_code)) + if ret_code != 0: + raise Exception("Traffic script execution failed") + + @staticmethod + def traffic_script_gen_arg(rx_if, tx_if, src_mac, dst_mac, src_ip, dst_ip): + """Generate traffic script basic arguments string. + + :param rx_if: Interface that sends traffic. + :param tx_if: Interface that receives traffic. + :param src_mac: Source MAC address. + :param dst_mac: Destination MAC address. + :param src_ip: Source IP address. + :param dst_ip: Destination IP address. + :type rx_if: str + :type tx_if: str + :type src_mac: str + :type dst_mac: str + :type src_ip: str + :type dst_ip: str + :return: Traffic script arguments string. + :rtype: str + """ + args = '--rx_if {0} --tx_if {1} --src_mac {2} --dst_mac {3} --src_ip' \ + ' {4} --dst_ip {5}'.format(rx_if, tx_if, src_mac, dst_mac, src_ip, + dst_ip) + return args diff --git a/resources/libraries/python/VatConfigGenerator.py b/resources/libraries/python/VatConfigGenerator.py new file mode 100644 index 0000000000..98be9d3448 --- /dev/null +++ b/resources/libraries/python/VatConfigGenerator.py @@ -0,0 +1,58 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Can be used to generate VAT scripts from VAT template files.""" + +from robot.api import logger + + +class VatConfigGenerator(object): + """Generates VAT configuration scripts from VAT script template files. + """ + def __init__(self): + pass + + @staticmethod + def generate_vat_config_file(template_file, env_var_dict, out_file): + """ Write VAT configuration script to out file. + + Generates VAT configuration script from template using + dictionary containing environment variables + :param template_file: file that contains the VAT script template + :param env_var_dict: python dictionary that maps test + environment variables + """ + + template_data = open(template_file).read() + logger.trace("Loaded template file: \n '{0}'".format(template_data)) + generated_config = template_data.format(**env_var_dict) + logger.trace("Generated script file: \n '{0}'".format(generated_config)) + with open(out_file, 'w') as work_file: + work_file.write(generated_config) + + @staticmethod + def generate_vat_config_string(template_file, env_var_dict): + """ Return wat config string generated from template. + + Generates VAT configuration script from template using + dictionary containing environment variables + :param template_file: file that contains the VAT script template + :param env_var_dict: python dictionary that maps test + environment variables + """ + + template_data = open(template_file).read() + logger.trace("Loaded template file: \n '{0}'".format(template_data)) + generated_config = template_data.format(**env_var_dict) + logger.trace("Generated script file: \n '{0}'".format(generated_config)) + return generated_config diff --git a/resources/libraries/python/VatExecutor.py b/resources/libraries/python/VatExecutor.py new file mode 100644 index 0000000000..5582a869b7 --- /dev/null +++ b/resources/libraries/python/VatExecutor.py @@ -0,0 +1,197 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from ssh import SSH +from robot.api import logger +from constants import Constants +import json + +__all__ = ['VatExecutor'] + + +def cleanup_vat_json_output(json_output): + """Return VAT json output cleaned from VAT clutter. + + Clean up VAT json output from clutter like vat# prompts and such + :param json_output: cluttered json output. + :return: cleaned up output json string + """ + + retval = json_output + clutter = ['vat#', 'dump_interface_table error: Misc'] + for garbage in clutter: + retval = retval.replace(garbage, '') + return retval + + +class VatExecutor(object): + + def __init__(self): + self._stdout = None + self._stderr = None + self._ret_code = None + + def execute_script(self, vat_name, node, timeout=10, json_out=True): + """Copy local_path script to node, execute it and return result. + + :param vat_name: name of the vat script file. Only the file name of + the script is required, the resources path is prepended + automatically. + :param node: node to execute the VAT script on. + :param timeout: seconds to allow the script to run. + :param json_out: require json output. + :return: (rc, stdout, stderr) tuple. + """ + + ssh = SSH() + ssh.connect(node) + + remote_file_path = '{0}/{1}/{2}'.format(Constants.REMOTE_FW_DIR, + Constants.RESOURCES_TPL_VAT, + vat_name) + # TODO this overwrites the output if the vat script has been used twice + remote_file_out = remote_file_path + ".out" + + cmd = "sudo -S {vat} {json} < {input}".format( + vat=Constants.VAT_BIN_NAME, + json="json" if json_out is True else "", + input=remote_file_path) + (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout) + self._ret_code = ret_code + self._stdout = stdout + self._stderr = stderr + + logger.trace("Command '{0}' returned {1}'".format(cmd, self._ret_code)) + logger.trace("stdout: '{0}'".format(self._stdout)) + logger.trace("stderr: '{0}'".format(self._stderr)) + + # TODO: download vpe_api_test output file + # self._delete_files(node, remote_file_path, remote_file_out) + + def execute_script_json_out(self, vat_name, node, timeout=10,): + self.execute_script(vat_name, node, timeout, json_out=True) + self._stdout = cleanup_vat_json_output(self._stdout) + + def _delete_files(self, node, *files): + ssh = SSH() + ssh.connect(node) + files = " ".join([str(x) for x in files]) + ssh.exec_command("rm {0}".format(files)) + + def script_should_have_failed(self): + if self._ret_code is None: + raise Exception("First execute the script!") + if self._ret_code == 0: + raise AssertionError( + "Script execution passed, but failure was expected") + + def script_should_have_passed(self): + if self._ret_code is None: + raise Exception("First execute the script!") + if self._ret_code != 0: + raise AssertionError( + "Script execution failed, but success was expected") + + def get_script_stdout(self): + return self._stdout + + def get_script_stderr(self): + return self._stderr + + @staticmethod + def cmd_from_template(node, vat_template_file, **vat_args): + """Execute VAT script on specified node. This method supports + script templates with parameters + :param node: node in topology on witch the scrtipt is executed + :param vat_template_file: template file of VAT script + :param vat_args: arguments to the template file + :return: list of json objects returned by VAT + """ + vat = VatTerminal(node) + ret = vat.vat_terminal_exec_cmd_from_template(vat_template_file, + **vat_args) + vat.vat_terminal_close() + return ret + + @staticmethod + def copy_config_to_remote(node, local_path, remote_path): + # TODO: will be removed once v4 is merged to master. + """Copies vat configuration file to node + + :param node: Remote node on which to copy the VAT configuration file + :param local_path: path of the VAT script on local device that launches + test cases. + :param remote_path: path on remote node where to copy the VAT + configuration script file + """ + ssh = SSH() + ssh.connect(node) + logger.trace("Removing old file {}".format(remote_path)) + ssh.exec_command_sudo("rm -f {}".format(remote_path)) + ssh.scp(local_path, remote_path) + + +class VatTerminal(object): + """VAT interactive terminal + + :param node: Node to open VAT terminal on. + """ + + __VAT_PROMPT = "vat# " + __LINUX_PROMPT = ":~$ " + + def __init__(self, node): + self._ssh = SSH() + self._ssh.connect(node) + self._tty = self._ssh.interactive_terminal_open() + self._ssh.interactive_terminal_exec_command( + self._tty, + 'sudo -S {vat} json'.format(vat=Constants.VAT_BIN_NAME), + self.__VAT_PROMPT) + + def vat_terminal_exec_cmd(self, cmd): + """Execute command on the opened VAT terminal. + + :param cmd: Command to be executed. + + :return: Command output in python representation of JSON format. + """ + logger.debug("Executing command in VAT terminal: {}".format(cmd)); + out = self._ssh.interactive_terminal_exec_command(self._tty, + cmd, + self.__VAT_PROMPT) + logger.debug("VAT output: {}".format(out)); + json_out = json.loads(out) + return json_out + + def vat_terminal_close(self): + """Close VAT terminal.""" + self._ssh.interactive_terminal_exec_command(self._tty, + 'quit', + self.__LINUX_PROMPT) + self._ssh.interactive_terminal_close(self._tty) + + def vat_terminal_exec_cmd_from_template(self, vat_template_file, **args): + """Execute VAT script from a file. + :param vat_template_file: template file name of a VAT script + :param args: dictionary of parameters for VAT script + :return: list of json objects returned by VAT + """ + file_path = '{}/{}'.format(Constants.RESOURCES_TPL_VAT, + vat_template_file) + with open(file_path, 'r') as template_file: + cmd_template = template_file.readlines() + ret = [] + for line_tmpl in cmd_template: + vat_cmd = line_tmpl.format(**args) + ret.append(self.vat_terminal_exec_cmd(vat_cmd)) + return ret diff --git a/resources/libraries/python/VppCounters.py b/resources/libraries/python/VppCounters.py new file mode 100644 index 0000000000..f34d7a76d1 --- /dev/null +++ b/resources/libraries/python/VppCounters.py @@ -0,0 +1,105 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP counters utilities library.""" + +import time +from topology import NodeType, Topology +from VatExecutor import VatExecutor, VatTerminal +from robot.api import logger + + +class VppCounters(object): + """VPP counters utilities.""" + + def __init__(self): + self._stats_table = None + + def vpp_nodes_clear_interface_counters(self, nodes): + """Clear interface counters on all VPP nodes in topology. + + :param nodes: Nodes in topology. + :type nodes: dict + """ + for node in nodes.values(): + if node['type'] == NodeType.DUT: + self.vpp_clear_interface_counters(node) + + @staticmethod + def vpp_clear_interface_counters(node): + """Clear interface counters on VPP node. + + :param node: Node to clear interface counters on. + :type node: dict + """ + vat = VatExecutor() + vat.execute_script('clear_interface.vat', node) + vat.script_should_have_passed() + + def vpp_dump_stats_table(self, node): + """Dump stats table on VPP node. + + :param node: Node to dump stats table on. + :type node: dict + :return: Stats table. + """ + vat = VatTerminal(node) + vat.vat_terminal_exec_cmd('want_stats enable') + for _ in range(0, 12): + stats_table = vat.vat_terminal_exec_cmd('dump_stats_table') + if_counters = stats_table['interface_counters'] + if len(if_counters) > 0: + self._stats_table = stats_table + vat.vat_terminal_close() + return stats_table + time.sleep(1) + + vat.vat_terminal_close() + return None + + def vpp_get_ipv4_interface_counter(self, node, interface): + return self.vpp_get_ipv46_interface_counter(node, interface, False) + + def vpp_get_ipv6_interface_counter(self, node, interface): + return self.vpp_get_ipv46_interface_counter(node, interface, True) + + def vpp_get_ipv46_interface_counter(self, node, interface, is_ipv6=True): + """Return interface IPv4/IPv6 counter + + :param node: Node to get interface IPv4/IPv6 counter on. + :param interface: Interface name. + :type node: dict + :type interface: str + :return: Interface IPv4/IPv6 counter. + :param is_ipv6: specify IP version + :type is_ipv6: bool + :rtype: int + """ + version = 'ip6' if is_ipv6 else 'ip4' + topo = Topology() + if_index = topo.get_interface_sw_index(node, interface) + if if_index is None: + logger.trace('{i} sw_index not found.'.format(i=interface)) + return 0 + + if_counters = self._stats_table.get('interface_counters') + if if_counters is None or len(if_counters) == 0: + logger.trace('No interface counters.') + return 0 + for counter in if_counters: + if counter['vnet_counter_type'] == version: + data = counter['data'] + return data[if_index] + logger.trace('{i} {v} counter not found.'.format(i=interface, + v=version)) + return 0 diff --git a/resources/libraries/python/__init__.py b/resources/libraries/python/__init__.py new file mode 100644 index 0000000000..16058f3941 --- /dev/null +++ b/resources/libraries/python/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +__init__ file for directory resources/libraries/python +""" diff --git a/resources/libraries/python/constants.py b/resources/libraries/python/constants.py new file mode 100644 index 0000000000..d7134cedcb --- /dev/null +++ b/resources/libraries/python/constants.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +class Constants(object): + #OpenVPP testing directory location at topology nodes + REMOTE_FW_DIR = '/tmp/openvpp-testing' + RESOURCES_LIB_SH = 'resources/libraries/bash' + RESOURCES_TPL_VAT = 'resources/templates/vat' + #OpenVPP VAT binary name + VAT_BIN_NAME = 'vpe_api_test' diff --git a/resources/libraries/python/parsers/JsonParser.py b/resources/libraries/python/parsers/JsonParser.py new file mode 100644 index 0000000000..1d177670ff --- /dev/null +++ b/resources/libraries/python/parsers/JsonParser.py @@ -0,0 +1,45 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Used to parse Json files or Json data strings to dictionaries""" + +import json + + +class JsonParser(object): + """Parses Json data string or files containing Json data strings""" + def __init__(self): + pass + + @staticmethod + def parse_data(json_data): + """Return list parsed from json data string. + + Translates json data into list of values/dictionaries/lists + :param json_data: data in json format + :return: json data parsed as python list + """ + parsed_data = json.loads(json_data) + return parsed_data + + def parse_file(self, json_file): + """Return list parsed from file containing json string. + + Translates json data found in file into list of + values/dictionaries/lists + :param json_file: file with json type data + :return: json data parsed as python list + """ + input_data = open(json_file).read() + parsed_data = self.parse_data(input_data) + return parsed_data diff --git a/resources/libraries/python/parsers/__init__.py b/resources/libraries/python/parsers/__init__.py new file mode 100644 index 0000000000..5a0e0e1c5e --- /dev/null +++ b/resources/libraries/python/parsers/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/resources/libraries/python/ssh.py b/resources/libraries/python/ssh.py new file mode 100644 index 0000000000..72e41c76a6 --- /dev/null +++ b/resources/libraries/python/ssh.py @@ -0,0 +1,235 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paramiko +from scp import SCPClient +from time import time +from robot.api import logger +from interruptingcow import timeout +from robot.utils.asserts import assert_equal, assert_not_equal + +__all__ = ["exec_cmd", "exec_cmd_no_error"] + +# TODO: load priv key + + +class SSH(object): + + __MAX_RECV_BUF = 10*1024*1024 + __existing_connections = {} + + def __init__(self): + self._ssh = paramiko.SSHClient() + self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self._hostname = None + + def _node_hash(self, node): + return hash(frozenset([node['host'], node['port']])) + + def connect(self, node): + """Connect to node prior to running exec_command or scp. + + If there already is a connection to the node, this method reuses it. + """ + self._hostname = node['host'] + node_hash = self._node_hash(node) + if node_hash in self.__existing_connections: + self._ssh = self.__existing_connections[node_hash] + else: + start = time() + self._ssh.connect(node['host'], username=node['username'], + password=node['password']) + self.__existing_connections[node_hash] = self._ssh + logger.trace('connect took {} seconds'.format(time() - start)) + + def exec_command(self, cmd, timeout=10): + """Execute SSH command on a new channel on the connected Node. + + Returns (return_code, stdout, stderr). + """ + logger.trace('exec_command on {0}: {1}'.format(self._hostname, cmd)) + start = time() + chan = self._ssh.get_transport().open_session() + if timeout is not None: + chan.settimeout(int(timeout)) + chan.exec_command(cmd) + end = time() + logger.trace('exec_command "{0}" on {1} took {2} seconds'.format( + cmd, self._hostname, end-start)) + + stdout = "" + while True: + buf = chan.recv(self.__MAX_RECV_BUF) + stdout += buf + if not buf: + break + + stderr = "" + while True: + buf = chan.recv_stderr(self.__MAX_RECV_BUF) + stderr += buf + if not buf: + break + + return_code = chan.recv_exit_status() + logger.trace('chan_recv/_stderr took {} seconds'.format(time()-end)) + + return (return_code, stdout, stderr) + + def exec_command_sudo(self, cmd, cmd_input=None, timeout=10): + """Execute SSH command with sudo on a new channel on the connected Node. + + :param cmd: Command to be executed. + :param cmd_input: Input redirected to the command. + :param timeout: Timeout. + :return: return_code, stdout, stderr + + :Example: + + >>> from ssh import SSH + >>> ssh = SSH() + >>> ssh.connect(node) + >>> #Execute command without input (sudo -S cmd) + >>> ssh.exex_command_sudo("ifconfig eth0 down") + >>> #Execute command with input (sudo -S cmd <<< "input") + >>> ssh.exex_command_sudo("vpe_api_test", "dump_interface_table") + """ + if cmd_input is None: + command = 'sudo -S {c}'.format(c=cmd) + else: + command = 'sudo -S {c} <<< "{i}"'.format(c=cmd, i=cmd_input) + return self.exec_command(command, timeout) + + def interactive_terminal_open(self, time_out=10): + """Open interactive terminal on a new channel on the connected Node. + + :param time_out: Timeout in seconds. + :return: SSH channel with opened terminal. + + .. warning:: Interruptingcow is used here, and it uses + signal(SIGALRM) to let the operating system interrupt program + execution. This has the following limitations: Python signal + handlers only apply to the main thread, so you cannot use this + from other threads. You must not use this in a program that + uses SIGALRM itself (this includes certain profilers) + """ + chan = self._ssh.get_transport().open_session() + chan.get_pty() + chan.invoke_shell() + chan.settimeout(int(time_out)) + + buf = '' + try: + with timeout(time_out, exception=RuntimeError): + while not buf.endswith(':~$ '): + if chan.recv_ready(): + buf = chan.recv(4096) + except RuntimeError: + raise Exception('Open interactive terminal timeout.') + return chan + + def interactive_terminal_exec_command(self, chan, cmd, prompt, + time_out=10): + """Execute command on interactive terminal. + + interactive_terminal_open() method has to be called first! + + :param chan: SSH channel with opened terminal. + :param cmd: Command to be executed. + :param prompt: Command prompt, sequence of characters used to + indicate readiness to accept commands. + :param time_out: Timeout in seconds. + :return: Command output. + + .. warning:: Interruptingcow is used here, and it uses + signal(SIGALRM) to let the operating system interrupt program + execution. This has the following limitations: Python signal + handlers only apply to the main thread, so you cannot use this + from other threads. You must not use this in a program that + uses SIGALRM itself (this includes certain profilers) + """ + chan.sendall('{c}\n'.format(c=cmd)) + buf = '' + try: + with timeout(time_out, exception=RuntimeError): + while not buf.endswith(prompt): + if chan.recv_ready(): + buf += chan.recv(4096) + except RuntimeError: + raise Exception("Exec '{c}' timeout.".format(c=cmd)) + tmp = buf.replace(cmd.replace('\n', ''), '') + return tmp.replace(prompt, '') + + def interactive_terminal_close(self, chan): + """Close interactive terminal SSH channel. + + :param: chan: SSH channel to be closed. + """ + chan.close() + + def scp(self, local_path, remote_path): + """Copy files from local_path to remote_path. + + connect() method has to be called first! + """ + logger.trace('SCP {0} to {1}:{2}'.format( + local_path, self._hostname, remote_path)) + # SCPCLient takes a paramiko transport as its only argument + scp = SCPClient(self._ssh.get_transport()) + start = time() + scp.put(local_path, remote_path) + scp.close() + end = time() + logger.trace('SCP took {0} seconds'.format(end-start)) + + +def exec_cmd(node, cmd, timeout=None, sudo=False): + """Convenience function to ssh/exec/return rc, out & err. + + Returns (rc, stdout, stderr). + """ + if node is None: + raise TypeError('Node parameter is None') + if cmd is None: + raise TypeError('Command parameter is None') + if len(cmd) == 0: + raise ValueError('Empty command parameter') + + ssh = SSH() + try: + ssh.connect(node) + except Exception, e: + logger.error("Failed to connect to node" + e) + return None + + try: + if not sudo: + (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=timeout) + else: + (ret_code, stdout, stderr) = ssh.exec_command_sudo(cmd, + timeout=timeout) + except Exception, e: + logger.error(e) + return None + + return (ret_code, stdout, stderr) + +def exec_cmd_no_error(node, cmd, timeout=None, sudo=False): + """Convenience function to ssh/exec/return out & err. + Verifies that return code is zero. + + Returns (stdout, stderr). + """ + (rc, stdout, stderr) = exec_cmd(node,cmd, timeout=timeout, sudo=sudo) + assert_equal(rc, 0, 'Command execution failed: "{}"\n{}'. + format(cmd, stderr)) + return (stdout, stderr) diff --git a/resources/libraries/python/topology.py b/resources/libraries/python/topology.py new file mode 100644 index 0000000000..522de37d13 --- /dev/null +++ b/resources/libraries/python/topology.py @@ -0,0 +1,539 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines nodes and topology structure.""" + +from resources.libraries.python.parsers.JsonParser import JsonParser +from resources.libraries.python.VatExecutor import VatExecutor +from resources.libraries.python.ssh import SSH +from resources.libraries.python.InterfaceSetup import InterfaceSetup +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn +from robot.api.deco import keyword +from yaml import load + +__all__ = ["DICT__nodes", 'Topology'] + + +def load_topo_from_yaml(): + """Loads topology from file defined in "${TOPOLOGY_PATH}" variable + + :return: nodes from loaded topology + """ + topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}") + + with open(topo_path) as work_file: + return load(work_file.read())['nodes'] + + +class NodeType(object): + """Defines node types used in topology dictionaries""" + # Device Under Test (this node has VPP running on it) + DUT = 'DUT' + # Traffic Generator (this node has traffic generator on it) + TG = 'TG' + +DICT__nodes = load_topo_from_yaml() + + +class Topology(object): + """Topology data manipulation and extraction methods + + Defines methods used for manipulation and extraction of data from + the used topology. + """ + + def __init__(self): + pass + + @staticmethod + def get_node_by_hostname(nodes, hostname): + """Get node from nodes of the topology by hostname. + + :param nodes: Nodes of the test topology. + :param hostname: Host name. + :type nodes: dict + :type hostname: str + :return: Node dictionary or None if not found. + """ + for node in nodes.values(): + if node['host'] == hostname: + return node + + return None + + @staticmethod + def get_links(nodes): + """Get list of links(networks) in the topology. + + :param nodes: Nodes of the test topology. + :type nodes: dict + :return: Links in the topology. + :rtype: list + """ + links = [] + + for node in nodes.values(): + for interface in node['interfaces'].values(): + link = interface.get('link') + if link is not None: + if link not in links: + links.append(link) + + return links + + @staticmethod + def _get_interface_by_key_value(node, key, value): + """ Return node interface name according to key and value + + :param node: :param node: the node dictionary + :param key: key by which to select the interface. + :param value: value that should be found using the key. + :return: + """ + + interfaces = node['interfaces'] + retval = None + for interface in interfaces.values(): + k_val = interface.get(key) + if k_val is not None: + if k_val == value: + retval = interface['name'] + break + return retval + + def get_interface_by_link_name(self, node, link_name): + """Return interface name of link on node. + + This method returns the interface name asociated with a given link + for a given node. + :param link_name: name of the link that a interface is connected to. + :param node: the node topology dictionary + :return: interface name of the interface connected to the given link + """ + + return self._get_interface_by_key_value(node, "link", link_name) + + def get_interfaces_by_link_names(self, node, link_names): + """Return dictionary of dicitonaries {"interfaceN", interface name}. + + This method returns the interface names asociated with given links + for a given node. + The resulting dictionary can be then used to with VatConfigGenerator + to generate a VAT script with proper interface names. + :param link_names: list of names of the link that a interface is + connected to. + :param node: the node topology directory + :return: dictionary of interface names that are connected to the given + links + """ + + retval = {} + interface_key_tpl = "interface{}" + interface_number = 1 + for link_name in link_names: + interface_name = self.get_interface_by_link_name(node, link_name) + interface_key = interface_key_tpl.format(str(interface_number)) + retval[interface_key] = interface_name + interface_number += 1 + return retval + + def get_interface_by_sw_index(self, node, sw_index): + """Return interface name of link on node. + + This method returns the interface name asociated with a software index + assigned to the interface by vpp for a given node. + :param sw_index: sw_index of the link that a interface is connected to. + :param node: the node topology dictionary + :return: interface name of the interface connected to the given link + """ + + return self._get_interface_by_key_value(node, "vpp_sw_index", sw_index) + + @staticmethod + def convert_mac_to_number_list(mac_address): + """Convert mac address string to list of decimal numbers. + + Converts a : separated mac address to decimal number list as used + in json interface dump. + :param mac_address: string mac address + :return: list representation of mac address + """ + + list_mac = [] + for num in mac_address.split(":"): + list_mac.append(int(num, 16)) + return list_mac + + def _extract_vpp_interface_by_mac(self, interfaces_list, mac_address): + """Returns interface dictionary from interface_list by mac address. + + Extracts interface dictionary from all of the interfaces in interfaces + list parsed from json according to mac_address of the interface + :param interfaces_list: dictionary of all interfaces parsed from json + :param mac_address: string mac address of interface we are looking for + :return: interface dictionary from json + """ + + interface_dict = {} + list_mac_address = self.convert_mac_to_number_list(mac_address) + logger.trace(list_mac_address.__str__()) + for interface in interfaces_list: + # TODO: create vat json integrity checking and move there + if "l2_address" not in interface: + raise KeyError( + "key l2_address not found in interface dict." + "Probably input list is not parsed from correct VAT " + "json output.") + if "l2_address_length" not in interface: + raise KeyError( + "key l2_address_length not found in interface " + "dict. Probably input list is not parsed from correct " + "VAT json output.") + mac_from_json = interface["l2_address"][:6] + if mac_from_json == list_mac_address: + if interface["l2_address_length"] != 6: + raise ValueError("l2_address_length value is not 6.") + interface_dict = interface + break + return interface_dict + + def vpp_interface_name_from_json_by_mac(self, json_data, mac_address): + """Return vpp interface name string from VAT interface dump json output + + Extracts the name given to an interface by VPP. + These interface names differ from what you would see if you + used the ipconfig or similar command. + Required json data can be obtained by calling : + VatExecutor.execute_script_json_out("dump_interfaces.vat", node) + :param json_data: string json data from sw_interface_dump VAT command + :param mac_address: string containing mac address of interface + whose vpp name we wish to discover. + :return: string vpp interface name + """ + + interfaces_list = JsonParser().parse_data(json_data) + # TODO: checking if json data is parsed correctly + interface_dict = self._extract_vpp_interface_by_mac(interfaces_list, + mac_address) + interface_name = interface_dict["interface_name"] + return interface_name + + def _update_node_interface_data_from_json(self, node, interface_dump_json): + """ Update node vpp data in node__DICT from json interface dump. + + This method updates vpp interface names and sw indexexs according to + interface mac addresses found in interface_dump_json + :param node: node dictionary + :param interface_dump_json: json output from dump_interface_list VAT + command + """ + + interface_list = JsonParser().parse_data(interface_dump_json) + for ifc in node['interfaces'].values(): + if 'link' not in ifc: + continue + if_mac = ifc['mac_address'] + interface_dict = self._extract_vpp_interface_by_mac(interface_list, + if_mac) + ifc['name'] = interface_dict["interface_name"] + ifc['vpp_sw_index'] = interface_dict["sw_if_index"] + + def update_vpp_interface_data_on_node(self, node): + """Update vpp generated interface data for a given node in DICT__nodes + + Updates interface names, software index numbers and any other details + generated specifically by vpp that are unknown before testcase run. + :param node: Node selected from DICT__nodes + """ + + vat_executor = VatExecutor() + vat_executor.execute_script_json_out("dump_interfaces.vat", node) + interface_dump_json = vat_executor.get_script_stdout() + self._update_node_interface_data_from_json(node, + interface_dump_json) + + @staticmethod + def update_tg_interface_data_on_node(node): + """Update interface name for TG/linux node in DICT__nodes + + :param node: Node selected from DICT__nodes. + :type node: dict + + .. note:: + # for dev in `ls /sys/class/net/`; + > do echo "\"`cat /sys/class/net/$dev/address`\": \"$dev\""; done + "52:54:00:9f:82:63": "eth0" + "52:54:00:77:ae:a9": "eth1" + "52:54:00:e1:8a:0f": "eth2" + "00:00:00:00:00:00": "lo" + + .. todo:: parse lshw -json instead + """ + # First setup interface driver specified in yaml file + InterfaceSetup.tg_set_interfaces_default_driver(node) + + # Get interface names + ssh = SSH() + ssh.connect(node) + + cmd = 'for dev in `ls /sys/class/net/`; do echo "\\"`cat ' \ + '/sys/class/net/$dev/address`\\": \\"$dev\\""; done;' + + (ret_code, stdout, _) = ssh.exec_command(cmd) + if int(ret_code) != 0: + raise Exception('Get interface name and MAC failed') + tmp = "{" + stdout.rstrip().replace('\n', ',') + "}" + interfaces = JsonParser().parse_data(tmp) + for if_k, if_v in node['interfaces'].items(): + if if_k == 'mgmt': + continue + name = interfaces.get(if_v['mac_address']) + if name is None: + continue + if_v['name'] = name + + # Set udev rules for interfaces + InterfaceSetup.tg_set_interfaces_udev_rules(node) + + def update_all_interface_data_on_all_nodes(self, nodes): + """ Update interface names on all nodes in DICT__nodes + + :param nodes: Nodes in the topology. + :type nodes: dict + + This method updates the topology dictionary by querying interface lists + of all nodes mentioned in the topology dictionary. + It does this by dumping interface list to json output from all devices + using vpe_api_test, and pairing known information from topology + (mac address/pci address of interface) to state from VPP. + For TG/linux nodes add interface name only. + """ + + for node_data in nodes.values(): + if node_data['type'] == NodeType.DUT: + self.update_vpp_interface_data_on_node(node_data) + elif node_data['type'] == NodeType.TG: + self.update_tg_interface_data_on_node(node_data) + + @staticmethod + def get_interface_sw_index(node, interface): + """Get VPP sw_index for the interface. + + :param node: Node to get interface sw_index on. + :param interface: Interface name. + :type node: dict + :type interface: str + :return: Return sw_index or None if not found. + """ + for port in node['interfaces'].values(): + port_name = port.get('name') + if port_name is None: + continue + if port_name == interface: + return port.get('vpp_sw_index') + + return None + + @staticmethod + def get_interface_mac(node, interface): + """Get MAC address for the interface. + + :param node: Node to get interface sw_index on. + :param interface: Interface name. + :type node: dict + :type interface: str + :return: Return MAC or None if not found. + """ + for port in node['interfaces'].values(): + port_name = port.get('name') + if port_name is None: + continue + if port_name == interface: + return port.get('mac_address') + + return None + + @staticmethod + def get_adjacent_interface(node, interface_name): + """Get interface adjacent to specified interface on local network. + + :param node: Node that contains specified interface. + :param interface_name: Interface name. + :type node: dict + :type interface_name: str + :return: Return interface or None if not found. + :rtype: dict + """ + link_name = None + # get link name where the interface belongs to + for _, port_data in node['interfaces'].iteritems(): + if port_data['name'] == interface_name: + link_name = port_data['link'] + break + + if link_name is None: + return None + + # find link + for _, node_data in DICT__nodes.iteritems(): + # skip self + if node_data['host'] == node['host']: + continue + for interface, interface_data \ + in node_data['interfaces'].iteritems(): + if 'link' not in interface_data: + continue + if interface_data['link'] == link_name: + return node_data['interfaces'][interface] + + @staticmethod + def get_interface_pci_addr(node, interface): + """Get interface PCI address. + + :param node: Node to get interface PCI address on. + :param interface: Interface name. + :type node: dict + :type interface: str + :return: Return PCI address or None if not found. + """ + for port in node['interfaces'].values(): + if interface == port.get('name'): + return port.get('pci_address') + return None + + @staticmethod + def get_interface_driver(node, interface): + """Get interface driver. + + :param node: Node to get interface driver on. + :param interface: Interface name. + :type node: dict + :type interface: str + :return: Return interface driver or None if not found. + """ + for port in node['interfaces'].values(): + if interface == port.get('name'): + return port.get('driver') + return None + + @staticmethod + def get_node_link_mac(node, link_name): + """Return interface mac address by link name + + :param node: Node to get interface sw_index on + :param link_name: link name + :type node: dict + :type link_name: string + :return: mac address string + """ + for port in node['interfaces'].values(): + if port.get('link') == link_name: + return port.get('mac_address') + return None + + @staticmethod + def _get_node_active_link_names(node): + """Returns list of link names that are other than mgmt links + + :param node: node topology dictionary + :return: list of strings that represent link names occupied by the node + """ + interfaces = node['interfaces'] + link_names = [] + for interface in interfaces.values(): + if 'link' in interface: + link_names.append(interface['link']) + if len(link_names) == 0: + link_names = None + return link_names + + @keyword('Get active links connecting "${node1}" and "${node2}"') + def get_active_connecting_links(self, node1, node2): + """Returns list of link names that connect together node1 and node2 + + :param node1: node topology dictionary + :param node2: node topology dictionary + :return: list of strings that represent connecting link names + """ + + logger.trace("node1: {}".format(str(node1))) + logger.trace("node2: {}".format(str(node2))) + node1_links = self._get_node_active_link_names(node1) + node2_links = self._get_node_active_link_names(node2) + connecting_links = list(set(node1_links).intersection(node2_links)) + + return connecting_links + + @keyword('Get first active connecting link between node "${node1}" and ' + '"${node2}"') + def get_first_active_connecting_link(self, node1, node2): + """ + + :param node1: Connected node + :type node1: dict + :param node2: Connected node + :type node2: dict + :return: name of link connecting the two nodes together + :raises: RuntimeError + """ + + connecting_links = self.get_active_connecting_links(node1, node2) + if len(connecting_links) == 0: + raise RuntimeError("No links connecting the nodes were found") + else: + return connecting_links[0] + + @keyword('Get egress interfaces on "${node1}" for link with "${node2}"') + def get_egress_interfaces_for_nodes(self, node1, node2): + """Get egress interfaces on node1 for link with node2. + + :param node1: First node, node to get egress interface on. + :param node2: Second node. + :type node1: dict + :type node2: dict + :return: Engress interfaces. + :rtype: list + """ + interfaces = [] + links = self.get_active_connecting_links(node1, node2) + if len(links) == 0: + raise RuntimeError('No link between nodes') + for interface in node1['interfaces'].values(): + link = interface.get('link') + if link is None: + continue + if link in links: + continue + name = interface.get('name') + if name is None: + continue + interfaces.append(name) + return interfaces + + @keyword('Get first egress interface on "${node1}" for link with ' + '"${node2}"') + def get_first_egress_interface_for_nodes(self, node1, node2): + """Get first egress interface on node1 for link with node2. + + :param node1: First node, node to get egress interface on. + :param node2: Second node. + :type node1: dict + :type node2: dict + :return: Engress interface. + :rtype: str + """ + interfaces = self.get_egress_interfaces_for_nodes(node1, node2) + if not interfaces: + raise RuntimeError('No engress interface for nodes') + return interfaces[0] |