diff --git a/examples/README.md b/examples/README.md index b540fe7..2e69755 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,6 +13,7 @@ included: - `register`: how to use registers in P4 and read / write the state from the control plane - `counter`: how to use counters in P4 +- `action_profile`: how to use action profiles in P4, using ECMP has support All examples are orgranized the same way, with a `p4src` directory containing the P4 source code, and a `README` file describing the P4 program and explaining diff --git a/examples/action_profile/README.md b/examples/action_profile/README.md new file mode 100644 index 0000000..bc2e140 --- /dev/null +++ b/examples/action_profile/README.md @@ -0,0 +1,76 @@ +# Action Profile + +## Description + +This example gives a skeleton ECMP implementation, using an action profile. A P4 +action profile is mapped to an indirect table in bmv2. More precisely, in our +case, the P4 objects `ecmp_group` and `ecmp_action_profile` are mapped to one +bmv2 indirect match table. This has the limitation that sharing action profiles +between P4 tables is not supported in bmv2. + +### Running the demo + +As always, you can start the switch with `./run_switch.sh` (and wait until +`READY` is displayed). We program the dataplane with the following CLI commands +(from [commands.txt] (commands.txt)): + +``` +01. table_indirect_create_group ecmp_group +02. table_indirect_create_member ecmp_group _nop +03. table_indirect_create_member ecmp_group set_ecmp_nexthop 1 +04. table_indirect_create_member ecmp_group set_ecmp_nexthop 2 +05. table_indirect_create_member ecmp_group set_ecmp_nexthop 3 +06. table_indirect_add_member_to_group ecmp_group 1 0 +07. table_indirect_add_member_to_group ecmp_group 3 0 +08. table_indirect_set_default ecmp_group 0 +09. table_indirect_add_with_group ecmp_group 10.0.0.1 => 0 +10. table_dump ecmp_group +``` + +These commands do the following: + +01. create a group, which receives group handle 0 +02. create a member with action _nop and no action data (member handle 0) +03. create a member with action set_ecmp_nexthop and action data port=1 (member handle 1) +04. create a member with action set_ecmp_nexthop and action data port=2 (member handle 2) +05. create a member with action set_ecmp_nexthop and action data port=3 (member handle 3) +06. add member 1 to group 0 +07. add member 3 to group 0 +08. set member 0 (_nop) has the default for table ecmp_group +09. add an entry to table ecmp_group, which maps destination IPv4 address + 10.0.0.1 to group 0 (which includes members 1 and 3) +10. simply dump the table + +These commands are sent automatically when you run `./run_switch.sh`. The last +command should display the following information: + +``` +ecmp_group: +0: 0a000001 => group(0) +members: +0: _nop - +1: set_ecmp_nexthop - 1, +2: set_ecmp_nexthop - 2, +3: set_ecmp_nexthop - 3, +4: set_ecmp_nexthop - 4, +groups: +0: { 1, 3, } +``` + +When we send a packet to the switch (on any port) with destination address +10.0.0.1, the egress port will be set to either 1 or 3. This is because we only +added members 1 and 3 to the group. Which port / member is used for a given +packet is determined by computing a hash over the IP source address and the IP +protocol number. See the P4 program for more details. + +After starting the switch, run `sudo python send_and_count.py`. This Python +script will send 200 packets to the switch and count the pakcets coming out of +ports 2 and 3. The two numbers should be almost the same: + +``` +Sending 200 packets on port 0 +port 1: 106 packets (53%) +port 2: 0 packets (0%) +port 3: 94 packets (47%) +port 4: 0 packets (0%) +``` diff --git a/examples/action_profile/commands.txt b/examples/action_profile/commands.txt new file mode 100644 index 0000000..e7da93c --- /dev/null +++ b/examples/action_profile/commands.txt @@ -0,0 +1,11 @@ +table_indirect_create_group ecmp_group +table_indirect_create_member ecmp_group _nop +table_indirect_create_member ecmp_group set_ecmp_nexthop 1 +table_indirect_create_member ecmp_group set_ecmp_nexthop 2 +table_indirect_create_member ecmp_group set_ecmp_nexthop 3 +table_indirect_create_member ecmp_group set_ecmp_nexthop 4 +table_indirect_add_member_to_group ecmp_group 1 0 +table_indirect_add_member_to_group ecmp_group 3 0 +table_indirect_set_default ecmp_group 0 +table_indirect_add_with_group ecmp_group 10.0.0.1 => 0 +table_dump ecmp_group diff --git a/examples/action_profile/p4src/action_profile.p4 b/examples/action_profile/p4src/action_profile.p4 new file mode 100644 index 0000000..92757f2 --- /dev/null +++ b/examples/action_profile/p4src/action_profile.p4 @@ -0,0 +1,124 @@ +/* +Copyright 2013-present Barefoot Networks, Inc. + +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. +*/ + +header_type ethernet_t { + fields { + dstAddr : 48; + srcAddr : 48; + etherType : 16; + } +} + +header_type ipv4_t { + fields { + version : 4; + ihl : 4; + diffserv : 8; + totalLen : 16; + identification : 16; + flags : 3; + fragOffset : 13; + ttl : 8; + protocol : 8; + hdrChecksum : 16; + srcAddr : 32; + dstAddr: 32; + } +} + +header_type intrinsic_metadata_t { + fields { + mcast_grp : 4; + egress_rid : 4; + mcast_hash : 16; + lf_field_list: 32; + } +} + +parser start { + return parse_ethernet; +} + +header ethernet_t ethernet; +header ipv4_t ipv4; +metadata intrinsic_metadata_t intrinsic_metadata; + +#define ETHERTYPE_IPV4 0x0800 + +parser parse_ethernet { + extract(ethernet); + return select(ethernet.etherType) { + ETHERTYPE_IPV4: parse_ipv4; + default: ingress; + } +} + +parser parse_ipv4 { + extract(ipv4); + return ingress; +} + +action _drop() { + drop(); +} + +action _nop() { +} + +field_list l3_hash_fields { + ipv4.srcAddr; + ipv4.protocol; +} + +field_list_calculation ecmp_hash { + input { + l3_hash_fields; + } + algorithm : bmv2_hash; + output_width : 16; +} + +action_selector ecmp_selector { + selection_key : ecmp_hash; +} + +action set_ecmp_nexthop(port) { + modify_field(standard_metadata.egress_spec, port); +} + +action_profile ecmp_action_profile { + actions { + _nop; + set_ecmp_nexthop; + } + size : 64; + dynamic_action_selection : ecmp_selector; +} + +table ecmp_group { + reads { + ipv4.dstAddr : exact; + } + action_profile: ecmp_action_profile; + size : 1024; +} + +control ingress { + apply(ecmp_group); +} + +control egress { +} diff --git a/examples/action_profile/run_switch.sh b/examples/action_profile/run_switch.sh new file mode 100755 index 0000000..54bd14a --- /dev/null +++ b/examples/action_profile/run_switch.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Copyright 2013-present Barefoot Networks, Inc. +# +# 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. + +THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +source $THIS_DIR/../env.sh + +P4C_BM_SCRIPT=$P4C_BM_PATH/p4c_bm/__main__.py + +SWITCH_PATH=$BMV2_PATH/targets/simple_switch/simple_switch + +CLI_PATH=$BMV2_PATH/targets/simple_switch/sswitch_CLI + +# Probably not very elegant but it works nice here: we enable interactive mode +# to be able to use fg. We start the switch in the background, sleep for 2 +# minutes to give it time to start, then add the entries and put the switch +# process back in the foreground +set -m +$P4C_BM_SCRIPT p4src/action_profile.p4 --json action_profile.json +sudo echo "sudo" > /dev/null +sudo $BMV2_PATH/targets/simple_switch/simple_switch action_profile.json \ + -i 0@veth0 -i 1@veth2 -i 2@veth4 -i 3@veth6 -i 4@veth8 \ + --nanolog ipc:///tmp/bm-0-log.ipc \ + --pcap & +sleep 2 +$CLI_PATH action_profile.json < commands.txt +echo "READY!!!" +fg diff --git a/examples/action_profile/send_and_count.py b/examples/action_profile/send_and_count.py new file mode 100644 index 0000000..d67c935 --- /dev/null +++ b/examples/action_profile/send_and_count.py @@ -0,0 +1,59 @@ +from scapy.all import * +import sys +import threading +import time + + +big_lock = threading.Lock() +counts = {} + + +class Receiver(threading.Thread): + def __init__(self, port, veth): + threading.Thread.__init__(self) + self.daemon = True + self.port = port + self.veth = veth + + def received(self, p): + # no need for a lock, each thread is accessing a different key, and the + # dictionary itself is not modified + counts[self.port] += 1 + + def run(self): + sniff(iface=self.veth, prn=lambda x: self.received(x)) + + +def main(): + try: + num_packets = int(sys.argv[1]) + except: + num_packets = 200 + print "Sending", num_packets, "packets on port 0" + + port_map = { + 1: "veth3", + 2: "veth5", + 3: "veth7", + 4: "veth9" + } + + for port in port_map: + counts[port] = 0 + for port, veth in port_map.items(): + Receiver(port, veth).start() + + for i in xrange(num_packets): + src = "11.0.%d.%d" % (i >> 256, i % 256) + p = Ether(src="aa:aa:aa:aa:aa:aa") / IP(dst="10.0.0.1", src=src) / TCP() / "aaaaaaaaaaaaaaaaaaa" + sendp(p, iface="veth1", verbose=0) + + time.sleep(1) + + for port in port_map: + print "port {0}: {1} packets ({2}%)".format( + port, counts[port], (100 * counts[port]) / num_packets + ) + +if __name__ == '__main__': + main()