Merge pull request #60 from rlane/actions
initial OF 1.3 actions tests
diff --git a/src/python/loxi/generic_util.py b/src/python/loxi/generic_util.py
index ce31049..f965e64 100644
--- a/src/python/loxi/generic_util.py
+++ b/src/python/loxi/generic_util.py
@@ -40,6 +40,13 @@
return deserializer(reader.slice(length), typ)
return unpack_list(reader, wrapper)
+def pad_to(alignment, length):
+ """
+ Return a string of zero bytes that will pad a string of length 'length' to
+ a multiple of 'alignment'.
+ """
+ return "\x00" * ((length + alignment - 1)/alignment*alignment - length)
+
class OFReader(object):
"""
Cursor over a read-only buffer
@@ -79,6 +86,12 @@
raise loxi.ProtocolError("Buffer too short")
self.offset += length
+ def skip_align(self):
+ new_offset = (self.offset + 7) / 8 * 8
+ if new_offset > len(self.buf):
+ raise loxi.ProtocolError("Buffer too short")
+ self.offset = new_offset
+
def is_empty(self):
return self.offset == len(self.buf)
diff --git a/src/python/loxi/of10/const.py b/src/python/loxi/of10/const.py
index 1d29975..dc768c6 100644
--- a/src/python/loxi/of10/const.py
+++ b/src/python/loxi/of10/const.py
@@ -111,6 +111,15 @@
65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
}
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+ 0: 'OF_BSN_VPORT_STATUS_OK',
+ 1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
# Identifiers from group ofp_capabilities
OFPC_FLOW_STATS = 1
OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of11/const.py b/src/python/loxi/of11/const.py
index bd0fd11..4360d37 100644
--- a/src/python/loxi/of11/const.py
+++ b/src/python/loxi/of11/const.py
@@ -186,6 +186,15 @@
65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
}
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+ 0: 'OF_BSN_VPORT_STATUS_OK',
+ 1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
# Identifiers from group ofp_capabilities
OFPC_FLOW_STATS = 1
OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of12/action.py b/src/python/loxi/of12/action.py
index f1f9272..9ae3d8f 100644
--- a/src/python/loxi/of12/action.py
+++ b/src/python/loxi/of12/action.py
@@ -11,6 +11,7 @@
import util
import loxi.generic_util
import loxi
+import oxm # for unpack
def unpack_list(reader):
def deserializer(reader, typ):
@@ -756,15 +757,17 @@
if field != None:
self.field = field
else:
- self.field = ''
+ self.field = None
return
def pack(self):
packed = []
packed.append(struct.pack("!H", self.type))
packed.append(struct.pack("!H", 0)) # placeholder for len at index 1
- packed.append(self.field)
+ packed.append(self.field.pack())
length = sum([len(x) for x in packed])
+ packed.append(loxi.generic_util.pad_to(8, length))
+ length += len(packed[-1])
packed[1] = struct.pack("!H", length)
return ''.join(packed)
@@ -778,7 +781,8 @@
_type = reader.read("!H")[0]
assert(_type == 25)
_len = reader.read("!H")[0]
- obj.field = str(reader.read_all())
+ obj.field = oxm.unpack(reader)
+ reader.skip_align()
return obj
def __eq__(self, other):
diff --git a/src/python/loxi/of12/common.py b/src/python/loxi/of12/common.py
index f2d2493..698fe7a 100644
--- a/src/python/loxi/of12/common.py
+++ b/src/python/loxi/of12/common.py
@@ -706,7 +706,7 @@
packed.append(util.pack_list(self.oxm_list))
length = sum([len(x) for x in packed])
packed[1] = struct.pack("!H", length)
- packed.append('\x00' * ((length + 7)/8*8 - length))
+ packed.append(loxi.generic_util.pad_to(8, length))
return ''.join(packed)
@staticmethod
@@ -720,7 +720,7 @@
assert(_type == 1)
_length = reader.read("!H")[0]
obj.oxm_list = oxm.unpack_list(reader.slice(_length-4))
- reader.skip((_length + 7)/8*8 - _length)
+ reader.skip_align()
return obj
def __eq__(self, other):
diff --git a/src/python/loxi/of12/const.py b/src/python/loxi/of12/const.py
index 20f99bb..b654b1a 100644
--- a/src/python/loxi/of12/const.py
+++ b/src/python/loxi/of12/const.py
@@ -187,6 +187,15 @@
65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
}
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+ 0: 'OF_BSN_VPORT_STATUS_OK',
+ 1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
# Identifiers from group ofp_capabilities
OFPC_FLOW_STATS = 1
OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of12/oxm.py b/src/python/loxi/of12/oxm.py
index f17151e..0fd7948 100644
--- a/src/python/loxi/of12/oxm.py
+++ b/src/python/loxi/of12/oxm.py
@@ -12,15 +12,16 @@
import loxi.generic_util
import loxi
+def unpack(reader):
+ type_len, = reader.peek('!L')
+ if type_len in parsers:
+ return parsers[type_len](reader)
+ else:
+ raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
+ ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
+
def unpack_list(reader):
- def deserializer(reader):
- type_len, = reader.peek('!L')
- if type_len in parsers:
- return parsers[type_len](reader)
- else:
- raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
- ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
- return loxi.generic_util.unpack_list(reader, deserializer)
+ return loxi.generic_util.unpack_list(reader, unpack)
class OXM(object):
type_len = None # override in subclass
@@ -1907,7 +1908,7 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.breakable()
q.text('}')
@@ -1963,10 +1964,10 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.text(","); q.breakable()
q.text("value_mask = ");
- q.text("%#x" % self.value_mask)
+ q.text(util.pretty_ipv4(self.value_mask))
q.breakable()
q.text('}')
@@ -2015,7 +2016,7 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.breakable()
q.text('}')
@@ -2071,10 +2072,10 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.text(","); q.breakable()
q.text("value_mask = ");
- q.text("%#x" % self.value_mask)
+ q.text(util.pretty_ipv4(self.value_mask))
q.breakable()
q.text('}')
diff --git a/src/python/loxi/of13/action.py b/src/python/loxi/of13/action.py
index 645916a..a1e7877 100644
--- a/src/python/loxi/of13/action.py
+++ b/src/python/loxi/of13/action.py
@@ -11,6 +11,7 @@
import util
import loxi.generic_util
import loxi
+import oxm # for unpack
def unpack_list(reader):
def deserializer(reader, typ):
@@ -859,15 +860,17 @@
if field != None:
self.field = field
else:
- self.field = ''
+ self.field = None
return
def pack(self):
packed = []
packed.append(struct.pack("!H", self.type))
packed.append(struct.pack("!H", 0)) # placeholder for len at index 1
- packed.append(self.field)
+ packed.append(self.field.pack())
length = sum([len(x) for x in packed])
+ packed.append(loxi.generic_util.pad_to(8, length))
+ length += len(packed[-1])
packed[1] = struct.pack("!H", length)
return ''.join(packed)
@@ -881,7 +884,8 @@
_type = reader.read("!H")[0]
assert(_type == 25)
_len = reader.read("!H")[0]
- obj.field = str(reader.read_all())
+ obj.field = oxm.unpack(reader)
+ reader.skip_align()
return obj
def __eq__(self, other):
diff --git a/src/python/loxi/of13/common.py b/src/python/loxi/of13/common.py
index e3f9921..952aeba 100644
--- a/src/python/loxi/of13/common.py
+++ b/src/python/loxi/of13/common.py
@@ -781,7 +781,7 @@
packed.append(util.pack_list(self.oxm_list))
length = sum([len(x) for x in packed])
packed[1] = struct.pack("!H", length)
- packed.append('\x00' * ((length + 7)/8*8 - length))
+ packed.append(loxi.generic_util.pad_to(8, length))
return ''.join(packed)
@staticmethod
@@ -795,7 +795,7 @@
assert(_type == 1)
_length = reader.read("!H")[0]
obj.oxm_list = oxm.unpack_list(reader.slice(_length-4))
- reader.skip((_length + 7)/8*8 - _length)
+ reader.skip_align()
return obj
def __eq__(self, other):
diff --git a/src/python/loxi/of13/const.py b/src/python/loxi/of13/const.py
index e2545f1..648617f 100644
--- a/src/python/loxi/of13/const.py
+++ b/src/python/loxi/of13/const.py
@@ -193,6 +193,15 @@
65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
}
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+ 0: 'OF_BSN_VPORT_STATUS_OK',
+ 1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
# Identifiers from group ofp_capabilities
OFPC_FLOW_STATS = 1
OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of13/oxm.py b/src/python/loxi/of13/oxm.py
index f17151e..0fd7948 100644
--- a/src/python/loxi/of13/oxm.py
+++ b/src/python/loxi/of13/oxm.py
@@ -12,15 +12,16 @@
import loxi.generic_util
import loxi
+def unpack(reader):
+ type_len, = reader.peek('!L')
+ if type_len in parsers:
+ return parsers[type_len](reader)
+ else:
+ raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
+ ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
+
def unpack_list(reader):
- def deserializer(reader):
- type_len, = reader.peek('!L')
- if type_len in parsers:
- return parsers[type_len](reader)
- else:
- raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
- ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
- return loxi.generic_util.unpack_list(reader, deserializer)
+ return loxi.generic_util.unpack_list(reader, unpack)
class OXM(object):
type_len = None # override in subclass
@@ -1907,7 +1908,7 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.breakable()
q.text('}')
@@ -1963,10 +1964,10 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.text(","); q.breakable()
q.text("value_mask = ");
- q.text("%#x" % self.value_mask)
+ q.text(util.pretty_ipv4(self.value_mask))
q.breakable()
q.text('}')
@@ -2015,7 +2016,7 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.breakable()
q.text('}')
@@ -2071,10 +2072,10 @@
with q.indent(2):
q.breakable()
q.text("value = ");
- q.text("%#x" % self.value)
+ q.text(util.pretty_ipv4(self.value))
q.text(","); q.breakable()
q.text("value_mask = ");
- q.text("%#x" % self.value_mask)
+ q.text(util.pretty_ipv4(self.value_mask))
q.breakable()
q.text('}')
diff --git a/src/python/oftest/parse.py b/src/python/oftest/parse.py
index 8c98c91..7963c0c 100644
--- a/src/python/oftest/parse.py
+++ b/src/python/oftest/parse.py
@@ -196,7 +196,7 @@
if type(layer.payload) == scapy.Dot1Q:
layer = layer.payload
match.oxm_list.append(ofp.oxm.eth_type(layer.type))
- match.oxm_list.append(ofp.oxm.vlan_vid(layer.vlan))
+ match.oxm_list.append(ofp.oxm.vlan_vid(ofp.OFPVID_PRESENT|layer.vlan))
match.oxm_list.append(ofp.oxm.vlan_pcp(layer.prio))
else:
match.oxm_list.append(ofp.oxm.eth_type(layer.type))
diff --git a/src/python/oftest/testutils.py b/src/python/oftest/testutils.py
index bf2b24a..e87b7a0 100644
--- a/src/python/oftest/testutils.py
+++ b/src/python/oftest/testutils.py
@@ -1598,4 +1598,17 @@
test.assertTrue(msg == None, "Did not expect a packet-in message on port %d" % in_port)
+def openflow_ports(num=None):
+ """
+ Return a list of 'num' OpenFlow port numbers
+
+ If 'num' is None, return all available ports. Otherwise, limit the length
+ of the result to 'num' and raise an exception if not enough ports are
+ available.
+ """
+ ports = sorted(oftest.config["port_map"].keys())
+ if num != None and len(ports) < num:
+ raise Exception("test requires %d ports but only %d are available" % (num, len(ports)))
+ return ports[:num]
+
__all__ = list(set(locals()) - _import_blacklist)
diff --git a/tests-1.3/actions.py b/tests-1.3/actions.py
new file mode 100644
index 0000000..39369d0
--- /dev/null
+++ b/tests-1.3/actions.py
@@ -0,0 +1,161 @@
+# Distributed under the OpenFlow Software License (see LICENSE)
+# Copyright (c) 2010 The Board of Trustees of The Leland Stanford Junior University
+# Copyright (c) 2012, 2013 Big Switch Networks, Inc.
+"""
+Action test cases
+
+These tests check the behavior of each type of action. The matches used are
+exact-match, to satisfy the OXM prerequisites of the set-field actions.
+These tests use a single apply-actions instruction.
+"""
+
+import logging
+
+from oftest import config
+import oftest.base_tests as base_tests
+import ofp
+from loxi.pp import pp
+
+from oftest.testutils import *
+from oftest.parse import parse_ipv6
+
+class Output(base_tests.SimpleDataPlane):
+ """
+ Output to a single port
+ """
+ def runTest(self):
+ in_port, out_port = openflow_ports(2)
+
+ actions = [ofp.action.output(out_port)]
+
+ pkt = simple_tcp_packet()
+
+ logging.info("Running actions test for %s", pp(actions))
+
+ delete_all_flows(self.controller)
+
+ logging.info("Inserting flow")
+ request = ofp.message.flow_add(
+ table_id=0,
+ match=packet_to_flow_match(self, pkt),
+ instructions=[
+ ofp.instruction.apply_actions(actions)],
+ buffer_id=ofp.OFP_NO_BUFFER,
+ priority=1000)
+ self.controller.message_send(request)
+
+ do_barrier(self.controller)
+
+ pktstr = str(pkt)
+
+ logging.info("Sending packet, expecting output to port %d", out_port)
+ self.dataplane.send(in_port, pktstr)
+ receive_pkt_check(self.dataplane, pktstr, [out_port],
+ set(openflow_ports()) - set([out_port]), self)
+
+class OutputMultiple(base_tests.SimpleDataPlane):
+ """
+ Output to three ports
+ """
+ def runTest(self):
+ ports = openflow_ports(4)
+ in_port = ports[0]
+ out_ports = ports[1:4]
+
+ actions = [ofp.action.output(x) for x in out_ports]
+
+ pkt = simple_tcp_packet()
+
+ logging.info("Running actions test for %s", pp(actions))
+
+ delete_all_flows(self.controller)
+
+ logging.info("Inserting flow")
+ request = ofp.message.flow_add(
+ table_id=0,
+ match=packet_to_flow_match(self, pkt),
+ instructions=[
+ ofp.instruction.apply_actions(actions)],
+ buffer_id=ofp.OFP_NO_BUFFER,
+ priority=1000)
+ self.controller.message_send(request)
+
+ do_barrier(self.controller)
+
+ pktstr = str(pkt)
+
+ logging.info("Sending packet, expecting output to ports %r", out_ports)
+ self.dataplane.send(in_port, pktstr)
+ receive_pkt_check(self.dataplane, pktstr, out_ports,
+ set(openflow_ports()) - set(out_ports), self)
+
+class BaseModifyPacketTest(base_tests.SimpleDataPlane):
+ """
+ Base class for action tests that modify a packet
+ """
+
+ def verify_modify(self, actions, pkt, exp_pkt):
+ in_port, out_port = openflow_ports(2)
+
+ actions = actions + [ofp.action.output(out_port)]
+
+ logging.info("Running actions test for %s", pp(actions))
+
+ delete_all_flows(self.controller)
+
+ logging.info("Inserting flow")
+ request = ofp.message.flow_add(
+ table_id=0,
+ match=packet_to_flow_match(self, pkt),
+ instructions=[
+ ofp.instruction.apply_actions(actions)],
+ buffer_id=ofp.OFP_NO_BUFFER,
+ priority=1000)
+ self.controller.message_send(request)
+
+ do_barrier(self.controller)
+
+ logging.info("Sending packet, expecting output to port %d", out_port)
+ self.dataplane.send(in_port, str(pkt))
+ receive_pkt_check(self.dataplane, str(exp_pkt), [out_port],
+ set(openflow_ports()) - set([out_port]), self)
+
+class PushVlan(BaseModifyPacketTest):
+ """
+ Push a vlan tag (vid=0, pcp=0)
+ """
+ def runTest(self):
+ actions = [ofp.action.push_vlan(ethertype=0x8100)]
+ pkt = simple_tcp_packet()
+ exp_pkt = simple_tcp_packet(dl_vlan_enable=True, pktlen=104)
+ self.verify_modify(actions, pkt, exp_pkt)
+
+class PopVlan(BaseModifyPacketTest):
+ """
+ Pop a vlan tag
+ """
+ def runTest(self):
+ actions = [ofp.action.pop_vlan()]
+ pkt = simple_tcp_packet(dl_vlan_enable=True, pktlen=104)
+ exp_pkt = simple_tcp_packet()
+ self.verify_modify(actions, pkt, exp_pkt)
+
+class SetVlanVid(BaseModifyPacketTest):
+ """
+ Set the vlan vid
+ """
+ def runTest(self):
+ actions = [ofp.action.set_field(ofp.oxm.vlan_vid(2))]
+ pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_vid=1)
+ exp_pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_vid=2)
+ self.verify_modify(actions, pkt, exp_pkt)
+
+class SetVlanPcp(BaseModifyPacketTest):
+ """
+ Set the vlan priority
+ """
+ def runTest(self):
+ actions = [ofp.action.set_field(ofp.oxm.vlan_pcp(2))]
+ pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_pcp=1)
+ exp_pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_pcp=2)
+ self.verify_modify(actions, pkt, exp_pkt)
diff --git a/tests-1.3/match.py b/tests-1.3/match.py
index c8bf51d..c807ee8 100644
--- a/tests-1.3/match.py
+++ b/tests-1.3/match.py
@@ -34,9 +34,7 @@
dicts mapping from string names (used in log messages) to string
packet data.
"""
- ports = sorted(config["port_map"].keys())
- in_port = ports[0]
- out_port = ports[1]
+ in_port, out_port = openflow_ports(2)
logging.info("Running match test for %s", match.show())
@@ -89,10 +87,7 @@
Match on ingress port
"""
def runTest(self):
- ports = sorted(config["port_map"].keys())
- in_port = ports[0]
- out_port = ports[1]
- bad_port = ports[2]
+ in_port, out_port, bad_port = openflow_ports(3)
match = ofp.match([
ofp.oxm.in_port(in_port)
diff --git a/tests/nicira_dec_ttl.py b/tests/nicira_dec_ttl.py
index b29f611..39b7f21 100644
--- a/tests/nicira_dec_ttl.py
+++ b/tests/nicira_dec_ttl.py
@@ -26,7 +26,7 @@
outpkt = simple_tcp_packet(pktlen=100, ip_ttl=3)
msg = ofp.message.packet_out(in_port=ofp.OFPP_NONE,
data=str(outpkt),
- buffer_id=x0xffffffff,
+ buffer_id=0xffffffff,
actions=[
ofp.action.nicira_dec_ttl(),
ofp.action.output(port=portA),
@@ -39,3 +39,25 @@
receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=2), [portA], [], self)
receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=1), [portB], [], self)
receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=0), [], [portC], self)
+
+@nonstandard
+class TtlDecrementZeroTtl(base_tests.SimpleDataPlane):
+ def runTest(self):
+ of_ports = config["port_map"].keys()
+ of_ports.sort()
+ self.assertTrue(len(of_ports) >= 2, "Not enough ports for test")
+ portA = of_ports[0]
+ portB = of_ports[1]
+
+ outpkt = simple_tcp_packet(pktlen=100, ip_ttl=0)
+ msg = ofp.message.packet_out(in_port=ofp.OFPP_NONE,
+ data=str(outpkt),
+ buffer_id=0xffffffff,
+ actions=[
+ ofp.action.output(port=portA),
+ ofp.action.nicira_dec_ttl(),
+ ofp.action.output(port=portB)])
+ self.controller.message_send(msg)
+
+ receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=0), [portA], [], self)
+ receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=0), [], [portB], self)