| """ |
| 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 netutils |
| from threading import Thread |
| from threading import Lock |
| from threading import Condition |
| import select |
| import logging |
| from oft_assert import oft_assert |
| |
| ##@todo Find a better home for these identifiers (dataplane) |
| RCV_SIZE_DEFAULT = 4096 |
| ETH_P_ALL = 0x03 |
| RCV_TIMEOUT = 10000 |
| |
| class DataPlanePort(Thread): |
| """ |
| Class defining a port monitoring object. |
| |
| Control a dataplane port connected to the switch under test. |
| Creates a promiscuous socket on a physical interface. |
| Queues the packets received on that interface with time stamps. |
| Inherits from Thread class as meant to run in background. Also |
| supports polling. |
| Use accessors to dequeue packets for proper synchronization. |
| |
| Currently assumes a controlling 'parent' which maintains a |
| common Lock object and a total packet-pending count. May want |
| to decouple that some day. |
| """ |
| |
| def __init__(self, interface_name, port_number, parent, max_pkts=1024): |
| """ |
| Set up a port monitor object |
| @param interface_name The name of the physical interface like eth1 |
| @param port_number The port number associated with this port |
| @param parent The controlling dataplane object; for pkt wait CV |
| @param max_pkts Maximum number of pkts to keep in queue |
| """ |
| Thread.__init__(self) |
| self.interface_name = interface_name |
| self.max_pkts = max_pkts |
| self.packets_total = 0 |
| self.packets = [] |
| self.packets_discarded = 0 |
| self.port_number = port_number |
| logname = "dp-" + interface_name |
| self.logger = logging.getLogger(logname) |
| try: |
| self.socket = self.interface_open(interface_name) |
| except: |
| self.logger.info("Could not open socket") |
| sys.exit(1) |
| self.logger.info("Openned port monitor socket") |
| self.parent = parent |
| self.pkt_sync = self.parent.pkt_sync |
| |
| def interface_open(self, interface_name): |
| """ |
| Open a socket in a promiscuous mode for a data connection. |
| @param interface_name port name as a string such as 'eth1' |
| @retval s socket |
| """ |
| s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, |
| socket.htons(ETH_P_ALL)) |
| s.bind((interface_name, 0)) |
| netutils.set_promisc(s, interface_name) |
| s.settimeout(RCV_TIMEOUT) |
| return s |
| |
| def run(self): |
| """ |
| Activity function for class |
| """ |
| self.running = True |
| self.socs = [self.socket] |
| error_warned = False # Have we warned about error? |
| while self.running: |
| try: |
| sel_in, sel_out, sel_err = \ |
| select.select(self.socs, [], [], 1) |
| except: |
| print sys.exc_info() |
| self.logger.error("Select error, exiting") |
| break |
| |
| if not self.running: |
| break |
| |
| if (sel_in is None) or (len(sel_in) == 0): |
| continue |
| |
| try: |
| rcvmsg = self.socket.recv(RCV_SIZE_DEFAULT) |
| except socket.error: |
| if not error_warned: |
| self.logger.info("Socket error on recv") |
| error_warned = True |
| continue |
| |
| if len(rcvmsg) == 0: |
| self.logger.info("Zero len pkt rcvd") |
| self.kill() |
| break |
| |
| rcvtime = time.clock() |
| self.logger.debug("Pkt len " + str(len(rcvmsg)) + |
| " in at " + str(rcvtime)) |
| |
| # Enqueue packet |
| self.pkt_sync.acquire() |
| if len(self.packets) >= self.max_pkts: |
| # Queue full, throw away oldest |
| self.packets.pop(0) |
| self.packets_discarded += 1 |
| else: |
| self.parent.packets_pending += 1 |
| # Check if parent is waiting on this (or any) port |
| drop_pkt = False |
| if self.parent.want_pkt: |
| if (not self.parent.want_pkt_port or |
| self.parent.want_pkt_port == self.port_number): |
| if self.parent.exp_pkt: |
| if str(self.parent.exp_pkt) != str(rcvmsg): |
| drop_pkt = True |
| if not drop_pkt: |
| self.parent.got_pkt_port = self.port_number |
| self.parent.want_pkt = False |
| self.parent.pkt_sync.notify() |
| if not drop_pkt: |
| self.packets.append((rcvmsg, rcvtime)) |
| self.packets_total += 1 |
| self.pkt_sync.release() |
| |
| self.logger.info("Thread exit ") |
| |
| def kill(self): |
| """ |
| Terminate the running thread |
| """ |
| self.logger.debug("Port monitor kill") |
| self.running = False |
| try: |
| self.socket.close() |
| except: |
| self.logger.info("Ignoring dataplane soc shutdown error") |
| |
| def dequeue(self, use_lock=True): |
| """ |
| Get the oldest packet in the queue |
| @param use_lock If True, acquires the packet sync lock (which is |
| really the parent's lock) |
| @return The pair packet, packet time-stamp |
| """ |
| if use_lock: |
| self.pkt_sync.acquire() |
| if len(self.packets) > 0: |
| pkt, pkt_time = self.packets.pop(0) |
| self.parent.packets_pending -= 1 |
| else: |
| pkt = pkt_time = None |
| if use_lock: |
| self.pkt_sync.release() |
| return pkt, pkt_time |
| |
| def timestamp_head(self): |
| """ |
| Return the timestamp of the head of queue or None if empty |
| """ |
| rv = None |
| try: |
| rv = self.packets[0][1] |
| except: |
| rv = None |
| return rv |
| |
| def flush(self): |
| """ |
| Clear the packet queue |
| """ |
| self.pkt_sync.acquire() |
| self.packets_discarded += len(self.packets) |
| self.parent.packets_pending -= len(self.packets) |
| self.packets = [] |
| self.packet_times = [] |
| self.pkt_sync.release() |
| |
| |
| def send(self, packet): |
| """ |
| Send a packet to the dataplane port |
| @param packet The packet data to send to the port |
| @retval The number of bytes sent |
| """ |
| return self.socket.send(packet) |
| |
| |
| def register(self, handler): |
| """ |
| Register a callback function to receive packets from this |
| port. The callback will be passed the packet, the |
| interface name and the port number (if set) on which the |
| packet was received. |
| |
| To be implemented |
| """ |
| pass |
| |
| def show(self, prefix=''): |
| print prefix + "Name: " + self.interface_name |
| print prefix + "Pkts pending: " + str(len(self.packets)) |
| print prefix + "Pkts total: " + str(self.packets_total) |
| print prefix + "socket: " + str(self.socket) |
| |
| |
| class DataPlane: |
| """ |
| Class defining access primitives to the data plane |
| Controls a list of DataPlanePort objects |
| """ |
| def __init__(self): |
| self.port_list = {} |
| # pkt_sync serves double duty as a regular top level lock and |
| # as a condition variable |
| self.pkt_sync = Condition() |
| |
| # These are used to signal async pkt arrival for polling |
| self.want_pkt = False |
| self.exp_pkt = None |
| self.want_pkt_port = None # What port required (or None) |
| self.got_pkt_port = None # On what port received? |
| self.packets_pending = 0 # Total pkts in all port queues |
| self.logger = logging.getLogger("dataplane") |
| |
| def port_add(self, interface_name, port_number): |
| """ |
| Add a port to the dataplane |
| TBD: Max packets for queue? |
| @param interface_name The name of the physical interface like eth1 |
| @param port_number The port number used to refer to the port |
| """ |
| |
| self.port_list[port_number] = DataPlanePort(interface_name, |
| port_number, self) |
| self.port_list[port_number].start() |
| |
| 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)) |
| bytes = self.port_list[port_number].send(packet) |
| if bytes != len(packet): |
| self.logger.error("Unhandled send error, length mismatch %d != %d" % |
| (bytes, len(packet))) |
| return bytes |
| |
| def flood(self, packet): |
| """ |
| Send a packet to all ports |
| @param packet Raw packet data to send to port |
| """ |
| for port_number in self.port_list.keys(): |
| bytes = self.port_list[port_number].send(packet) |
| if bytes != len(packet): |
| self.logger.error("Unhandled send error" + |
| ", port %d, length mismatch %d != %d" % |
| (port_number, bytes, len(packet))) |
| |
| def _oldest_packet_find(self): |
| # Find port with oldest packet |
| min_time = 0 |
| min_port = -1 |
| for port_number in self.port_list.keys(): |
| ptime = self.port_list[port_number].timestamp_head() |
| if ptime: |
| if (min_port == -1) or (ptime < min_time): |
| min_time = ptime |
| min_port = port_number |
| oft_assert(min_port != -1, "Could not find port when pkts pending") |
| |
| return min_port |
| |
| def poll(self, port_number=None, timeout=None, 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. |
| @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. Requires port_number to be specified |
| @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 |
| """ |
| |
| self.pkt_sync.acquire() |
| |
| if exp_pkt and not port_number: |
| print "WARNING: Dataplane poll: exp_pkt without port number" |
| |
| # Check if requested specific port and it has a packet |
| if port_number and len(self.port_list[port_number].packets) != 0: |
| while len(self.port_list[port_number].packets) != 0: |
| pkt, time = self.port_list[port_number].dequeue(use_lock=False) |
| if not exp_pkt: |
| break |
| if str(pkt) == str(exp_pkt): |
| break |
| pkt = None # Discard silently |
| if pkt: |
| self.pkt_sync.release() |
| oft_assert(pkt, "Poll: packet not found on port " + |
| str(port_number)) |
| return port_number, pkt, time |
| |
| # Check if requested any port and some packet pending |
| if not port_number and self.packets_pending != 0: |
| port = self._oldest_packet_find() |
| pkt, time = self.port_list[port].dequeue(use_lock=False) |
| self.pkt_sync.release() |
| oft_assert(pkt, "Poll: oldest packet not found") |
| return port, pkt, time |
| |
| # No packet pending; blocking call requested? |
| if not timeout: |
| self.pkt_sync.release() |
| return None, None, None |
| |
| # Desired packet isn't available and timeout is specified |
| # Already holding pkt_sync; wait on pkt_sync variable |
| self.want_pkt = True |
| self.exp_pkt = exp_pkt |
| self.want_pkt_port = port_number |
| self.got_pkt_port = None |
| self.pkt_sync.wait(timeout) |
| self.want_pkt = False |
| self.exp_pkt = None |
| if self.got_pkt_port: |
| pkt, time = \ |
| self.port_list[self.got_pkt_port].dequeue(use_lock=False) |
| self.pkt_sync.release() |
| oft_assert(pkt, "Poll: pkt reported, but not found at " + |
| str(self.got_pkt_port)) |
| return self.got_pkt_port, pkt, time |
| |
| self.pkt_sync.release() |
| self.logger.debug("Poll time out, no packet from " + str(port_number)) |
| |
| return None, None, None |
| |
| def kill(self, join_threads=True): |
| """ |
| Close all sockets for dataplane |
| @param join_threads If True call join on each thread |
| """ |
| for port_number in self.port_list.keys(): |
| self.port_list[port_number].kill() |
| if join_threads: |
| self.logger.debug("Joining " + str(port_number)) |
| self.port_list[port_number].join() |
| |
| self.logger.info("DataPlane shutdown") |
| |
| def show(self, prefix=''): |
| print prefix + "Dataplane Controller" |
| print prefix + "Packets pending" + str(self.packets_pending) |
| for pnum, port in self.port_list.items(): |
| print prefix + "OpenFlow Port Number " + str(pnum) |
| port.show(prefix + ' ') |
| |