Initial set of Fabric switch test cases

Change-Id: I86fd2b67d3b773aa496f5ef61f1e1fdf51fd9925
diff --git a/Fabric/Utilities/src/python/oftest/controller.py b/Fabric/Utilities/src/python/oftest/controller.py
new file mode 100755
index 0000000..e52bfbe
--- /dev/null
+++ b/Fabric/Utilities/src/python/oftest/controller.py
@@ -0,0 +1,767 @@
+
+# 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
+
+Controller class
+
+Provide the interface to the control channel to the switch under test.  
+
+Class inherits from thread so as to run in background allowing
+asynchronous callbacks (if needed, not required).  Also supports
+polling.
+
+The controller thread maintains a queue.  Incoming messages that
+are not handled by a callback function are placed in this queue for 
+poll calls.  
+
+Callbacks and polling support specifying the message type
+
+@todo Support transaction semantics via xid
+@todo Support select and listen on an administrative socket (or
+use a timeout to support clean shutdown).
+
+Currently only one connection is accepted during the life of
+the controller.   There seems
+to be no clean way to interrupt an accept call.  Using select that also listens
+on an administrative socket and can shut down the socket might work.
+
+"""
+
+import sys
+import os
+import socket
+import time
+import struct
+import select
+import logging
+from threading import Thread
+from threading import Lock
+from threading import Condition
+
+import ofutils
+import loxi
+
+# Configured openflow version
+import ofp as cfg_ofp
+
+FILTER = ''.join( [ (len( repr( chr( x ) ) ) == 3) and chr( x ) or '.'
+                    for x in range( 256 ) ] )
+
+
+def hex_dump_buffer( src, length=16 ):
+    """
+    Convert src to a hex dump string and return the string
+    @param src The source buffer
+    @param length The number of bytes shown in each line
+    @returns A string showing the hex dump
+    """
+    result = [ "\n" ]
+    for i in xrange( 0, len( src ), length ):
+        chars = src[ i:i + length ]
+        hex = ' '.join( [ "%02x" % ord( x ) for x in chars ] )
+        printable = ''.join( [ "%s" % ((ord( x ) <= 127 and
+                                        FILTER[ ord( x ) ]) or '.') for x in
+                               chars ] )
+        result.append( "%04x  %-*s  %s\n" % (i, length * 3, hex, printable) )
+    return ''.join( result )
+
+
+##@todo Find a better home for these identifiers (controller)
+RCV_SIZE_DEFAULT = 32768
+LISTEN_QUEUE_SIZE = 1
+
+
+class Controller( Thread ):
+    """
+    Class abstracting the control interface to the switch.  
+
+    For receiving messages, two mechanism will be implemented.  First,
+    query the interface with poll.  Second, register to have a
+    function called by message type.  The callback is passed the
+    message type as well as the raw packet (or message object)
+
+    One of the main purposes of this object is to translate between network 
+    and host byte order.  'Above' this object, things should be in host
+    byte order.
+
+    @todo Consider using SocketServer for listening socket
+    @todo Test transaction code
+
+    @var rcv_size The receive size to use for receive calls
+    @var max_pkts The max size of the receive queue
+    @var keep_alive If true, listen for echo requests and respond w/
+    echo replies
+    @var initial_hello If true, will send a hello message immediately
+    upon connecting to the switch
+    @var switch If not None, do an active connection to the switch
+    @var host The host to use for connect
+    @var port The port to connect on 
+    @var packets_total Total number of packets received
+    @var packets_expired Number of packets popped from queue as queue full
+    @var packets_handled Number of packets handled by something
+    @var dbg_state Debug indication of state
+    """
+
+    def __init__( self, switch=None, host='127.0.0.1', port=6653, max_pkts=1024,
+                  force=False ):
+        Thread.__init__( self )
+        # Socket related
+        self.rcv_size = RCV_SIZE_DEFAULT
+        self.listen_socket = None
+        self.switch_socket = None
+        self.switch_addr = None
+        self.connect_cv = Condition( )
+        self.message_cv = Condition( )
+        self.tx_lock = Lock( )
+
+        # Used to wake up the event loop from another thread
+        self.waker = ofutils.EventDescriptor( )
+
+        # Counters
+        self.socket_errors = 0
+        self.parse_errors = 0
+        self.packets_total = 0
+        self.packets_expired = 0
+        self.packets_handled = 0
+        self.poll_discards = 0
+
+        # State
+        self.sync = Lock( )
+        self.handlers = { }
+        self.keep_alive = False
+        self.active = True
+        self.initial_hello = True
+
+        # OpenFlow message/packet queue
+        # Protected by the packets_cv lock / condition variable
+        self.packets = [ ]
+        self.packets_cv = Condition( )
+        self.packet_in_count = 0
+
+        # Settings
+        self.max_pkts = max_pkts
+        self.switch = switch
+        self.passive = not self.switch
+        self.force = force
+        self.host = host
+        self.port = port
+        self.dbg_state = "init"
+        self.logger = logging.getLogger( "controller" )
+        self.filter_packet_in = False  # Drop "excessive" packet ins
+        self.pkt_in_run = 0  # Count on run of packet ins
+        self.pkt_in_filter_limit = 50  # Count on run of packet ins
+        self.pkt_in_dropped = 0  # Total dropped packet ins
+        self.transact_to = 15  # Transact timeout default value; add to config
+
+        # Transaction and message type waiting variables 
+        #   xid_cv: Condition variable (semaphore) for packet waiters
+        #   xid: Transaction ID being waited on
+        #   xid_response: Transaction response message
+        self.xid_cv = Condition( )
+        self.xid = None
+        self.xid_response = None
+
+        self.buffered_input = ""
+
+        # Create listen socket
+        if self.passive:
+            self.logger.info( "Create/listen at " + self.host + ":" +
+                              str( self.port ) )
+            ai = socket.getaddrinfo( self.host, self.port, socket.AF_UNSPEC,
+                                     socket.SOCK_STREAM, 0, socket.AI_PASSIVE )
+            # Use first returned addrinfo
+            (family, socktype, proto, name, sockaddr) = ai[ 0 ]
+            self.listen_socket = socket.socket( family, socktype )
+            self.listen_socket.setsockopt( socket.SOL_SOCKET,
+                                           socket.SO_REUSEADDR, 1 )
+            self.listen_socket.bind( sockaddr )
+            self.listen_socket.listen( LISTEN_QUEUE_SIZE )
+
+    def filter_packet( self, rawmsg, hdr ):
+        """
+        Check if packet should be filtered
+
+        Currently filters packet in messages
+        @return Boolean, True if packet should be dropped
+        """
+        # XXX didn't actually check for packet-in...
+        return False
+        # Add check for packet in and rate limit
+        if self.filter_packet_in:
+            # If we were dropping packets, report number dropped
+            # TODO dont drop expected packet ins
+            if self.pkt_in_run > self.pkt_in_filter_limit:
+                self.logger.debug( "Dropped %d packet ins (%d total)"
+                                   % ((self.pkt_in_run -
+                                       self.pkt_in_filter_limit),
+                                      self.pkt_in_dropped) )
+            self.pkt_in_run = 0
+
+        return False
+
+    def _pkt_handle( self, pkt ):
+        """
+        Check for all packet handling conditions
+
+        Parse and verify message 
+        Check if XID matches something waiting
+        Check if message is being expected for a poll operation
+        Check if keep alive is on and message is an echo request
+        Check if any registered handler wants the packet
+        Enqueue if none of those conditions is met
+
+        an echo request in case keep_alive is true, followed by
+        registered message handlers.
+        @param pkt The raw packet (string) which may contain multiple OF msgs
+        """
+
+        # snag any left over data from last read()
+        pkt = self.buffered_input + pkt
+        self.buffered_input = ""
+
+        # Process each of the OF msgs inside the pkt
+        offset = 0
+        while offset < len( pkt ):
+            if offset + 8 > len( pkt ):
+                break
+
+            # Parse the header to get type
+            hdr_version, hdr_type, hdr_length, hdr_xid = cfg_ofp.message.parse_header(
+                    pkt[ offset: ] )
+
+            # Use loxi to resolve to ofp of matching version
+            ofp = loxi.protocol( hdr_version )
+
+            # Extract the raw message bytes
+            if (offset + hdr_length) > len( pkt ):
+                break
+            rawmsg = pkt[ offset: offset + hdr_length ]
+            offset += hdr_length
+
+            # if self.filter_packet(rawmsg, hdr):
+            #    continue
+
+            msg = ofp.message.parse_message( rawmsg )
+            if not msg:
+                self.parse_errors += 1
+                self.logger.warn( "Could not parse message" )
+                continue
+
+            self.logger.debug( "Msg in: version %d class %s len %d xid %d",
+                               hdr_version, type( msg ).__name__, hdr_length,
+                               hdr_xid )
+
+            with self.sync:
+                # Check if transaction is waiting
+                with self.xid_cv:
+                    if self.xid and hdr_xid == self.xid:
+                        self.logger.debug(
+                                "Matched expected XID " + str( hdr_xid ) )
+                        self.xid_response = (msg, rawmsg)
+                        self.xid = None
+                        self.xid_cv.notify( )
+                        continue
+
+                # Check if keep alive is set; if so, respond to echo requests
+                if self.keep_alive:
+                    if hdr_type == ofp.OFPT_ECHO_REQUEST:
+                        self.logger.debug( "Responding to echo request" )
+                        rep = ofp.message.echo_reply( )
+                        rep.xid = hdr_xid
+                        # Ignoring additional data
+                        self.message_send( rep )
+                        continue
+
+                # Generalize to counters for all packet types?
+                if msg.type == ofp.OFPT_PACKET_IN:
+                    self.packet_in_count += 1
+
+                # Log error messages
+                if isinstance( msg, ofp.message.error_msg ):
+                    # pylint: disable=E1103
+                    if msg.err_type in ofp.ofp_error_type_map:
+                        type_str = ofp.ofp_error_type_map[ msg.err_type ]
+                        if msg.err_type == ofp.OFPET_HELLO_FAILED:
+                            code_map = ofp.ofp_hello_failed_code_map
+                        elif msg.err_type == ofp.OFPET_BAD_REQUEST:
+                            code_map = ofp.ofp_bad_request_code_map
+                        elif msg.err_type == ofp.OFPET_BAD_ACTION:
+                            code_map = ofp.ofp_bad_action_code_map
+                        elif msg.err_type == ofp.OFPET_FLOW_MOD_FAILED:
+                            code_map = ofp.ofp_flow_mod_failed_code_map
+                        elif msg.err_type == ofp.OFPET_PORT_MOD_FAILED:
+                            code_map = ofp.ofp_port_mod_failed_code_map
+                        elif msg.err_type == ofp.OFPET_QUEUE_OP_FAILED:
+                            code_map = ofp.ofp_queue_op_failed_code_map
+                        else:
+                            code_map = None
+
+                        if code_map and msg.code in code_map:
+                            code_str = code_map[ msg.code ]
+                        else:
+                            code_str = "unknown"
+                    else:
+                        type_str = "unknown"
+                        code_str = "unknown"
+                    self.logger.warn(
+                            "Received error message: xid=%d type=%s (%d) code=%s (%d)",
+                            hdr_xid, type_str, msg.err_type, code_str,
+                            msg.code )
+
+                # Now check for message handlers; preference is given to
+                # handlers for a specific packet
+                handled = False
+                if hdr_type in self.handlers.keys( ):
+                    handled = self.handlers[ hdr_type ]( self, msg, rawmsg )
+                if not handled and ("all" in self.handlers.keys( )):
+                    handled = self.handlers[ "all" ]( self, msg, rawmsg )
+
+                if not handled:  # Not handled, enqueue
+                    with self.packets_cv:
+                        if len( self.packets ) >= self.max_pkts:
+                            self.packets.pop( 0 )
+                            self.packets_expired += 1
+                        self.packets.append( (msg, rawmsg) )
+                        self.packets_cv.notify_all( )
+                    self.packets_total += 1
+                else:
+                    self.packets_handled += 1
+                    self.logger.debug( "Message handled by callback" )
+
+        # end of 'while offset < len(pkt)'
+        #   note that if offset = len(pkt), this is
+        #   appends a harmless empty string
+        self.buffered_input += pkt[ offset: ]
+
+    def _socket_ready_handle( self, s ):
+        """
+        Handle an input-ready socket
+
+        @param s The socket object that is ready
+        @returns 0 on success, -1 on error
+        """
+
+        if self.passive and s and s == self.listen_socket:
+            if self.switch_socket:
+                self.logger.warning(
+                        "Ignoring incoming connection; already connected to switch" )
+                (sock, addr) = self.listen_socket.accept( )
+                sock.close( )
+                return 0
+
+            try:
+                (sock, addr) = self.listen_socket.accept( )
+            except:
+                self.logger.warning( "Error on listen socket accept" )
+                return -1
+            self.logger.info( self.host + ":" + str(
+                    self.port ) + ": Incoming connection from " + str( addr ) )
+
+            with self.connect_cv:
+                (self.switch_socket, self.switch_addr) = (sock, addr)
+                self.switch_socket.setsockopt( socket.IPPROTO_TCP,
+                                               socket.TCP_NODELAY, True )
+                if self.initial_hello:
+                    self.message_send( cfg_ofp.message.hello( ) )
+                self.connect_cv.notify( )  # Notify anyone waiting
+
+            # Prevent further connections
+            self.listen_socket.close( )
+            self.listen_socket = None
+        elif s and s == self.switch_socket:
+            for idx in range( 3 ):  # debug: try a couple of times
+                try:
+                    pkt = self.switch_socket.recv( self.rcv_size )
+                except:
+                    self.logger.warning( "Error on switch read" )
+                    return -1
+
+                if not self.active:
+                    return 0
+
+                if len( pkt ) == 0:
+                    self.logger.warning( "Zero-length switch read, %d" % idx )
+                else:
+                    break
+
+            if len( pkt ) == 0:  # Still no packet
+                self.logger.warning( "Zero-length switch read; closing cxn" )
+                self.logger.info( str( self ) )
+                return -1
+
+            self._pkt_handle( pkt )
+        elif s and s == self.waker:
+            self.waker.wait( )
+        else:
+            self.logger.error( "Unknown socket ready: " + str( s ) )
+            return -1
+
+        return 0
+
+    def active_connect( self ):
+        """
+        Actively connect to a switch IP addr
+        """
+        try:
+            self.logger.info( "Trying active connection to %s" % self.switch )
+            soc = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
+            soc.connect( (self.switch, self.port) )
+            self.logger.info( "Connected to " + self.switch + " on " +
+                              str( self.port ) )
+            soc.setsockopt( socket.IPPROTO_TCP, socket.TCP_NODELAY, True )
+            self.switch_addr = (self.switch, self.port)
+            return soc
+        except (StandardError, socket.error), e:
+            self.logger.error( "Could not connect to %s at %d:: %s" %
+                               (self.switch, self.port, str( e )) )
+        return None
+
+    def wakeup( self ):
+        """
+        Wake up the event loop, presumably from another thread.
+        """
+        self.waker.notify( )
+
+    def sockets( self ):
+        """
+        Return list of sockets to select on.
+        """
+        socs = [ self.listen_socket, self.switch_socket, self.waker ]
+        return [ x for x in socs if x ]
+
+    def run( self ):
+        """
+        Activity function for class
+
+        Assumes connection to switch already exists.  Listens on
+        switch_socket for messages until an error (or zero len pkt)
+        occurs.
+
+        When there is a message on the socket, check for handlers; queue the
+        packet if no one handles the packet.
+
+        See note for controller describing the limitation of a single
+        connection for now.
+        """
+
+        self.dbg_state = "running"
+
+        while self.active:
+            try:
+                sel_in, sel_out, sel_err = \
+                    select.select( self.sockets( ), [ ], self.sockets( ), 1 )
+            except:
+                print sys.exc_info( )
+                self.logger.error( "Select error, disconnecting" )
+                self.disconnect( )
+
+            for s in sel_err:
+                self.logger.error(
+                        "Got socket error on: " + str( s ) + ", disconnecting" )
+                self.disconnect( )
+
+            for s in sel_in:
+                if self._socket_ready_handle( s ) == -1:
+                    self.disconnect( )
+
+        # End of main loop
+        self.dbg_state = "closing"
+        self.logger.info( "Exiting controller thread" )
+        self.shutdown( )
+
+    def connect( self, timeout=-1 ):
+        """
+        Connect to the switch
+
+        @param timeout Block for up to timeout seconds. Pass -1 for the default.
+        @return Boolean, True if connected
+        """
+
+        if not self.passive:  # Do active connection now
+            self.logger.info( "Attempting to connect to %s on port %s" %
+                                  (self.switch, str( self.port )) )
+            soc = self.active_connect( )
+            if soc:
+                self.logger.info( "Connected to %s", self.switch )
+                self.dbg_state = "running"
+                self.switch_socket = soc
+                self.wakeup( )
+                with self.connect_cv:
+                    if self.initial_hello:
+                        self.message_send( cfg_ofp.message.hello( ) )
+                    self.connect_cv.notify( )  # Notify anyone waiting
+            else:
+                self.logger.error( "Could not actively connect to switch %s",
+                                   self.switch )
+                self.active = False
+        else:
+            with self.connect_cv:
+                ofutils.timed_wait( self.connect_cv, lambda: self.switch_socket,
+                                    timeout=timeout )
+
+        return self.switch_socket is not None
+
+    def disconnect( self, timeout=-1 ):
+        """
+        If connected to a switch, disconnect.
+        """
+        if self.switch_socket:
+            self.switch_socket.close( )
+            self.switch_socket = None
+            self.switch_addr = None
+            with self.packets_cv:
+                self.packets = [ ]
+            with self.connect_cv:
+                self.connect_cv.notifyAll( )
+
+    def wait_disconnected( self, timeout=-1 ):
+        """
+        @param timeout Block for up to timeout seconds. Pass -1 for the default.
+        @return Boolean, True if disconnected
+        """
+
+        with self.connect_cv:
+            ofutils.timed_wait( self.connect_cv,
+                                lambda: True if not self.switch_socket else None,
+                                timeout=timeout )
+        return self.switch_socket is None
+
+    def kill( self ):
+        """
+        Force the controller thread to quit
+        """
+        self.active = False
+        self.wakeup( )
+        self.join( )
+
+    def shutdown( self ):
+        """
+        Shutdown the controller closing all sockets
+
+        @todo Might want to synchronize shutdown with self.sync...
+        """
+
+        self.active = False
+        try:
+            self.switch_socket.shutdown( socket.SHUT_RDWR )
+        except:
+            self.logger.info( "Ignoring switch soc shutdown error" )
+        self.switch_socket = None
+
+        try:
+            self.listen_socket.shutdown( socket.SHUT_RDWR )
+        except:
+            self.logger.info( "Ignoring listen soc shutdown error" )
+        self.listen_socket = None
+
+        # Wakeup condition variables on which controller may be wait
+        with self.xid_cv:
+            self.xid_cv.notifyAll( )
+
+        with self.connect_cv:
+            self.connect_cv.notifyAll( )
+
+        self.wakeup( )
+        self.dbg_state = "down"
+
+    def register( self, msg_type, handler ):
+        """
+        Register a callback to receive a specific message type.
+
+        Only one handler may be registered for a given message type.
+
+        WARNING:  A lock is held during the handler call back, so 
+        the handler should not make any blocking calls
+
+        @param msg_type The type of message to receive.  May be DEFAULT 
+        for all non-handled packets.  The special type, the string "all"
+        will send all packets to the handler.
+        @param handler The function to call when a message of the given 
+        type is received.
+        """
+        # Should check type is valid
+        if not handler and msg_type in self.handlers.keys( ):
+            del self.handlers[ msg_type ]
+            return
+        self.handlers[ msg_type ] = handler
+
+    def poll( self, exp_msg=None, timeout=-1 ):
+        """
+        Wait for the next OF message received from the switch.
+
+        @param exp_msg If set, return only when this type of message 
+        is received (unless timeout occurs).
+
+        @param timeout Maximum number of seconds to wait for the message.
+        Pass -1 for the default timeout.
+
+        @retval A pair (msg, pkt) where msg is a message object and pkt
+        the string representing the packet as received from the socket.
+        This allows additional parsing by the receiver if necessary.
+
+        The data members in the message are in host endian order.
+        If an error occurs, (None, None) is returned
+        """
+
+        if exp_msg is None:
+            self.logger.warn( "DEPRECATED polling for any message class" )
+            klass = None
+        elif isinstance( exp_msg, int ):
+            klass = cfg_ofp.message.message.subtypes[ exp_msg ]
+        elif issubclass( exp_msg, loxi.OFObject ):
+            klass = exp_msg
+        else:
+            raise ValueError( "Unexpected exp_msg argument %r" % exp_msg )
+
+        self.logger.debug( "Polling for %s", klass.__name__ )
+
+        # Take the packet from the queue
+        def grab( ):
+            for i, (msg, pkt) in enumerate( self.packets ):
+                if klass is None or isinstance( msg, klass ):
+                    self.logger.debug( "Got %s message",
+                                       msg.__class__.__name__ )
+                    return self.packets.pop( i )
+            # Not found
+            self.logger.debug( "%s message not in queue", klass.__name__ )
+            return None
+
+        with self.packets_cv:
+            ret = ofutils.timed_wait( self.packets_cv, grab, timeout=timeout )
+
+        if ret != None:
+            (msg, pkt) = ret
+            return (msg, pkt)
+        else:
+            return (None, None)
+
+    def transact( self, msg, timeout=-1 ):
+        """
+        Run a message transaction with the switch
+
+        Send the message in msg and wait for a reply with a matching
+        transaction id.  Transactions have the highest priority in
+        received message handling.
+
+        @param msg The message object to send; must not be a string
+        @param timeout The timeout in seconds; if -1 use default.
+        """
+
+        if msg.xid == None:
+            msg.xid = ofutils.gen_xid( )
+
+        self.logger.debug( "Running transaction %d" % msg.xid )
+
+        with self.xid_cv:
+            if self.xid:
+                self.logger.error( "Can only run one transaction at a time" )
+                return (None, None)
+
+            self.xid = msg.xid
+            self.xid_response = None
+            self.message_send( msg )
+
+            self.logger.debug( "Waiting for transaction %d" % msg.xid )
+            ofutils.timed_wait( self.xid_cv, lambda: self.xid_response,
+                                timeout=timeout )
+
+            if self.xid_response:
+                (resp, pkt) = self.xid_response
+                self.xid_response = None
+            else:
+                (resp, pkt) = (None, None)
+
+        if resp is None:
+            self.logger.warning( "No response for xid " + str( self.xid ) )
+        return (resp, pkt)
+
+    def message_send( self, msg ):
+        """
+        Send the message to the switch
+
+        @param msg A string or OpenFlow message object to be forwarded to
+        the switch.
+        """
+        if not self.switch_socket:
+            # Sending a string indicates the message is ready to go
+            raise Exception( "no socket" )
+
+        if msg.xid == None:
+            msg.xid = ofutils.gen_xid( )
+
+        outpkt = msg.pack( )
+
+        self.logger.debug( "Msg out: version %d class %s len %d xid %d",
+                           msg.version, type( msg ).__name__, len( outpkt ),
+                           msg.xid )
+
+        with self.tx_lock:
+            if self.switch_socket.sendall( outpkt ) is not None:
+                raise AssertionError( "failed to send message to switch" )
+
+        return 0  # for backwards compatibility
+
+    def clear_queue( self ):
+        """
+        Clear the input queue and report the number of messages
+        that were in it
+        """
+        enqueued_pkt_count = len( self.packets )
+        with self.packets_cv:
+            self.packets = [ ]
+        return enqueued_pkt_count
+
+    def __str__( self ):
+        string = "Controller:\n"
+        string += "  state           " + self.dbg_state + "\n"
+        string += "  switch_addr     " + str( self.switch_addr ) + "\n"
+        string += "  pending pkts    " + str( len( self.packets ) ) + "\n"
+        string += "  total pkts      " + str( self.packets_total ) + "\n"
+        string += "  expired pkts    " + str( self.packets_expired ) + "\n"
+        string += "  handled pkts    " + str( self.packets_handled ) + "\n"
+        string += "  poll discards   " + str( self.poll_discards ) + "\n"
+        string += "  parse errors    " + str( self.parse_errors ) + "\n"
+        string += "  sock errrors    " + str( self.socket_errors ) + "\n"
+        string += "  max pkts        " + str( self.max_pkts ) + "\n"
+        string += "  target switch   " + str( self.switch ) + "\n"
+        string += "  host            " + str( self.host ) + "\n"
+        string += "  port            " + str( self.port ) + "\n"
+        string += "  keep_alive      " + str( self.keep_alive ) + "\n"
+        string += "  pkt_in_run      " + str( self.pkt_in_run ) + "\n"
+        string += "  pkt_in_dropped  " + str( self.pkt_in_dropped ) + "\n"
+        return string
+
+    def show( self ):
+        print str( self )
+
+
+def sample_handler( controller, msg, pkt ):
+    """
+    Sample message handler
+
+    This is the prototype for functions registered with the controller
+    class for packet reception
+
+    @param controller The controller calling the handler
+    @param msg The parsed message object
+    @param pkt The raw packet that was received on the socket.  This is
+    in case the packet contains extra unparsed data.
+    @returns Boolean value indicating if the packet was handled.  If
+    not handled, the packet is placed in the queue for pollers to received
+    """
+    pass