Initial set of Fabric switch test cases

Change-Id: I86fd2b67d3b773aa496f5ef61f1e1fdf51fd9925
diff --git a/Fabric/Utilities/src/python/oftest/dataplane.py b/Fabric/Utilities/src/python/oftest/dataplane.py
new file mode 100644
index 0000000..df79d47
--- /dev/null
+++ b/Fabric/Utilities/src/python/oftest/dataplane.py
@@ -0,0 +1,394 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
+
+
+"""
+OpenFlow Test Framework
+
+DataPlane and DataPlanePort classes
+
+Provide the interface to the control the set of ports being used
+to stimulate the switch under test.
+
+See the class dataplaneport for more details.  This class wraps
+a set of those objects allowing general calls and parsing
+configuration.
+
+@todo Add "filters" for matching packets.  Actions supported
+for filters should include a callback or a counter
+"""
+
+import sys
+import os
+import socket
+import time
+import select
+import logging
+from threading import Thread
+from threading import Lock
+from threading import Condition
+import ofutils
+import netutils
+from pcap_writer import PcapWriter
+
+if "linux" in sys.platform:
+    import afpacket
+else:
+    import pcap
+
+def match_exp_pkt(self, exp_pkt, pkt):
+    """
+    Compare the string value of pkt with the string value of exp_pkt,
+    and return True iff they are identical.  If the length of exp_pkt is
+    less than the minimum Ethernet frame size (60 bytes), then padding
+    bytes in pkt are ignored.
+    """
+    e = str(exp_pkt)
+    p = str(pkt)
+    if len(e) < 60:
+        p = p[:len(e)]
+
+    #return e == p
+    #some nic card have capature problem, will have more bytes capatured.
+    if pkt.find(exp_pkt) >=0:
+        return True
+    else:
+        if self.config["dump_packet"]:
+            self.logger.debug("rx pkt    ->"+(" ".join("{:02x}".format(ord(c)) for c in pkt)))
+            self.logger.debug("expect pkt->"+(" ".join("{:02x}".format(ord(c)) for c in exp_pkt)))
+
+        return False
+
+
+class DataPlanePortLinux:
+    """
+    Uses raw sockets to capture and send packets on a network interface.
+    """
+
+    RCV_SIZE_DEFAULT = 4096
+    ETH_P_ALL = 0x03
+    RCV_TIMEOUT = 10000
+
+    def __init__(self, interface_name, port_number):
+        """
+        @param interface_name The name of the physical interface like eth1
+        """
+        self.interface_name = interface_name
+        self.socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, 0)
+        afpacket.enable_auxdata(self.socket)
+        self.socket.bind((interface_name, self.ETH_P_ALL))
+        netutils.set_promisc(self.socket, interface_name)
+        self.socket.settimeout(self.RCV_TIMEOUT)
+
+    def __del__(self):
+        if self.socket:
+            self.socket.close()
+
+    def fileno(self):
+        """
+        Return an integer file descriptor that can be passed to select(2).
+        """
+        return self.socket.fileno()
+
+    def recv(self):
+        """
+        Receive a packet from this port.
+        @retval (packet data, timestamp)
+        """
+        pkt = afpacket.recv(self.socket, self.RCV_SIZE_DEFAULT)
+        return (pkt, time.time())
+
+    def send(self, packet):
+        """
+        Send a packet out this port.
+        @param packet The packet data to send to the port
+        @retval The number of bytes sent
+        """
+        return self.socket.send(packet)
+
+    def down(self):
+        """
+        Bring the physical link down.
+        """
+        #os.system("ifconfig down %s" % self.interface_name)
+        os.system("ifconfig %s down" % self.interface_name)
+
+    def up(self):
+        """
+        Bring the physical link up.
+        """
+        #os.system("ifconfig up %s" % self.interface_name)
+        os.system("ifconfig %s up" % self.interface_name)
+
+
+class DataPlanePortPcap:
+    """
+    Alternate port implementation using libpcap. This is used by non-Linux
+    operating systems.
+    """
+
+    def __init__(self, interface_name, port_number):
+        self.pcap = pcap.pcap(interface_name)
+        self.pcap.setnonblock()
+
+    def fileno(self):
+        return self.pcap.fileno()
+
+    def recv(self):
+        (timestamp, pkt) = next(self.pcap)
+        return (pkt[:], timestamp)
+
+    def send(self, packet):
+        if hasattr(self.pcap, "inject"):
+            return self.pcap.inject(packet, len(packet))
+        else:
+           return self.pcap.sendpacket(packet)
+
+    def down(self):
+        pass
+
+    def up(self):
+        pass
+
+class DataPlane(Thread):
+    """
+    This class provides methods to send and receive packets on the dataplane.
+    It uses the DataPlanePort class, or an alternative implementation of that
+    interface, to do IO on a particular port. A background thread is used to
+    read packets from the dataplane ports and enqueue them to be read by the
+    test. The kill() method must be called to shutdown this thread.
+    """
+
+    MAX_QUEUE_LEN = 100
+
+    def __init__(self, config=None):
+        Thread.__init__(self)
+
+        # dict from port number to port object
+        self.ports = {}
+
+        # dict from port number to list of (timestamp, packet)
+        self.packet_queues = {}
+
+        # cvar serves double duty as a regular top level lock and
+        # as a condition variable
+        self.cvar = Condition()
+
+        # Used to wake up the event loop from another thread
+        self.waker = ofutils.EventDescriptor()
+        self.killed = False
+
+        self.logger = logging.getLogger("dataplane")
+        self.pcap_writer = None
+
+        if config is None:
+            self.config = {}
+        else:
+            self.config = config;
+
+        ############################################################
+        #
+        # The platform/config can provide a custom DataPlanePort class
+        # here if you have a custom implementation with different
+        # behavior.
+        #
+        # Set config.dataplane.portclass = MyDataPlanePortClass
+        # where MyDataPlanePortClass has the same interface as the class
+        # DataPlanePort defined here.
+        #
+        if "dataplane" in self.config and "portclass" in self.config["dataplane"]:
+            self.dppclass = self.config["dataplane"]["portclass"]
+        elif "linux" in sys.platform:
+            self.dppclass = DataPlanePortLinux
+        else:
+            self.dppclass = DataPlanePortPcap
+
+        self.start()
+
+    def run(self):
+        """
+        Activity function for class
+        """
+        while not self.killed:
+            sockets = [self.waker] + self.ports.values()
+            try:
+                sel_in, sel_out, sel_err = select.select(sockets, [], [], 1)
+            except:
+                print sys.exc_info()
+                self.logger.error("Select error, exiting")
+                break
+
+            with self.cvar:
+                for port in sel_in:
+                    if port == self.waker:
+                        self.waker.wait()
+                        continue
+                    else:
+                        # Enqueue packet
+                        pkt, timestamp = port.recv()
+                        port_number = port._port_number
+                        self.logger.debug("Pkt len %d in on port %d",
+                                          len(pkt), port_number)
+                        if self.pcap_writer:
+                            self.pcap_writer.write(pkt, timestamp, port_number)
+                        queue = self.packet_queues[port_number]
+                        if len(queue) >= self.MAX_QUEUE_LEN:
+                            # Queue full, throw away oldest
+                            queue.pop(0)
+                            self.logger.debug("Discarding oldest packet to make room")
+                        queue.append((pkt, timestamp))
+                self.cvar.notify_all()
+
+        self.logger.info("Thread exit")
+
+    def port_add(self, interface_name, port_number):
+        """
+        Add a port to the dataplane
+        @param interface_name The name of the physical interface like eth1
+        @param port_number The port number used to refer to the port
+        Stashes the port number on the created port object.
+        """
+        self.ports[port_number] = self.dppclass(interface_name, port_number)
+        self.ports[port_number]._port_number = port_number
+        self.packet_queues[port_number] = []
+        # Need to wake up event loop to change the sockets being selected on.
+        self.waker.notify()
+
+    def send(self, port_number, packet):
+        """
+        Send a packet to the given port
+        @param port_number The port to send the data to
+        @param packet Raw packet data to send to port
+        """
+        self.logger.debug("Sending %d bytes to port %d" %
+                          (len(packet), port_number))
+        if self.pcap_writer:
+            self.pcap_writer.write(packet, time.time(), port_number)
+        bytes = self.ports[port_number].send(packet)
+        if bytes != len(packet):
+            self.logger.error("Unhandled send error, length mismatch %d != %d" %
+                     (bytes, len(packet)))
+        return bytes
+
+    def oldest_port_number(self):
+        """
+        Returns the port number with the oldest packet, or
+        None if no packets are queued.
+        """
+        min_port_number = None
+        min_time = float('inf')
+        for (port_number, queue) in self.packet_queues.items():
+            if queue and queue[0][1] < min_time:
+                min_time = queue[0][1]
+                min_port_number = port_number
+        return min_port_number
+
+    # Dequeues and yields packets in the order they were received.
+    # Yields (port number, packet, received time).
+    # If port_number is not specified yields packets from all ports.
+    def packets(self, port_number=None):
+        while True:
+            rcv_port_number = port_number or self.oldest_port_number()
+
+            if rcv_port_number == None:
+                self.logger.debug("Out of packets on all ports")
+                break
+
+            queue = self.packet_queues[rcv_port_number]
+
+            if len(queue) == 0:
+                self.logger.debug("Out of packets on port %d", rcv_port_number)
+                break
+
+            pkt, time = queue.pop(0)
+            yield (rcv_port_number, pkt, time)
+
+    def poll(self, port_number=None, timeout=-1, exp_pkt=None):
+        """
+        Poll one or all dataplane ports for a packet
+
+        If port_number is given, get the oldest packet from that port.
+        Otherwise, find the port with the oldest packet and return
+        that packet.
+
+        If exp_pkt is true, discard all packets until that one is found
+
+        @param port_number If set, get packet from this port
+        @param timeout If positive and no packet is available, block
+        until a packet is received or for this many seconds
+        @param exp_pkt If not None, look for this packet and ignore any
+        others received.  Note that if port_number is None, all packets
+        from all ports will be discarded until the exp_pkt is found
+        @return The triple port_number, packet, pkt_time where packet
+        is received from port_number at time pkt_time.  If a timeout
+        occurs, return None, None, None
+        """
+
+        if exp_pkt and not port_number:
+            self.logger.warn("Dataplane poll with exp_pkt but no port number")
+
+        # Retrieve the packet. Returns (port number, packet, time).
+        def grab():
+            self.logger.debug("Grabbing packet")
+            for (rcv_port_number, pkt, time) in self.packets(port_number):
+                self.logger.debug("Checking packet from port %d", rcv_port_number)
+                if not exp_pkt or match_exp_pkt(self, exp_pkt, pkt):
+                    return (rcv_port_number, pkt, time)
+            self.logger.debug("Did not find packet")
+            return None
+
+        with self.cvar:
+            ret = ofutils.timed_wait(self.cvar, grab, timeout=timeout)
+
+        if ret != None:
+            return ret
+        else:
+            self.logger.debug("Poll time out, no packet from " + str(port_number))
+            return (None, None, None)
+
+    def kill(self):
+        """
+        Stop the dataplane thread.
+        """
+        self.killed = True
+        self.waker.notify()
+        self.join()
+        # Explicitly release ports to ensure we don't run out of sockets
+        # even if someone keeps holding a reference to the dataplane.
+        del self.ports
+
+    def port_down(self, port_number):
+        """Brings the specified port down"""
+        self.ports[port_number].down()
+
+    def port_up(self, port_number):
+        """Brings the specified port up"""
+        self.ports[port_number].up()
+
+    def flush(self):
+        """
+        Drop any queued packets.
+        """
+        for port_number in self.packet_queues.keys():
+            self.packet_queues[port_number] = []
+
+    def start_pcap(self, filename):
+        assert(self.pcap_writer == None)
+        self.pcap_writer = PcapWriter(filename)
+
+    def stop_pcap(self):
+        if self.pcap_writer:
+            self.pcap_writer.close()
+            self.pcap_writer = None