aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python
diff options
context:
space:
mode:
authorStefan Kobza <skobza@cisco.com>2016-01-11 18:03:25 +0100
committerStefan Kobza <skobza@cisco.com>2016-02-08 22:38:32 +0100
commit33499c81c94c2d3baef9d3e9f061cd76ef86fa74 (patch)
tree2d000ba17b821339a05e0c039f71e48e09553de9 /resources/libraries/python
parent5cbeca02602061d32212e14f289d65cf648920e4 (diff)
New version of RF tests.
Change-Id: I241a2b7a7706e65f71cfd4a62e2a40f053fc5d07 Signed-off-by: Stefan Kobza <skobza@cisco.com>
Diffstat (limited to 'resources/libraries/python')
-rw-r--r--resources/libraries/python/DUTSetup.py41
-rw-r--r--resources/libraries/python/IPUtil.py43
-rw-r--r--resources/libraries/python/IPv4NodeAddress.py104
-rw-r--r--resources/libraries/python/IPv4Util.py499
-rw-r--r--resources/libraries/python/IPv6NodesAddr.py67
-rw-r--r--resources/libraries/python/IPv6Setup.py289
-rw-r--r--resources/libraries/python/IPv6Util.py101
-rw-r--r--resources/libraries/python/InterfaceSetup.py152
-rw-r--r--resources/libraries/python/PacketVerifier.py310
-rw-r--r--resources/libraries/python/SetupFramework.py137
-rw-r--r--resources/libraries/python/TGSetup.py32
-rw-r--r--resources/libraries/python/TrafficGenerator.py57
-rw-r--r--resources/libraries/python/TrafficScriptArg.py60
-rw-r--r--resources/libraries/python/TrafficScriptExecutor.py91
-rw-r--r--resources/libraries/python/VatConfigGenerator.py58
-rw-r--r--resources/libraries/python/VatExecutor.py197
-rw-r--r--resources/libraries/python/VppCounters.py105
-rw-r--r--resources/libraries/python/__init__.py16
-rw-r--r--resources/libraries/python/constants.py19
-rw-r--r--resources/libraries/python/parsers/JsonParser.py45
-rw-r--r--resources/libraries/python/parsers/__init__.py12
-rw-r--r--resources/libraries/python/ssh.py235
-rw-r--r--resources/libraries/python/topology.py539
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]