From e7e6899d5c5c90fd3033d9bc22e23a84e48ba81c Mon Sep 17 00:00:00 2001 From: Georgios Nikolaidis Date: Wed, 25 Apr 2018 00:56:09 -0700 Subject: [PATCH] Added advanced Heavy Hitter Detection example (#136) * Added advanced Heavy Hitter Detection example * Changed directory location * Restored skeleton version * Added files for common run infra with the other tutorials * Updated readme * Autogenerate setup rules * Commends in simple_router.p4 * Fix typos * Removed commended out lines --- Teaching/Stanford_CS344_2018/.gitignore | 1 + Teaching/Stanford_CS344_2018/README.md | 126 ++++++ Teaching/Stanford_CS344_2018/filter_reset.sh | 26 ++ Teaching/Stanford_CS344_2018/header.p4 | 83 ++++ Teaching/Stanford_CS344_2018/p4app.json | 10 + Teaching/Stanford_CS344_2018/parser.p4 | 51 +++ Teaching/Stanford_CS344_2018/run.sh | 6 + Teaching/Stanford_CS344_2018/setup.py | 21 + .../simple_router.config.template | 12 + Teaching/Stanford_CS344_2018/simple_router.p4 | 210 ++++++++++ .../solution/simple_router.p4 | 222 ++++++++++ Teaching/utils/Makefile | 42 ++ Teaching/utils/mininet/appcontroller.py | 93 +++++ Teaching/utils/mininet/apptopo.py | 70 ++++ .../utils/mininet/multi_switch_mininet.py | 243 +++++++++++ Teaching/utils/mininet/p4_mininet.py | 161 ++++++++ Teaching/utils/mininet/shortest_path.py | 78 ++++ .../utils/mininet/single_switch_mininet.py | 133 ++++++ Teaching/utils/netstat.py | 21 + Teaching/utils/p4_mininet.py | 162 ++++++++ Teaching/utils/p4apprunner.py | 320 +++++++++++++++ Teaching/utils/p4runtime_switch.py | 122 ++++++ Teaching/utils/run_exercise.py | 382 ++++++++++++++++++ 23 files changed, 2595 insertions(+) create mode 100644 Teaching/Stanford_CS344_2018/.gitignore create mode 100644 Teaching/Stanford_CS344_2018/README.md create mode 100755 Teaching/Stanford_CS344_2018/filter_reset.sh create mode 100644 Teaching/Stanford_CS344_2018/header.p4 create mode 100644 Teaching/Stanford_CS344_2018/p4app.json create mode 100644 Teaching/Stanford_CS344_2018/parser.p4 create mode 100755 Teaching/Stanford_CS344_2018/run.sh create mode 100644 Teaching/Stanford_CS344_2018/setup.py create mode 100644 Teaching/Stanford_CS344_2018/simple_router.config.template create mode 100644 Teaching/Stanford_CS344_2018/simple_router.p4 create mode 100644 Teaching/Stanford_CS344_2018/solution/simple_router.p4 create mode 100644 Teaching/utils/Makefile create mode 100644 Teaching/utils/mininet/appcontroller.py create mode 100644 Teaching/utils/mininet/apptopo.py create mode 100755 Teaching/utils/mininet/multi_switch_mininet.py create mode 100644 Teaching/utils/mininet/p4_mininet.py create mode 100644 Teaching/utils/mininet/shortest_path.py create mode 100755 Teaching/utils/mininet/single_switch_mininet.py create mode 100644 Teaching/utils/netstat.py create mode 100644 Teaching/utils/p4_mininet.py create mode 100755 Teaching/utils/p4apprunner.py create mode 100644 Teaching/utils/p4runtime_switch.py create mode 100755 Teaching/utils/run_exercise.py diff --git a/Teaching/Stanford_CS344_2018/.gitignore b/Teaching/Stanford_CS344_2018/.gitignore new file mode 100644 index 0000000..816826e --- /dev/null +++ b/Teaching/Stanford_CS344_2018/.gitignore @@ -0,0 +1 @@ +simple_router.config diff --git a/Teaching/Stanford_CS344_2018/README.md b/Teaching/Stanford_CS344_2018/README.md new file mode 100644 index 0000000..6cdc701 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/README.md @@ -0,0 +1,126 @@ +# Instructions + +## Introduction + +In this tutorial, you will implement a heavy hitter detection filter. + +Network flows typically have a fairly wide distribution in terms of the +data they transmit, with most of the flows sending little data and few +flows sending a lot. The latter flows are called heavy hitters, and they +often have a detrimental effect to network performance. This is +because they cause congestion, leading to significantly increased completion +times for small, short-lived flows. Detecting heavy hitters allows us to treat them +differently, e.g. we can put their packets in low priority queues, allowing +packets of other flows to face little or no congestion. + +In this example, you will implement a heavy hitter detection filter within +a router. You can find a skeleton of the program in simple_router.p4. In that +file, you have to fill in the parts that are marked with TODO. + +This example is based on [count-min sketch](http://theory.stanford.edu/~tim/s15/l/l2.pdf). +In fact, we use two count-min sketches which are reset with an offset +equal to their half-life. With every new packet coming in, we update +the values of both sketches but we use only the ones of the least +recently reset one to decide whether a packet belongs to a heavy hitter +flow or not. + +> **Spoiler alert:** There is a reference solution in the `solution` +> sub-directory. Feel free to compare your implementation to the +> reference. + + +## Step 1: Run the (incomplete) starter code + +The directory with this README also contains a skeleton P4 program, +`simple_router.p4`, which implements a simple router. Your job will be to +extend this skeleton program to properly implement a heavy hitter +detection filter. + +Before that, let's compile the incomplete `simple_router.p4` and bring +up a switch in Mininet to test its behavior. + +1. In your shell, run: + ```bash + ./run.sh + ``` + This will: + * create a p4app application, + * compile `simple_switch.p4`, + * generate control plane code, + * start a Mininet instance with one switch (`s1`) conected to + two hosts (`h1` and `h2`). + * install the control plane code to your switch, + * The hosts are assigned IPs of `10.0.0.10` and `10.0.1.10`. + +2. You should now see a Mininet command prompt. Run ping between + `h1` and `h2` to make sure that everything runs correctly: + ```bash + mininet> h1 ping h2 + ``` + You should see all packets going through. + +3. Type `exit` to leave each Mininet command line. + +### A note about the control plane + +A P4 program defines a packet-processing pipeline, but the rules +within each table are inserted by the control plane. When a rule +matches a packet, its action is invoked with parameters supplied by +the control plane as part of the rule. + +In this exercise, we have already implemented the control plane +logic for you. As part of invoking `run.sh`, a set of rules is generated +by `setup.py` and when bringing up the Mininet instance, these +packet-processing rules are installed in the tables of +the switch. These are defined in the `simple_router.config` file. + +## Step 2: Implement the heavy hitter detection filter + +The `simple_router.p4` file contains a skeleton P4 program with key pieces of +logic replaced by `TODO` comments. Your implementation should follow +the structure given in this file, just replace each `TODO` with logic +implementing the missing piece. + +More specifically, you need to implement the main actions used within +the heavy hitter detection block. In this example, when our filter +classifies a packet as belonging to a heavy hitter flow, it marks +it as such and then the switch drops it before reaching the +egress control. + +## Step 3: Run your solution + +Our heavy hitter filter requires periodic reset of the registers of the +count-min sketches. Running: +```bash +bash filter_reset.sh +``` +in a terminal window does that periodic reset for you. + +The filter currently allows 1000 bytes/sec (you can change that value +in `setup.py`). + +In another terminal window, run: +```bash +./run.sh +``` + +In the minigraph window, you can try: +``` +h1 ping -s 80 -i 0.1 h2 +``` +With this command h1, sends a packet with a total IP length +of 100 bytes every 100 ms. When you run this command, you +shouldn't see any drops. If on the other hand you run: +``` +h1 ping -s 80 -i 0.05 h2 +``` +h1 sends a packet every 50 ms, which puts the flow above +the filter limit. In this case you will observe that about +half of the packets send by h1 are being dropped at the switch. + +### Next steps +Check out the code in `setup.py` and `filter_reset.sh`. By changing +the constants in those, you can experiment with different +heavy hitter threshold levels, count-min sketch sizes and the accuracy +of the throughput approximation. + diff --git a/Teaching/Stanford_CS344_2018/filter_reset.sh b/Teaching/Stanford_CS344_2018/filter_reset.sh new file mode 100755 index 0000000..f5ca34c --- /dev/null +++ b/Teaching/Stanford_CS344_2018/filter_reset.sh @@ -0,0 +1,26 @@ +#!/bin/sh +CONTAINER_ID=`docker ps | tail -n 1 | cut -d ' ' -f 1` +ACTIVE_FILTER='A' + +while true; do + CUR_TIME=`echo "get_time_elapsed" | docker exec -i $CONTAINER_ID simple_switch_CLI | grep Runtime | head -n 1 | cut -d ':' -f 2` + CUR_TIME=${CUR_TIME}000 + echo $CUR_TIME + echo "register_write last_reset_time 0 $CUR_TIME" | docker exec -i $CONTAINER_ID simple_switch_CLI + if [ $ACTIVE_FILTER == 'A' ] ; then + echo "register_write is_a_active 0 1" + echo "register_reset hashtable_b0" | docker exec -i $CONTAINER_ID simple_switch_CLI + echo "register_reset hashtable_b1" | docker exec -i $CONTAINER_ID simple_switch_CLI + echo "register_reset hashtable_b2" | docker exec -i $CONTAINER_ID simple_switch_CLI + echo "register_reset hashtable_b3" | docker exec -i $CONTAINER_ID simple_switch_CLI + ACTIVE_FILTER='B' + else + echo "register_write is_a_active 0 0" + echo "register_reset hashtable_a0" | docker exec -i $CONTAINER_ID simple_switch_CLI + echo "register_reset hashtable_a1" | docker exec -i $CONTAINER_ID simple_switch_CLI + echo "register_reset hashtable_a2" | docker exec -i $CONTAINER_ID simple_switch_CLI + echo "register_reset hashtable_a3" | docker exec -i $CONTAINER_ID simple_switch_CLI + ACTIVE_FILTER='A' + fi + sleep 4 +done diff --git a/Teaching/Stanford_CS344_2018/header.p4 b/Teaching/Stanford_CS344_2018/header.p4 new file mode 100644 index 0000000..8709cd6 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/header.p4 @@ -0,0 +1,83 @@ +#ifndef __HEADER_P4__ +#define __HEADER_P4__ 1 + +struct ingress_metadata_t { + bit<32> nhop_ipv4; +} + +header ethernet_t { + bit<48> dstAddr; + bit<48> srcAddr; + bit<16> etherType; +} + +header ipv4_t { + bit<4> version; + bit<4> ihl; + bit<8> diffserv; + bit<16> totalLen; + bit<16> identification; + bit<3> flags; + bit<13> fragOffset; + bit<8> ttl; + bit<8> protocol; + bit<16> hdrChecksum; + bit<32> srcAddr; + bit<32> dstAddr; +} + +header tcp_t { + bit<16> srcPort; + bit<16> dstPort; + bit<32> seqNo; + bit<32> ackNo; + bit<4> dataOffset; + bit<4> res; + bit<8> flags; + bit<16> window; + bit<16> checksum; + bit<16> urgentPtr; +} + +header udp_t { + bit<16> srcPort; + bit<16> dstPort; + bit<16> hdrLength; + bit<16> checksum; +} + +struct hhd_t { + @name("filter_age") + bit<48> filter_age; + bit<32> value_a0; + bit<32> value_a1; + bit<32> value_a2; + bit<32> value_a3; + bit<32> value_b0; + bit<32> value_b1; + bit<32> value_b2; + bit<32> value_b3; + bit<32> threshold; + bit<1> is_a_active; + bit<1> is_heavy_hitter; +} + +struct metadata { + @name("ingress_metadata") + ingress_metadata_t ingress_metadata; + @name("hhd") + hhd_t hhd; +} + +struct headers { + @name("ethernet") + ethernet_t ethernet; + @name("ipv4") + ipv4_t ipv4; + @name("tcp") + tcp_t tcp; + @name("udp") + udp_t udp; +} + +#endif // __HEADER_P4__ diff --git a/Teaching/Stanford_CS344_2018/p4app.json b/Teaching/Stanford_CS344_2018/p4app.json new file mode 100644 index 0000000..3220ccc --- /dev/null +++ b/Teaching/Stanford_CS344_2018/p4app.json @@ -0,0 +1,10 @@ +{ + "program": "simple_router.p4", + "language": "p4-16", + "targets": { + "mininet": { + "num-hosts": 2, + "switch-config": "simple_router.config" + } + } +} diff --git a/Teaching/Stanford_CS344_2018/parser.p4 b/Teaching/Stanford_CS344_2018/parser.p4 new file mode 100644 index 0000000..0b64560 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/parser.p4 @@ -0,0 +1,51 @@ +parser ParserImpl(packet_in packet, out headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { + state start { + transition parse_ethernet; + } + + state parse_ethernet { + packet.extract(hdr.ethernet); + transition select(hdr.ethernet.etherType) { + 16w0x800: parse_ipv4; + default: accept; + } + } + + state parse_ipv4 { + packet.extract(hdr.ipv4); + transition select(hdr.ipv4.protocol) { + 8w0x6: parse_tcp; + default: accept; + } + } + + state parse_tcp { + packet.extract(hdr.tcp); + transition accept; + } +} + +control DeparserImpl(packet_out packet, in headers hdr) { + apply { + packet.emit(hdr.ethernet); + packet.emit(hdr.ipv4); + packet.emit(hdr.tcp); + } +} + +control verifyChecksum(inout headers hdr, inout metadata meta) { + apply { } +} + +control computeChecksum(inout headers hdr, inout metadata meta) { + apply { + update_checksum( + hdr.ipv4.isValid(), + { hdr.ipv4.version, hdr.ipv4.ihl, hdr.ipv4.diffserv, + hdr.ipv4.totalLen, hdr.ipv4.identification, + hdr.ipv4.flags, hdr.ipv4.fragOffset, hdr.ipv4.ttl, + hdr.ipv4.protocol, hdr.ipv4.srcAddr, hdr.ipv4.dstAddr }, + hdr.ipv4.hdrChecksum, + HashAlgorithm.csum16); + } +} diff --git a/Teaching/Stanford_CS344_2018/run.sh b/Teaching/Stanford_CS344_2018/run.sh new file mode 100755 index 0000000..d3dd067 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/run.sh @@ -0,0 +1,6 @@ +P4APPRUNNER=../utils/p4apprunner.py +python setup.py +mkdir -p build +tar -czf build/p4app.tgz * --exclude='build' +#cd build +sudo python $P4APPRUNNER p4app.tgz --build-dir ./build diff --git a/Teaching/Stanford_CS344_2018/setup.py b/Teaching/Stanford_CS344_2018/setup.py new file mode 100644 index 0000000..f3e7087 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/setup.py @@ -0,0 +1,21 @@ +import os +from shutil import copyfile + +unit_duration = 20 # log_2 of unit duration (so 2**unit_duration) +total_time_bits = 48 +log_units = 3 # log_2 of number of units +units = 2**log_units +threshold = 8*1000.0 # in bytes + +copyfile('simple_router.config.template', 'simple_router.config') + +with open('simple_router.config', 'a') as fd: + time_mask = (2**(unit_duration+log_units)-1) - (2**unit_duration -1) + for unit in range(units): + time_value = unit*2**unit_duration + if unit < units/2: + unit_threshold = int((unit+1) * threshold / units + threshold/2 ) + else: + unit_threshold = int((unit+1) * threshold / units) + fd.write('table_add threshold_table set_threshold %d&&&%d => %d 0\n' % (time_value, time_mask, unit_threshold)) + diff --git a/Teaching/Stanford_CS344_2018/simple_router.config.template b/Teaching/Stanford_CS344_2018/simple_router.config.template new file mode 100644 index 0000000..f22ae70 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/simple_router.config.template @@ -0,0 +1,12 @@ +set_crc16_parameters calc_2 0x1021 0xffff 0x0000 false false +set_crc32_parameters calc_0 0x4c11db7 0xffffffff 0x00000000 false false +table_set_default send_frame egress_drop +table_set_default forward ingress_drop +table_set_default ipv4_lpm ingress_drop +table_add send_frame rewrite_mac 1 => 00:aa:bb:00:00:00 +table_add send_frame rewrite_mac 2 => 00:aa:bb:00:00:01 +table_add forward set_dmac 10.0.0.10 => 00:04:00:00:00:00 +table_add forward set_dmac 10.0.1.10 => 00:04:00:00:00:01 +table_add ipv4_lpm set_nhop 10.0.0.10/32 => 10.0.0.10 1 +table_add ipv4_lpm set_nhop 10.0.1.10/32 => 10.0.1.10 2 +table_add drop_heavy_hitter heavy_hitter_drop 1 0 diff --git a/Teaching/Stanford_CS344_2018/simple_router.p4 b/Teaching/Stanford_CS344_2018/simple_router.p4 new file mode 100644 index 0000000..db9f0e5 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/simple_router.p4 @@ -0,0 +1,210 @@ +#include +#include + +#include "header.p4" +#include "parser.p4" + +const bit<16> MAX_ADDRESS = 0x1F; +const bit<16> THRESHOLD_COUNT = 8; + +register>(32w1) last_reset_time; +register>(32w32) hashtable_a0; +register>(32w32) hashtable_a1; +register>(32w32) hashtable_a2; +register>(32w32) hashtable_a3; +register>(32w32) hashtable_b0; +register>(32w32) hashtable_b1; +register>(32w32) hashtable_b2; +register>(32w32) hashtable_b3; +register>(32w1) is_a_active; + + +control egress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { + action rewrite_mac(bit<48> smac) { + hdr.ethernet.srcAddr = smac; + } + action egress_drop() { + mark_to_drop(); + } + table send_frame { + actions = { + rewrite_mac; + egress_drop; + NoAction; + } + key = { + standard_metadata.egress_port: exact; + } + size = 256; + default_action = NoAction(); + } + apply { + if (hdr.ipv4.isValid()) { + send_frame.apply(); + } + } +} + +control HashtableUpdate(in register> hashtable, + in HashAlgorithm algo, + in headers hdr, + inout bit<32> bytecount) { + + action update_hashtable() { + /* TODO + Use a hashfunction and calculate the corresponding address + of the count-min sketch based on its five-tuple (hdr.ipv4.srcAddr, + hdr.ipv4.dstAddr, hdr.ipv4.protocol, hdr.tcp.srcPort, hdr.tcp.dstPort) + Read the previous contents of that address, add the packet length to + the previous bytecount, update the register address and keep a + copy of the value in the metadata. + */ + } + + apply { + if (hdr.ipv4.isValid()) { + update_hashtable(); + } + } +} + +control HHD(inout headers hdr, + inout metadata meta, + inout standard_metadata_t standard_metadata) { + + + HashtableUpdate() update_hashtable_a0; + HashtableUpdate() update_hashtable_a1; + HashtableUpdate() update_hashtable_a2; + HashtableUpdate() update_hashtable_a3; + HashtableUpdate() update_hashtable_b0; + HashtableUpdate() update_hashtable_b1; + HashtableUpdate() update_hashtable_b2; + HashtableUpdate() update_hashtable_b3; + + action calculate_age() { + /* TODO + Read the last_reset_time register and calculate + how long has it been since last reset of sketch A based + on standard_metadata.ingress_global_timestamp. + Save the result in meta.hhd.filter_age. + */ + } + + action set_threshold(bit<32> threshold) { + /* TODO + Copy the threshlod to metamhhd.threshold + */ + } + + action set_filter() { + /* TODO + Check whether count-min sketch A is active + and set meta.hhd.is_a_active flag appropriately + */ + } + + action heavy_hitter_drop() { + mark_to_drop(); + } + + action decide_heavy_hitter() { + /* TODO + Based on whether A is active and the appropriate + meta.hhd.value_xx values, decide, whether + the packet belongs to a heavy hitter flow or not + and set meta.hhd.is_heavy_hitter flag. + */ + } + + + table threshold_table { + key = { + meta.hhd.filter_age : ternary; + } + + actions = { + set_threshold; + } + + size = THRESHOLD_COUNT; + } + + table drop_heavy_hitter { + key = { + meta.hhd.is_heavy_hitter : exact; + } + + actions = { + heavy_hitter_drop; + NoAction; + } + size = 2; + default_action = NoAction(); + } + + apply { + calculate_age(); + set_filter(); + threshold_table.apply(); + update_hashtable_a0.apply(hashtable_a0, HashAlgorithm.crc32, hdr, meta.hhd.value_a0); + update_hashtable_a1.apply(hashtable_a1, HashAlgorithm.crc32_custom, hdr, meta.hhd.value_a1); + update_hashtable_a2.apply(hashtable_a2, HashAlgorithm.crc16, hdr, meta.hhd.value_a2); + update_hashtable_a3.apply(hashtable_a3, HashAlgorithm.crc16_custom, hdr, meta.hhd.value_a3); + update_hashtable_b0.apply(hashtable_b0, HashAlgorithm.crc32, hdr, meta.hhd.value_b0); + update_hashtable_b1.apply(hashtable_b1, HashAlgorithm.crc32_custom, hdr, meta.hhd.value_b1); + update_hashtable_b2.apply(hashtable_b2, HashAlgorithm.crc16, hdr, meta.hhd.value_b2); + update_hashtable_b3.apply(hashtable_b3, HashAlgorithm.crc16_custom, hdr, meta.hhd.value_b3); + decide_heavy_hitter(); + drop_heavy_hitter.apply(); + } + +} + +control ingress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { + action ingress_drop() { + mark_to_drop(); + } + action set_nhop(bit<32> nhop_ipv4, bit<9> port) { + meta.ingress_metadata.nhop_ipv4 = nhop_ipv4; + standard_metadata.egress_spec = port; + hdr.ipv4.ttl = hdr.ipv4.ttl + 8w255; + } + action set_dmac(bit<48> dmac) { + hdr.ethernet.dstAddr = dmac; + } + table ipv4_lpm { + actions = { + ingress_drop; + set_nhop; + NoAction; + } + key = { + hdr.ipv4.dstAddr: lpm; + } + size = 1024; + default_action = NoAction(); + } + table forward { + actions = { + set_dmac; + ingress_drop; + NoAction; + } + key = { + meta.ingress_metadata.nhop_ipv4: exact; + } + size = 512; + default_action = NoAction(); + } + HHD() hhd; + apply { + if (hdr.ipv4.isValid()) { + ipv4_lpm.apply(); + forward.apply(); + hhd.apply(hdr, meta, standard_metadata); + } + } +} + +V1Switch(ParserImpl(), verifyChecksum(), ingress(), egress(), computeChecksum(), DeparserImpl()) main; diff --git a/Teaching/Stanford_CS344_2018/solution/simple_router.p4 b/Teaching/Stanford_CS344_2018/solution/simple_router.p4 new file mode 100644 index 0000000..5b75ea8 --- /dev/null +++ b/Teaching/Stanford_CS344_2018/solution/simple_router.p4 @@ -0,0 +1,222 @@ +#include +#include + +#include "header.p4" +#include "parser.p4" + +const bit<16> MAX_ADDRESS = 0x1F; +const bit<16> THRESHOLD_COUNT = 8; + +register>(32w1) last_reset_time; +register>(32w32) hashtable_a0; +register>(32w32) hashtable_a1; +register>(32w32) hashtable_a2; +register>(32w32) hashtable_a3; +register>(32w32) hashtable_b0; +register>(32w32) hashtable_b1; +register>(32w32) hashtable_b2; +register>(32w32) hashtable_b3; +register>(32w1) is_a_active; + + +control egress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { + action rewrite_mac(bit<48> smac) { + hdr.ethernet.srcAddr = smac; + } + action egress_drop() { + mark_to_drop(); + } + table send_frame { + actions = { + rewrite_mac; + egress_drop; + NoAction; + } + key = { + standard_metadata.egress_port: exact; + } + size = 256; + default_action = NoAction(); + } + apply { + if (hdr.ipv4.isValid()) { + send_frame.apply(); + } + } +} + +control HashtableUpdate(in register> hashtable, + in HashAlgorithm algo, + in headers hdr, + inout bit<32> bytecount) { + + action update_hashtable() { + bit<32> hashtable_address; + hash(hashtable_address, + algo, + 32w0, + { hdr.ipv4.srcAddr, + hdr.ipv4.dstAddr, + hdr.ipv4.protocol, + hdr.tcp.srcPort, + hdr.tcp.dstPort }, + MAX_ADDRESS); + hashtable.read(bytecount, hashtable_address); + bytecount = bytecount + (bit<32>)hdr.ipv4.totalLen; + hashtable.write(hashtable_address, bytecount); + + } + + apply { + if (hdr.ipv4.isValid()) { + update_hashtable(); + } + } +} + +control HHD(inout headers hdr, + inout metadata meta, + inout standard_metadata_t standard_metadata) { + + + HashtableUpdate() update_hashtable_a0; + HashtableUpdate() update_hashtable_a1; + HashtableUpdate() update_hashtable_a2; + HashtableUpdate() update_hashtable_a3; + HashtableUpdate() update_hashtable_b0; + HashtableUpdate() update_hashtable_b1; + HashtableUpdate() update_hashtable_b2; + HashtableUpdate() update_hashtable_b3; + + action calculate_age() { + last_reset_time.read(meta.hhd.filter_age, 32w0); + meta.hhd.filter_age = standard_metadata.ingress_global_timestamp - meta.hhd.filter_age; + } + + action set_threshold(bit<32> threshold) { + meta.hhd.threshold = threshold; + } + + action set_filter() { + is_a_active.read(meta.hhd.is_a_active, 32w0); + } + + action heavy_hitter_drop() { + mark_to_drop(); + } + + action decide_heavy_hitter() { + if (meta.hhd.is_a_active == 1w1) { + if (meta.hhd.value_a0 > meta.hhd.threshold && + meta.hhd.value_a1 > meta.hhd.threshold && + meta.hhd.value_a2 > meta.hhd.threshold && + meta.hhd.value_a3 > meta.hhd.threshold) { + + meta.hhd.is_heavy_hitter = 1w1; + } else { + meta.hhd.is_heavy_hitter = 1w0; + } + } else { + if (meta.hhd.value_b0 > meta.hhd.threshold && + meta.hhd.value_b1 > meta.hhd.threshold && + meta.hhd.value_b2 > meta.hhd.threshold && + meta.hhd.value_b3 > meta.hhd.threshold) { + + meta.hhd.is_heavy_hitter = 1w1; + } else { + meta.hhd.is_heavy_hitter = 1w0; + } + } + } + + + table threshold_table { + key = { + meta.hhd.filter_age : ternary; + } + + actions = { + set_threshold; + } + + size = THRESHOLD_COUNT; + } + + table drop_heavy_hitter { + key = { + meta.hhd.is_heavy_hitter : exact; + } + + actions = { + heavy_hitter_drop; + NoAction; + } + size = 2; + default_action = NoAction(); + } + + apply { + calculate_age(); + set_filter(); + threshold_table.apply(); + update_hashtable_a0.apply(hashtable_a0, HashAlgorithm.crc32, hdr, meta.hhd.value_a0); + update_hashtable_a1.apply(hashtable_a1, HashAlgorithm.crc32_custom, hdr, meta.hhd.value_a1); + update_hashtable_a2.apply(hashtable_a2, HashAlgorithm.crc16, hdr, meta.hhd.value_a2); + update_hashtable_a3.apply(hashtable_a3, HashAlgorithm.crc16_custom, hdr, meta.hhd.value_a3); + update_hashtable_b0.apply(hashtable_b0, HashAlgorithm.crc32, hdr, meta.hhd.value_b0); + update_hashtable_b1.apply(hashtable_b1, HashAlgorithm.crc32_custom, hdr, meta.hhd.value_b1); + update_hashtable_b2.apply(hashtable_b2, HashAlgorithm.crc16, hdr, meta.hhd.value_b2); + update_hashtable_b3.apply(hashtable_b3, HashAlgorithm.crc16_custom, hdr, meta.hhd.value_b3); + decide_heavy_hitter(); + drop_heavy_hitter.apply(); + } + +} + +control ingress(inout headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) { + action ingress_drop() { + mark_to_drop(); + } + action set_nhop(bit<32> nhop_ipv4, bit<9> port) { + meta.ingress_metadata.nhop_ipv4 = nhop_ipv4; + standard_metadata.egress_spec = port; + hdr.ipv4.ttl = hdr.ipv4.ttl + 8w255; + } + action set_dmac(bit<48> dmac) { + hdr.ethernet.dstAddr = dmac; + } + table ipv4_lpm { + actions = { + ingress_drop; + set_nhop; + NoAction; + } + key = { + hdr.ipv4.dstAddr: lpm; + } + size = 1024; + default_action = NoAction(); + } + table forward { + actions = { + set_dmac; + ingress_drop; + NoAction; + } + key = { + meta.ingress_metadata.nhop_ipv4: exact; + } + size = 512; + default_action = NoAction(); + } + HHD() hhd; + apply { + if (hdr.ipv4.isValid()) { + ipv4_lpm.apply(); + forward.apply(); + hhd.apply(hdr, meta, standard_metadata); + } + } +} + +V1Switch(ParserImpl(), verifyChecksum(), ingress(), egress(), computeChecksum(), DeparserImpl()) main; diff --git a/Teaching/utils/Makefile b/Teaching/utils/Makefile new file mode 100644 index 0000000..3483f08 --- /dev/null +++ b/Teaching/utils/Makefile @@ -0,0 +1,42 @@ +BUILD_DIR = build +PCAP_DIR = pcaps +LOG_DIR = logs + +TOPO = topology.json +P4C = p4c-bm2-ss +RUN_SCRIPT = ../utils/run_exercise.py + +source := $(wildcard *.p4) +outfile := $(source:.p4=.json) + +compiled_json := $(BUILD_DIR)/$(outfile) + +# Define NO_P4 to start BMv2 without a program +ifndef NO_P4 +run_args += -j $(compiled_json) +endif + +# Set BMV2_SWITCH_EXE to override the BMv2 target +ifdef BMV2_SWITCH_EXE +run_args += -b $(BMV2_SWITCH_EXE) +endif + +all: run + +run: build + sudo python $(RUN_SCRIPT) -t $(TOPO) $(run_args) + +stop: + sudo mn -c + +build: dirs $(compiled_json) + +$(BUILD_DIR)/%.json: %.p4 + $(P4C) --p4v 16 $(P4C_ARGS) -o $@ $< + +dirs: + mkdir -p $(BUILD_DIR) $(PCAP_DIR) $(LOG_DIR) + +clean: stop + rm -f *.pcap + rm -rf $(BUILD_DIR) $(PCAP_DIR) $(LOG_DIR) diff --git a/Teaching/utils/mininet/appcontroller.py b/Teaching/utils/mininet/appcontroller.py new file mode 100644 index 0000000..57d2fca --- /dev/null +++ b/Teaching/utils/mininet/appcontroller.py @@ -0,0 +1,93 @@ +import subprocess + +from shortest_path import ShortestPath + +class AppController: + + def __init__(self, manifest=None, target=None, topo=None, net=None, links=None): + self.manifest = manifest + self.target = target + self.conf = manifest['targets'][target] + self.topo = topo + self.net = net + self.links = links + + def read_entries(self, filename): + entries = [] + with open(filename, 'r') as f: + for line in f: + line = line.strip() + if line == '': continue + entries.append(line) + return entries + + def add_entries(self, thrift_port=9090, sw=None, entries=None): + assert entries + if sw: thrift_port = sw.thrift_port + + print '\n'.join(entries) + p = subprocess.Popen(['simple_switch_CLI', '--thrift-port', str(thrift_port)], stdin=subprocess.PIPE) + p.communicate(input='\n'.join(entries)) + + def read_register(self, register, idx, thrift_port=9090, sw=None): + if sw: thrift_port = sw.thrift_port + p = subprocess.Popen(['simple_switch_CLI', '--thrift-port', str(thrift_port)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate(input="register_read %s %d" % (register, idx)) + reg_val = filter(lambda l: ' %s[%d]' % (register, idx) in l, stdout.split('\n'))[0].split('= ', 1)[1] + return long(reg_val) + + def start(self): + shortestpath = ShortestPath(self.links) + entries = {} + for sw in self.topo.switches(): + entries[sw] = [] + if 'switches' in self.conf and sw in self.conf['switches'] and 'entries' in self.conf['switches'][sw]: + extra_entries = self.conf['switches'][sw]['entries'] + if type(extra_entries) == list: # array of entries + entries[sw] += extra_entries + else: # path to file that contains entries + entries[sw] += self.read_entries(extra_entries) + + for host_name in self.topo._host_links: + h = self.net.get(host_name) + for link in self.topo._host_links[host_name].values(): + sw = link['sw'] + iface = h.intfNames()[link['idx']] + # use mininet to set ip and mac to let it know the change + h.setIP(link['host_ip'], 24) + h.setMAC(link['host_mac']) + h.cmd('arp -i %s -s %s %s' % (iface, link['sw_ip'], link['sw_mac'])) + h.cmd('ethtool --offload %s rx off tx off' % iface) + h.cmd('ip route add %s dev %s' % (link['sw_ip'], iface)) + h.setDefaultRoute("via %s" % link['sw_ip']) + + for h in self.net.hosts: + h_link = self.topo._host_links[h.name].values()[0] + for sw in self.net.switches: + path = shortestpath.get(sw.name, h.name, exclude=lambda n: n[0]=='h') + if not path: continue + if not path[1][0] == 's': continue # next hop is a switch + sw_link = self.topo._sw_links[sw.name][path[1]] + + for h2 in self.net.hosts: + if h == h2: continue + path = shortestpath.get(h.name, h2.name, exclude=lambda n: n[0]=='h') + if not path: continue + h_link = self.topo._host_links[h.name][path[1]] + h2_link = self.topo._host_links[h2.name].values()[0] + h.cmd('ip route add %s via %s' % (h2_link['host_ip'], h_link['sw_ip'])) + + + print "**********" + print "Configuring entries in p4 tables" + for sw_name in entries: + print + print "Configuring switch... %s" % sw_name + sw = self.net.get(sw_name) + if entries[sw_name]: + self.add_entries(sw=sw, entries=entries[sw_name]) + print "Configuration complete." + print "**********" + + def stop(self): + pass diff --git a/Teaching/utils/mininet/apptopo.py b/Teaching/utils/mininet/apptopo.py new file mode 100644 index 0000000..3491a3d --- /dev/null +++ b/Teaching/utils/mininet/apptopo.py @@ -0,0 +1,70 @@ +from mininet.topo import Topo + +class AppTopo(Topo): + + def __init__(self, links, latencies={}, manifest=None, target=None, + log_dir="/tmp", bws={}, **opts): + Topo.__init__(self, **opts) + + nodes = sum(map(list, zip(*links)), []) + host_names = sorted(list(set(filter(lambda n: n[0] == 'h', nodes)))) + sw_names = sorted(list(set(filter(lambda n: n[0] == 's', nodes)))) + sw_ports = dict([(sw, []) for sw in sw_names]) + + self._host_links = {} + self._sw_links = dict([(sw, {}) for sw in sw_names]) + + for sw_name in sw_names: + self.addSwitch(sw_name, log_file="%s/%s.log" %(log_dir, sw_name)) + + for host_name in host_names: + host_num = int(host_name[1:]) + + self.addHost(host_name) + + self._host_links[host_name] = {} + host_links = filter(lambda l: l[0]==host_name or l[1]==host_name, links) + + sw_idx = 0 + for link in host_links: + sw = link[0] if link[0] != host_name else link[1] + sw_num = int(sw[1:]) + assert sw[0]=='s', "Hosts should be connected to switches, not " + str(sw) + host_ip = "10.0.%d.%d" % (sw_num, host_num) + host_mac = '00:00:00:00:%02x:%02x' % (sw_num, host_num) + delay_key = ''.join([host_name, sw]) + delay = latencies[delay_key] if delay_key in latencies else '0ms' + bw = bws[delay_key] if delay_key in bws else None + sw_ports[sw].append(host_name) + self._host_links[host_name][sw] = dict( + idx=sw_idx, + host_mac = host_mac, + host_ip = host_ip, + sw = sw, + sw_mac = "00:00:00:00:%02x:%02x" % (sw_num, host_num), + sw_ip = "10.0.%d.%d" % (sw_num, 254), + sw_port = sw_ports[sw].index(host_name)+1 + ) + self.addLink(host_name, sw, delay=delay, bw=bw, + addr1=host_mac, addr2=self._host_links[host_name][sw]['sw_mac']) + sw_idx += 1 + + for link in links: # only check switch-switch links + sw1, sw2 = link + if sw1[0] != 's' or sw2[0] != 's': continue + + delay_key = ''.join(sorted([sw1, sw2])) + delay = latencies[delay_key] if delay_key in latencies else '0ms' + bw = bws[delay_key] if delay_key in bws else None + + self.addLink(sw1, sw2, delay=delay, bw=bw)#, max_queue_size=10) + sw_ports[sw1].append(sw2) + sw_ports[sw2].append(sw1) + + sw1_num, sw2_num = int(sw1[1:]), int(sw2[1:]) + sw1_port = dict(mac="00:00:00:%02x:%02x:00" % (sw1_num, sw2_num), port=sw_ports[sw1].index(sw2)+1) + sw2_port = dict(mac="00:00:00:%02x:%02x:00" % (sw2_num, sw1_num), port=sw_ports[sw2].index(sw1)+1) + + self._sw_links[sw1][sw2] = [sw1_port, sw2_port] + self._sw_links[sw2][sw1] = [sw2_port, sw1_port] + diff --git a/Teaching/utils/mininet/multi_switch_mininet.py b/Teaching/utils/mininet/multi_switch_mininet.py new file mode 100755 index 0000000..0bb406f --- /dev/null +++ b/Teaching/utils/mininet/multi_switch_mininet.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python2 + +# 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. +# + +import signal +import os +import sys +import subprocess +import argparse +import json +import importlib +import re +from time import sleep + +from mininet.net import Mininet +from mininet.topo import Topo +from mininet.link import TCLink +from mininet.log import setLogLevel, info +from mininet.cli import CLI + +from p4_mininet import P4Switch, P4Host +import apptopo +import appcontroller + +parser = argparse.ArgumentParser(description='Mininet demo') +parser.add_argument('--behavioral-exe', help='Path to behavioral executable', + type=str, action="store", required=True) +parser.add_argument('--thrift-port', help='Thrift server port for table updates', + type=int, action="store", default=9090) +parser.add_argument('--bmv2-log', help='verbose messages in log file', action="store_true") +parser.add_argument('--cli', help="start the mininet cli", action="store_true") +parser.add_argument('--auto-control-plane', help='enable automatic control plane population', action="store_true") +parser.add_argument('--json', help='Path to JSON config file', + type=str, action="store", required=True) +parser.add_argument('--pcap-dump', help='Dump packets on interfaces to pcap files', + action="store_true") +parser.add_argument('--manifest', '-m', help='Path to manifest file', + type=str, action="store", required=True) +parser.add_argument('--target', '-t', help='Target in manifest file to run', + type=str, action="store", required=True) +parser.add_argument('--log-dir', '-l', help='Location to save output to', + type=str, action="store", required=True) +parser.add_argument('--cli-message', help='Message to print before starting CLI', + type=str, action="store", required=False, default=False) + + +args = parser.parse_args() + + +next_thrift_port = args.thrift_port + +def run_command(command): + return os.WEXITSTATUS(os.system(command)) + +def configureP4Switch(**switch_args): + class ConfiguredP4Switch(P4Switch): + def __init__(self, *opts, **kwargs): + global next_thrift_port + kwargs.update(switch_args) + kwargs['thrift_port'] = next_thrift_port + next_thrift_port += 1 + P4Switch.__init__(self, *opts, **kwargs) + return ConfiguredP4Switch + + +def main(): + + with open(args.manifest, 'r') as f: + manifest = json.load(f) + + conf = manifest['targets'][args.target] + params = conf['parameters'] if 'parameters' in conf else {} + + os.environ.update(dict(map(lambda (k,v): (k, str(v)), params.iteritems()))) + + def formatParams(s): + for param in params: + s = re.sub('\$'+param+'(\W|$)', str(params[param]) + r'\1', s) + s = s.replace('${'+param+'}', str(params[param])) + return s + + AppTopo = apptopo.AppTopo + AppController = appcontroller.AppController + + if 'topo_module' in conf: + sys.path.insert(0, os.path.dirname(args.manifest)) + topo_module = importlib.import_module(conf['topo_module']) + AppTopo = topo_module.CustomAppTopo + + if 'controller_module' in conf: + sys.path.insert(0, os.path.dirname(args.manifest)) + controller_module = importlib.import_module(conf['controller_module']) + AppController = controller_module.CustomAppController + + if not os.path.isdir(args.log_dir): + if os.path.exists(args.log_dir): raise Exception('Log dir exists and is not a dir') + os.mkdir(args.log_dir) + os.environ['P4APP_LOGDIR'] = args.log_dir + + + links = [l[:2] for l in conf['links']] + latencies = dict([(''.join(sorted(l[:2])), l[2]) for l in conf['links'] if len(l)>=3]) + bws = dict([(''.join(sorted(l[:2])), l[3]) for l in conf['links'] if len(l)>=4]) + + for host_name in sorted(conf['hosts'].keys()): + host = conf['hosts'][host_name] + if 'latency' not in host: continue + for a, b in links: + if a != host_name and b != host_name: continue + other = a if a != host_name else b + latencies[host_name+other] = host['latency'] + + for l in latencies: + if isinstance(latencies[l], (str, unicode)): + latencies[l] = formatParams(latencies[l]) + else: + latencies[l] = str(latencies[l]) + "ms" + + bmv2_log = args.bmv2_log or ('bmv2_log' in conf and conf['bmv2_log']) + pcap_dump = args.pcap_dump or ('pcap_dump' in conf and conf['pcap_dump']) + + topo = AppTopo(links, latencies, manifest=manifest, target=args.target, + log_dir=args.log_dir, bws=bws) + switchClass = configureP4Switch( + sw_path=args.behavioral_exe, + json_path=args.json, + log_console=bmv2_log, + pcap_dump=pcap_dump) + net = Mininet(topo = topo, + link = TCLink, + host = P4Host, + switch = switchClass, + controller = None) + net.start() + + sleep(1) + + controller = None + if args.auto_control_plane or 'controller_module' in conf: + controller = AppController(manifest=manifest, target=args.target, + topo=topo, net=net, links=links) + controller.start() + + + for h in net.hosts: + h.describe() + + if args.cli_message is not None: + with open(args.cli_message, 'r') as message_file: + print message_file.read() + + if args.cli or ('cli' in conf and conf['cli']): + CLI(net) + + stdout_files = dict() + return_codes = [] + host_procs = [] + + + def formatCmd(cmd): + for h in net.hosts: + cmd = cmd.replace(h.name, h.defaultIntf().updateIP()) + return cmd + + def _wait_for_exit(p, host): + print p.communicate() + if p.returncode is None: + p.wait() + print p.communicate() + return_codes.append(p.returncode) + if host_name in stdout_files: + stdout_files[host_name].flush() + stdout_files[host_name].close() + + print '\n'.join(map(lambda (k,v): "%s: %s"%(k,v), params.iteritems())) + '\n' + + for host_name in sorted(conf['hosts'].keys()): + host = conf['hosts'][host_name] + if 'cmd' not in host: continue + + h = net.get(host_name) + stdout_filename = os.path.join(args.log_dir, h.name + '.stdout') + stdout_files[h.name] = open(stdout_filename, 'w') + cmd = formatCmd(host['cmd']) + print h.name, cmd + p = h.popen(cmd, stdout=stdout_files[h.name], shell=True, preexec_fn=os.setpgrp) + if 'startup_sleep' in host: sleep(host['startup_sleep']) + + if 'wait' in host and host['wait']: + _wait_for_exit(p, host_name) + else: + host_procs.append((p, host_name)) + + for p, host_name in host_procs: + if 'wait' in conf['hosts'][host_name] and conf['hosts'][host_name]['wait']: + _wait_for_exit(p, host_name) + + + for p, host_name in host_procs: + if 'wait' in conf['hosts'][host_name] and conf['hosts'][host_name]['wait']: + continue + if p.returncode is None: + run_command('pkill -INT -P %d' % p.pid) + sleep(0.2) + rc = run_command('pkill -0 -P %d' % p.pid) # check if it's still running + if rc == 0: # the process group is still running, send TERM + sleep(1) # give it a little more time to exit gracefully + run_command('pkill -TERM -P %d' % p.pid) + _wait_for_exit(p, host_name) + + if 'after' in conf and 'cmd' in conf['after']: + cmds = conf['after']['cmd'] if type(conf['after']['cmd']) == list else [conf['after']['cmd']] + for cmd in cmds: + os.system(cmd) + + if controller: controller.stop() + + net.stop() + +# if bmv2_log: +# os.system('bash -c "cp /tmp/p4s.s*.log \'%s\'"' % args.log_dir) +# if pcap_dump: +# os.system('bash -c "cp *.pcap \'%s\'"' % args.log_dir) + + bad_codes = [rc for rc in return_codes if rc != 0] + if len(bad_codes): sys.exit(1) + +if __name__ == '__main__': + setLogLevel( 'info' ) + main() diff --git a/Teaching/utils/mininet/p4_mininet.py b/Teaching/utils/mininet/p4_mininet.py new file mode 100644 index 0000000..8abe79f --- /dev/null +++ b/Teaching/utils/mininet/p4_mininet.py @@ -0,0 +1,161 @@ +# 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. +# + +from mininet.net import Mininet +from mininet.node import Switch, Host +from mininet.log import setLogLevel, info, error, debug +from mininet.moduledeps import pathCheck +from sys import exit +from time import sleep +import os +import tempfile +import socket + +class P4Host(Host): + def config(self, **params): + r = super(P4Host, self).config(**params) + + for off in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload %s %s off" % (self.defaultIntf().name, off) + self.cmd(cmd) + + # disable IPv6 + self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1") + + return r + + def describe(self, sw_addr=None, sw_mac=None): + print "**********" + print "Network configuration for: %s" % self.name + print "Default interface: %s\t%s\t%s" %( + self.defaultIntf().name, + self.defaultIntf().IP(), + self.defaultIntf().MAC() + ) + if sw_addr is not None or sw_mac is not None: + print "Default route to switch: %s (%s)" % (sw_addr, sw_mac) + print "**********" + +class P4Switch(Switch): + """P4 virtual switch""" + device_id = 0 + + def __init__(self, name, sw_path = None, json_path = None, + log_file = None, + thrift_port = None, + pcap_dump = False, + log_console = False, + verbose = False, + device_id = None, + enable_debugger = False, + **kwargs): + Switch.__init__(self, name, **kwargs) + assert(sw_path) + assert(json_path) + # make sure that the provided sw_path is valid + pathCheck(sw_path) + # make sure that the provided JSON file exists + if not os.path.isfile(json_path): + error("Invalid JSON file.\n") + exit(1) + self.sw_path = sw_path + self.json_path = json_path + self.verbose = verbose + self.log_file = log_file + if self.log_file is None: + self.log_file = "/tmp/p4s.{}.log".format(self.name) + self.output = open(self.log_file, 'w') + self.thrift_port = thrift_port + self.pcap_dump = pcap_dump + self.enable_debugger = enable_debugger + self.log_console = log_console + if device_id is not None: + self.device_id = device_id + P4Switch.device_id = max(P4Switch.device_id, device_id) + else: + self.device_id = P4Switch.device_id + P4Switch.device_id += 1 + self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) + + @classmethod + def setup(cls): + pass + + def check_switch_started(self, pid): + """While the process is running (pid exists), we check if the Thrift + server has been started. If the Thrift server is ready, we assume that + the switch was started successfully. This is only reliable if the Thrift + server is started at the end of the init process""" + while True: + if not os.path.exists(os.path.join("/proc", str(pid))): + return False + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(0.5) + result = sock.connect_ex(("localhost", self.thrift_port)) + if result == 0: + return True + + def start(self, controllers): + "Start up a new P4 switch" + info("Starting P4 switch {}.\n".format(self.name)) + args = [self.sw_path] + for port, intf in self.intfs.items(): + if not intf.IP(): + args.extend(['-i', str(port) + "@" + intf.name]) + if self.pcap_dump: + args.append("--pcap") + # args.append("--useFiles") + if self.thrift_port: + args.extend(['--thrift-port', str(self.thrift_port)]) + if self.nanomsg: + args.extend(['--nanolog', self.nanomsg]) + args.extend(['--device-id', str(self.device_id)]) + P4Switch.device_id += 1 + args.append(self.json_path) + if self.enable_debugger: + args.append("--debugger") + if self.log_console: + args.append("--log-console") + info(' '.join(args) + "\n") + + pid = None + with tempfile.NamedTemporaryFile() as f: + # self.cmd(' '.join(args) + ' > /dev/null 2>&1 &') + self.cmd(' '.join(args) + ' >' + self.log_file + ' 2>&1 & echo $! >> ' + f.name) + pid = int(f.read()) + debug("P4 switch {} PID is {}.\n".format(self.name, pid)) + sleep(1) + if not self.check_switch_started(pid): + error("P4 switch {} did not start correctly." + "Check the switch log file.\n".format(self.name)) + exit(1) + info("P4 switch {} has been started.\n".format(self.name)) + + def stop(self): + "Terminate P4 switch." + self.output.flush() + self.cmd('kill %' + self.sw_path) + self.cmd('wait') + self.deleteIntfs() + + def attach(self, intf): + "Connect a data port" + assert(0) + + def detach(self, intf): + "Disconnect a data port" + assert(0) diff --git a/Teaching/utils/mininet/shortest_path.py b/Teaching/utils/mininet/shortest_path.py new file mode 100644 index 0000000..971b1b4 --- /dev/null +++ b/Teaching/utils/mininet/shortest_path.py @@ -0,0 +1,78 @@ +class ShortestPath: + + def __init__(self, edges=[]): + self.neighbors = {} + for edge in edges: + self.addEdge(*edge) + + def addEdge(self, a, b): + if a not in self.neighbors: self.neighbors[a] = [] + if b not in self.neighbors[a]: self.neighbors[a].append(b) + + if b not in self.neighbors: self.neighbors[b] = [] + if a not in self.neighbors[b]: self.neighbors[b].append(a) + + def get(self, a, b, exclude=lambda node: False): + # Shortest path from a to b + return self._recPath(a, b, [], exclude) + + def _recPath(self, a, b, visited, exclude): + if a == b: return [a] + new_visited = visited + [a] + paths = [] + for neighbor in self.neighbors[a]: + if neighbor in new_visited: continue + if exclude(neighbor) and neighbor != b: continue + path = self._recPath(neighbor, b, new_visited, exclude) + if path: paths.append(path) + + paths.sort(key=len) + return [a] + paths[0] if len(paths) else None + +if __name__ == '__main__': + + edges = [ + (1, 2), + (1, 3), + (1, 5), + (2, 4), + (3, 4), + (3, 5), + (3, 6), + (4, 6), + (5, 6), + (7, 8) + + ] + sp = ShortestPath(edges) + + assert sp.get(1, 1) == [1] + assert sp.get(2, 2) == [2] + + assert sp.get(1, 2) == [1, 2] + assert sp.get(2, 1) == [2, 1] + + assert sp.get(1, 3) == [1, 3] + assert sp.get(3, 1) == [3, 1] + + assert sp.get(4, 6) == [4, 6] + assert sp.get(6, 4) == [6, 4] + + assert sp.get(2, 6) == [2, 4, 6] + assert sp.get(6, 2) == [6, 4, 2] + + assert sp.get(1, 6) in [[1, 3, 6], [1, 5, 6]] + assert sp.get(6, 1) in [[6, 3, 1], [6, 5, 1]] + + assert sp.get(2, 5) == [2, 1, 5] + assert sp.get(5, 2) == [5, 1, 2] + + assert sp.get(4, 5) in [[4, 3, 5], [4, 6, 5]] + assert sp.get(5, 4) in [[5, 3, 4], [6, 6, 4]] + + assert sp.get(7, 8) == [7, 8] + assert sp.get(8, 7) == [8, 7] + + assert sp.get(1, 7) == None + assert sp.get(7, 2) == None + diff --git a/Teaching/utils/mininet/single_switch_mininet.py b/Teaching/utils/mininet/single_switch_mininet.py new file mode 100755 index 0000000..e2e7636 --- /dev/null +++ b/Teaching/utils/mininet/single_switch_mininet.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python2 + +# 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. +# + +from mininet.net import Mininet +from mininet.topo import Topo +from mininet.log import setLogLevel, info +from mininet.cli import CLI + +from p4_mininet import P4Switch, P4Host + +import argparse +from subprocess import PIPE, Popen +from time import sleep + +parser = argparse.ArgumentParser(description='Mininet demo') +parser.add_argument('--behavioral-exe', help='Path to behavioral executable', + type=str, action="store", required=True) +parser.add_argument('--thrift-port', help='Thrift server port for table updates', + type=int, action="store", default=9090) +parser.add_argument('--num-hosts', help='Number of hosts to connect to switch', + type=int, action="store", default=2) +parser.add_argument('--mode', choices=['l2', 'l3'], type=str, default='l3') +parser.add_argument('--json', help='Path to JSON config file', + type=str, action="store", required=True) +parser.add_argument('--log-file', help='Path to write the switch log file', + type=str, action="store", required=False) +parser.add_argument('--pcap-dump', help='Dump packets on interfaces to pcap files', + type=str, action="store", required=False, default=False) +parser.add_argument('--switch-config', help='simple_switch_CLI script to configure switch', + type=str, action="store", required=False, default=False) +parser.add_argument('--cli-message', help='Message to print before starting CLI', + type=str, action="store", required=False, default=False) + +args = parser.parse_args() + + +class SingleSwitchTopo(Topo): + "Single switch connected to n (< 256) hosts." + def __init__(self, sw_path, json_path, log_file, + thrift_port, pcap_dump, n, **opts): + # Initialize topology and default options + Topo.__init__(self, **opts) + + switch = self.addSwitch('s1', + sw_path = sw_path, + json_path = json_path, + log_console = True, + log_file = log_file, + thrift_port = thrift_port, + enable_debugger = False, + pcap_dump = pcap_dump) + + for h in xrange(n): + host = self.addHost('h%d' % (h + 1), + ip = "10.0.%d.10/24" % h, + mac = '00:04:00:00:00:%02x' %h) + print "Adding host", str(host) + self.addLink(host, switch) + +def main(): + num_hosts = args.num_hosts + mode = args.mode + + topo = SingleSwitchTopo(args.behavioral_exe, + args.json, + args.log_file, + args.thrift_port, + args.pcap_dump, + num_hosts) + net = Mininet(topo = topo, + host = P4Host, + switch = P4Switch, + controller = None) + net.start() + + + sw_mac = ["00:aa:bb:00:00:%02x" % n for n in xrange(num_hosts)] + + sw_addr = ["10.0.%d.1" % n for n in xrange(num_hosts)] + + for n in xrange(num_hosts): + h = net.get('h%d' % (n + 1)) + if mode == "l2": + h.setDefaultRoute("dev %s" % h.defaultIntf().name) + else: + h.setARP(sw_addr[n], sw_mac[n]) + h.setDefaultRoute("dev %s via %s" % (h.defaultIntf().name, sw_addr[n])) + + for n in xrange(num_hosts): + h = net.get('h%d' % (n + 1)) + h.describe(sw_addr[n], sw_mac[n]) + + sleep(1) + + if args.switch_config is not None: + print + print "Reading switch configuration script:", args.switch_config + with open(args.switch_config, 'r') as config_file: + switch_config = config_file.read() + + print "Configuring switch..." + proc = Popen(["simple_switch_CLI"], stdin=PIPE) + proc.communicate(input=switch_config) + + print "Configuration complete." + print + + print "Ready !" + + if args.cli_message is not None: + with open(args.cli_message, 'r') as message_file: + print message_file.read() + + CLI( net ) + net.stop() + +if __name__ == '__main__': + setLogLevel( 'info' ) + main() diff --git a/Teaching/utils/netstat.py b/Teaching/utils/netstat.py new file mode 100644 index 0000000..bb12ffd --- /dev/null +++ b/Teaching/utils/netstat.py @@ -0,0 +1,21 @@ +# 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. +# + +import psutil +def check_listening_on_port(port): + for c in psutil.net_connections(kind='inet'): + if c.status == 'LISTEN' and c.laddr[1] == port: + return True + return False diff --git a/Teaching/utils/p4_mininet.py b/Teaching/utils/p4_mininet.py new file mode 100644 index 0000000..b7fbbcd --- /dev/null +++ b/Teaching/utils/p4_mininet.py @@ -0,0 +1,162 @@ +# 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. +# + +from mininet.net import Mininet +from mininet.node import Switch, Host +from mininet.log import setLogLevel, info, error, debug +from mininet.moduledeps import pathCheck +from sys import exit +import os +import tempfile +import socket +from time import sleep + +from netstat import check_listening_on_port + +SWITCH_START_TIMEOUT = 10 # seconds + +class P4Host(Host): + def config(self, **params): + r = super(Host, self).config(**params) + + self.defaultIntf().rename("eth0") + + for off in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload eth0 %s off" % off + self.cmd(cmd) + + # disable IPv6 + self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1") + + return r + + def describe(self): + print "**********" + print self.name + print "default interface: %s\t%s\t%s" %( + self.defaultIntf().name, + self.defaultIntf().IP(), + self.defaultIntf().MAC() + ) + print "**********" + +class P4Switch(Switch): + """P4 virtual switch""" + device_id = 0 + + def __init__(self, name, sw_path = None, json_path = None, + thrift_port = None, + pcap_dump = False, + log_console = False, + verbose = False, + device_id = None, + enable_debugger = False, + **kwargs): + Switch.__init__(self, name, **kwargs) + assert(sw_path) + assert(json_path) + # make sure that the provided sw_path is valid + pathCheck(sw_path) + # make sure that the provided JSON file exists + if not os.path.isfile(json_path): + error("Invalid JSON file.\n") + exit(1) + self.sw_path = sw_path + self.json_path = json_path + self.verbose = verbose + logfile = "/tmp/p4s.{}.log".format(self.name) + self.output = open(logfile, 'w') + self.thrift_port = thrift_port + if check_listening_on_port(self.thrift_port): + error('%s cannot bind port %d because it is bound by another process\n' % (self.name, self.grpc_port)) + exit(1) + self.pcap_dump = pcap_dump + self.enable_debugger = enable_debugger + self.log_console = log_console + if device_id is not None: + self.device_id = device_id + P4Switch.device_id = max(P4Switch.device_id, device_id) + else: + self.device_id = P4Switch.device_id + P4Switch.device_id += 1 + self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) + + @classmethod + def setup(cls): + pass + + def check_switch_started(self, pid): + """While the process is running (pid exists), we check if the Thrift + server has been started. If the Thrift server is ready, we assume that + the switch was started successfully. This is only reliable if the Thrift + server is started at the end of the init process""" + while True: + if not os.path.exists(os.path.join("/proc", str(pid))): + return False + if check_listening_on_port(self.thrift_port): + return True + sleep(0.5) + + def start(self, controllers): + "Start up a new P4 switch" + info("Starting P4 switch {}.\n".format(self.name)) + args = [self.sw_path] + for port, intf in self.intfs.items(): + if not intf.IP(): + args.extend(['-i', str(port) + "@" + intf.name]) + if self.pcap_dump: + args.append("--pcap") + # args.append("--useFiles") + if self.thrift_port: + args.extend(['--thrift-port', str(self.thrift_port)]) + if self.nanomsg: + args.extend(['--nanolog', self.nanomsg]) + args.extend(['--device-id', str(self.device_id)]) + P4Switch.device_id += 1 + args.append(self.json_path) + if self.enable_debugger: + args.append("--debugger") + if self.log_console: + args.append("--log-console") + logfile = "/tmp/p4s.{}.log".format(self.name) + info(' '.join(args) + "\n") + + pid = None + with tempfile.NamedTemporaryFile() as f: + # self.cmd(' '.join(args) + ' > /dev/null 2>&1 &') + self.cmd(' '.join(args) + ' >' + logfile + ' 2>&1 & echo $! >> ' + f.name) + pid = int(f.read()) + debug("P4 switch {} PID is {}.\n".format(self.name, pid)) + if not self.check_switch_started(pid): + error("P4 switch {} did not start correctly.\n".format(self.name)) + exit(1) + info("P4 switch {} has been started.\n".format(self.name)) + + def stop(self): + "Terminate P4 switch." + self.output.flush() + self.cmd('kill %' + self.sw_path) + self.cmd('wait') + self.deleteIntfs() + + def attach(self, intf): + "Connect a data port" + assert(0) + + def detach(self, intf): + "Disconnect a data port" + assert(0) diff --git a/Teaching/utils/p4apprunner.py b/Teaching/utils/p4apprunner.py new file mode 100755 index 0000000..36b9eea --- /dev/null +++ b/Teaching/utils/p4apprunner.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python2 +# 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. + +from __future__ import print_function + +import argparse +from collections import OrderedDict +import json +import os +import sys +import tarfile + +parser = argparse.ArgumentParser(description='p4apprunner') +parser.add_argument('--build-dir', help='Directory to build in.', + type=str, action='store', required=False, default='/tmp') +parser.add_argument('--quiet', help='Suppress log messages.', + action='store_true', required=False, default=False) +parser.add_argument('--manifest', help='Path to manifest file.', + type=str, action='store', required=False, default='./p4app.json') +parser.add_argument('app', help='.p4app package to run.', type=str) +parser.add_argument('target', help=('Target to run. Defaults to the first target ' + 'in the package.'), + nargs='?', type=str) + +args = parser.parse_args() + +def log(*items): + if args.quiet != True: + print(*items) + +def log_error(*items): + print(*items, file=sys.stderr) + +def run_command(command): + log('>', command) + return os.WEXITSTATUS(os.system(command)) + +class Manifest: + def __init__(self, program_file, language, target, target_config): + self.program_file = program_file + self.language = language + self.target = target + self.target_config = target_config + +def read_manifest(manifest_file): + manifest = json.load(manifest_file, object_pairs_hook=OrderedDict) + + if 'program' not in manifest: + log_error('No program defined in manifest.') + sys.exit(1) + program_file = manifest['program'] + + if 'language' not in manifest: + log_error('No language defined in manifest.') + sys.exit(1) + language = manifest['language'] + + if 'targets' not in manifest or len(manifest['targets']) < 1: + log_error('No targets defined in manifest.') + sys.exit(1) + + if args.target is not None: + chosen_target = args.target + elif 'default-target' in manifest: + chosen_target = manifest['default-target'] + else: + chosen_target = manifest['targets'].keys()[0] + + if chosen_target not in manifest['targets']: + log_error('Target not found in manifest:', chosen_target) + sys.exit(1) + + return Manifest(program_file, language, chosen_target, manifest['targets'][chosen_target]) + + +def run_compile_bmv2(manifest): + if 'run-before-compile' in manifest.target_config: + commands = manifest.target_config['run-before-compile'] + if not isinstance(commands, list): + log_error('run-before-compile should be a list:', commands) + sys.exit(1) + for command in commands: + run_command(command) + + compiler_args = [] + + if manifest.language == 'p4-14': + compiler_args.append('--p4v 14') + elif manifest.language == 'p4-16': + compiler_args.append('--p4v 16') + else: + log_error('Unknown language:', manifest.language) + sys.exit(1) + + if 'compiler-flags' in manifest.target_config: + flags = manifest.target_config['compiler-flags'] + if not isinstance(flags, list): + log_error('compiler-flags should be a list:', flags) + sys.exit(1) + compiler_args.extend(flags) + + # Compile the program. + output_file = manifest.program_file + '.json' + compiler_args.append('"%s"' % manifest.program_file) + compiler_args.append('-o "%s"' % output_file) + rv = run_command('p4c-bm2-ss %s' % ' '.join(compiler_args)) + + if 'run-after-compile' in manifest.target_config: + commands = manifest.target_config['run-after-compile'] + if not isinstance(commands, list): + log_error('run-after-compile should be a list:', commands) + sys.exit(1) + for command in commands: + run_command(command) + + if rv != 0: + log_error('Compile failed.') + sys.exit(1) + + return output_file + +def run_mininet(manifest): + output_file = run_compile_bmv2(manifest) + + # Run the program using the BMV2 Mininet simple switch. + switch_args = [] + + # We'll place the switch's log file in current (build) folder. + cwd = os.getcwd() + log_file = os.path.join(cwd, manifest.program_file + '.log') + print ("*** Log file %s" % log_file) + switch_args.append('--log-file "%s"' % log_file) + + pcap_dir = os.path.join(cwd) + print ("*** Pcap folder %s" % pcap_dir) + switch_args.append('--pcap-dump "%s" '% pcap_dir) + + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + message_file = 'mininet_message.txt' + with open(message_file, 'w') as message: + + print(file=message) + print('======================================================================', + file=message) + print('Welcome to the BMV2 Mininet CLI!', file=message) + print('======================================================================', + file=message) + print('Your P4 program is installed into the BMV2 software switch', file=message) + print('and your initial configuration is loaded. You can interact', file=message) + print('with the network using the mininet CLI below.', file=message) + print(file=message) + print('To inspect or change the switch configuration, connect to', file=message) + print('its CLI from your host operating system using this command:', file=message) + print(' simple_switch_CLI', file=message) + print(file=message) + print('To view the switch log, run this command from your host OS:', file=message) + print(' tail -f %s' % log_file, file=message) + print(file=message) + print('To view the switch output pcap, check the pcap files in %s:' % pcap_dir, file=message) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap', file=message) + print(file=message) +# print('To run the switch debugger, run this command from your host OS:', file=message) +# print(' bm_p4dbg' , file=message) +# print(file=message) + + switch_args.append('--cli-message "%s"' % message_file) + + if 'num-hosts' in manifest.target_config: + switch_args.append('--num-hosts %s' % manifest.target_config['num-hosts']) + + if 'switch-config' in manifest.target_config: + switch_args.append('--switch-config "%s"' % manifest.target_config['switch-config']) + + switch_args.append('--behavioral-exe "%s"' % 'simple_switch') + switch_args.append('--json "%s"' % output_file) + + program = '"%s/mininet/single_switch_mininet.py"' % sys.path[0] + return run_command('python2 %s %s' % (program, ' '.join(switch_args))) + +def run_multiswitch(manifest): + output_file = run_compile_bmv2(manifest) + + script_args = [] + cwd = os.getcwd() + log_dir = os.path.join(cwd, cwd + '/logs') + print ("*** Log directory %s" % log_dir) + script_args.append('--log-dir "%s"' % log_dir) + pcap_dir = os.path.join(cwd) + print ("*** Pcap directory %s" % cwd) + script_args.append('--manifest "%s"' % args.manifest) + script_args.append('--target "%s"' % manifest.target) + if 'auto-control-plane' in manifest.target_config and manifest.target_config['auto-control-plane']: + script_args.append('--auto-control-plane' ) + script_args.append('--behavioral-exe "%s"' % 'simple_switch') + script_args.append('--json "%s"' % output_file) + #script_args.append('--cli') + + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + message_file = 'mininet_message.txt' + with open(message_file, 'w') as message: + + print(file=message) + print('======================================================================', + file=message) + print('Welcome to the BMV2 Mininet CLI!', file=message) + print('======================================================================', + file=message) + print('Your P4 program is installed into the BMV2 software switch', file=message) + print('and your initial configuration is loaded. You can interact', file=message) + print('with the network using the mininet CLI below.', file=message) + print(file=message) + print('To inspect or change the switch configuration, connect to', file=message) + print('its CLI from your host operating system using this command:', file=message) + print(' simple_switch_CLI --thrift-port ', file=message) + print(file=message) + print('To view a switch log, run this command from your host OS:', file=message) + print(' tail -f %s/.log' % log_dir, file=message) + print(file=message) + print('To view the switch output pcap, check the pcap files in %s:' % pcap_dir, file=message) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap', file=message) + print(file=message) +# print('To run the switch debugger, run this command from your host OS:', file=message) +# print(' bm_p4dbg' , file=message) +# print(file=message) + + script_args.append('--cli-message "%s"' % message_file) + + program = '"%s/mininet/multi_switch_mininet.py"' % sys.path[0] + return run_command('python2 %s %s' % (program, ' '.join(script_args))) + +def run_stf(manifest): + output_file = run_compile_bmv2(manifest) + + if not 'test' in manifest.target_config: + log_error('No STF test file provided.') + sys.exit(1) + stf_file = manifest.target_config['test'] + + # Run the program using the BMV2 STF interpreter. + stf_args = [] + stf_args.append('-v') + stf_args.append(os.path.join(args.build_dir, output_file)) + stf_args.append(os.path.join(args.build_dir, stf_file)) + + program = '"%s/stf/bmv2stf.py"' % sys.path[0] + rv = run_command('python2 %s %s' % (program, ' '.join(stf_args))) + if rv != 0: + sys.exit(1) + return rv + +def run_custom(manifest): + output_file = run_compile_bmv2(manifest) + python_path = 'PYTHONPATH=$PYTHONPATH:/scripts/mininet/' + script_args = [] + script_args.append('--behavioral-exe "%s"' % 'simple_switch') + script_args.append('--json "%s"' % output_file) + script_args.append('--cli "%s"' % 'simple_switch_CLI') + if not 'program' in manifest.target_config: + log_error('No mininet program file provided.') + sys.exit(1) + program = manifest.target_config['program'] + rv = run_command('%s python2 %s %s' % (python_path, program, ' '.join(script_args))) + + if rv != 0: + sys.exit(1) + return rv + +def main(): + log('Entering build directory.') + os.chdir(args.build_dir) + + # A '.p4app' package is really just a '.tar.gz' archive. Extract it so we + # can process its contents. + log('Extracting package.') + tar = tarfile.open(args.app) + tar.extractall() + tar.close() + + log('Reading package manifest.') + with open(args.manifest, 'r') as manifest_file: + manifest = read_manifest(manifest_file) + + # Dispatch to the backend implementation for this target. + backend = manifest.target + if 'use' in manifest.target_config: + backend = manifest.target_config['use'] + + if backend == 'mininet': + rc = run_mininet(manifest) + elif backend == 'multiswitch': + rc = run_multiswitch(manifest) + elif backend == 'stf': + rc = run_stf(manifest) + elif backend == 'custom': + rc = run_custom(manifest) + elif backend == 'compile-bmv2': + run_compile_bmv2(manifest) + rc = 0 + else: + log_error('Target specifies unknown backend:', backend) + sys.exit(1) + + sys.exit(rc) + +if __name__ == '__main__': + main() diff --git a/Teaching/utils/p4runtime_switch.py b/Teaching/utils/p4runtime_switch.py new file mode 100644 index 0000000..df919ed --- /dev/null +++ b/Teaching/utils/p4runtime_switch.py @@ -0,0 +1,122 @@ +# Copyright 2017-present Barefoot Networks, Inc. +# 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. +# + +import sys, os, tempfile, socket +from time import sleep + +from mininet.node import Switch +from mininet.moduledeps import pathCheck +from mininet.log import info, error, debug + +from p4_mininet import P4Switch, SWITCH_START_TIMEOUT +from netstat import check_listening_on_port + +class P4RuntimeSwitch(P4Switch): + "BMv2 switch with gRPC support" + next_grpc_port = 50051 + + def __init__(self, name, sw_path = None, json_path = None, + grpc_port = None, + pcap_dump = False, + log_console = False, + verbose = False, + device_id = None, + enable_debugger = False, + **kwargs): + Switch.__init__(self, name, **kwargs) + assert (sw_path) + self.sw_path = sw_path + # make sure that the provided sw_path is valid + pathCheck(sw_path) + + if json_path is not None: + # make sure that the provided JSON file exists + if not os.path.isfile(json_path): + error("Invalid JSON file.\n") + exit(1) + self.json_path = json_path + else: + self.json_path = None + + if grpc_port is not None: + self.grpc_port = grpc_port + else: + self.grpc_port = P4RuntimeSwitch.next_grpc_port + P4RuntimeSwitch.next_grpc_port += 1 + + if check_listening_on_port(self.grpc_port): + error('%s cannot bind port %d because it is bound by another process\n' % (self.name, self.grpc_port)) + exit(1) + + self.verbose = verbose + logfile = "/tmp/p4s.{}.log".format(self.name) + self.output = open(logfile, 'w') + self.pcap_dump = pcap_dump + self.enable_debugger = enable_debugger + self.log_console = log_console + if device_id is not None: + self.device_id = device_id + P4Switch.device_id = max(P4Switch.device_id, device_id) + else: + self.device_id = P4Switch.device_id + P4Switch.device_id += 1 + self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) + + + def check_switch_started(self, pid): + for _ in range(SWITCH_START_TIMEOUT * 2): + if not os.path.exists(os.path.join("/proc", str(pid))): + return False + if check_listening_on_port(self.grpc_port): + return True + sleep(0.5) + + def start(self, controllers): + info("Starting P4 switch {}.\n".format(self.name)) + args = [self.sw_path] + for port, intf in self.intfs.items(): + if not intf.IP(): + args.extend(['-i', str(port) + "@" + intf.name]) + if self.pcap_dump: + args.append("--pcap") + if self.nanomsg: + args.extend(['--nanolog', self.nanomsg]) + args.extend(['--device-id', str(self.device_id)]) + P4Switch.device_id += 1 + if self.json_path: + args.append(self.json_path) + else: + args.append("--no-p4") + if self.enable_debugger: + args.append("--debugger") + if self.log_console: + args.append("--log-console") + if self.grpc_port: + args.append("-- --grpc-server-addr 0.0.0.0:" + str(self.grpc_port)) + cmd = ' '.join(args) + info(cmd + "\n") + + logfile = "/tmp/p4s.{}.log".format(self.name) + pid = None + with tempfile.NamedTemporaryFile() as f: + self.cmd(cmd + ' >' + logfile + ' 2>&1 & echo $! >> ' + f.name) + pid = int(f.read()) + debug("P4 switch {} PID is {}.\n".format(self.name, pid)) + if not self.check_switch_started(pid): + error("P4 switch {} did not start correctly.\n".format(self.name)) + exit(1) + info("P4 switch {} has been started.\n".format(self.name)) + diff --git a/Teaching/utils/run_exercise.py b/Teaching/utils/run_exercise.py new file mode 100755 index 0000000..84997d7 --- /dev/null +++ b/Teaching/utils/run_exercise.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python2 +# 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. +# +# Adapted by Robert MacDavid (macdavid@cs.princeton.edu) from scripts found in +# the p4app repository (https://github.com/p4lang/p4app) +# +# We encourage you to dissect this script to better understand the BMv2/Mininet +# environment used by the P4 tutorial. +# +import os, sys, json, subprocess, re, argparse +from time import sleep + +from p4_mininet import P4Switch, P4Host + +from mininet.net import Mininet +from mininet.topo import Topo +from mininet.link import TCLink +from mininet.cli import CLI + +from p4runtime_switch import P4RuntimeSwitch + +def configureP4Switch(**switch_args): + """ Helper class that is called by mininet to initialize + the virtual P4 switches. The purpose is to ensure each + switch's thrift server is using a unique port. + """ + if "sw_path" in switch_args and 'grpc' in switch_args['sw_path']: + # If grpc appears in the BMv2 switch target, we assume will start P4 Runtime + class ConfiguredP4RuntimeSwitch(P4RuntimeSwitch): + def __init__(self, *opts, **kwargs): + kwargs.update(switch_args) + P4RuntimeSwitch.__init__(self, *opts, **kwargs) + + def describe(self): + print "%s -> gRPC port: %d" % (self.name, self.grpc_port) + + return ConfiguredP4RuntimeSwitch + else: + class ConfiguredP4Switch(P4Switch): + next_thrift_port = 9090 + def __init__(self, *opts, **kwargs): + global next_thrift_port + kwargs.update(switch_args) + kwargs['thrift_port'] = ConfiguredP4Switch.next_thrift_port + ConfiguredP4Switch.next_thrift_port += 1 + P4Switch.__init__(self, *opts, **kwargs) + + def describe(self): + print "%s -> Thrift port: %d" % (self.name, self.thrift_port) + + return ConfiguredP4Switch + + +class ExerciseTopo(Topo): + """ The mininet topology class for the P4 tutorial exercises. + A custom class is used because the exercises make a few topology + assumptions, mostly about the IP and MAC addresses. + """ + def __init__(self, hosts, switches, links, log_dir, **opts): + Topo.__init__(self, **opts) + host_links = [] + switch_links = [] + self.sw_port_mapping = {} + + for link in links: + if link['node1'][0] == 'h': + host_links.append(link) + else: + switch_links.append(link) + + link_sort_key = lambda x: x['node1'] + x['node2'] + # Links must be added in a sorted order so bmv2 port numbers are predictable + host_links.sort(key=link_sort_key) + switch_links.sort(key=link_sort_key) + + for sw in switches: + self.addSwitch(sw, log_file="%s/%s.log" %(log_dir, sw)) + + for link in host_links: + host_name = link['node1'] + host_sw = link['node2'] + host_num = int(host_name[1:]) + sw_num = int(host_sw[1:]) + host_ip = "10.0.%d.%d" % (sw_num, host_num) + host_mac = '00:00:00:00:%02x:%02x' % (sw_num, host_num) + # Each host IP should be /24, so all exercise traffic will use the + # default gateway (the switch) without sending ARP requests. + self.addHost(host_name, ip=host_ip+'/24', mac=host_mac) + self.addLink(host_name, host_sw, + delay=link['latency'], bw=link['bandwidth'], + addr1=host_mac, addr2=host_mac) + self.addSwitchPort(host_sw, host_name) + + for link in switch_links: + self.addLink(link['node1'], link['node2'], + delay=link['latency'], bw=link['bandwidth']) + self.addSwitchPort(link['node1'], link['node2']) + self.addSwitchPort(link['node2'], link['node1']) + + self.printPortMapping() + + def addSwitchPort(self, sw, node2): + if sw not in self.sw_port_mapping: + self.sw_port_mapping[sw] = [] + portno = len(self.sw_port_mapping[sw])+1 + self.sw_port_mapping[sw].append((portno, node2)) + + def printPortMapping(self): + print "Switch port mapping:" + for sw in sorted(self.sw_port_mapping.keys()): + print "%s: " % sw, + for portno, node2 in self.sw_port_mapping[sw]: + print "%d:%s\t" % (portno, node2), + print + + +class ExerciseRunner: + """ + Attributes: + log_dir : string // directory for mininet log files + pcap_dir : string // directory for mininet switch pcap files + quiet : bool // determines if we print logger messages + + hosts : list // list of mininet host names + switches : dict // mininet host names and their associated properties + links : list // list of mininet link properties + + switch_json : string // json of the compiled p4 example + bmv2_exe : string // name or path of the p4 switch binary + + topo : Topo object // The mininet topology instance + net : Mininet object // The mininet instance + + """ + def logger(self, *items): + if not self.quiet: + print(' '.join(items)) + + def formatLatency(self, l): + """ Helper method for parsing link latencies from the topology json. """ + if isinstance(l, (str, unicode)): + return l + else: + return str(l) + "ms" + + + def __init__(self, topo_file, log_dir, pcap_dir, + switch_json, bmv2_exe='simple_switch', quiet=False): + """ Initializes some attributes and reads the topology json. Does not + actually run the exercise. Use run_exercise() for that. + + Arguments: + topo_file : string // A json file which describes the exercise's + mininet topology. + log_dir : string // Path to a directory for storing exercise logs + pcap_dir : string // Ditto, but for mininet switch pcap files + switch_json : string // Path to a compiled p4 json for bmv2 + bmv2_exe : string // Path to the p4 behavioral binary + quiet : bool // Enable/disable script debug messages + """ + + self.quiet = quiet + self.logger('Reading topology file.') + with open(topo_file, 'r') as f: + topo = json.load(f) + self.hosts = topo['hosts'] + self.switches = topo['switches'] + self.links = self.parse_links(topo['links']) + + # Ensure all the needed directories exist and are directories + for dir_name in [log_dir, pcap_dir]: + if not os.path.isdir(dir_name): + if os.path.exists(dir_name): + raise Exception("'%s' exists and is not a directory!" % dir_name) + os.mkdir(dir_name) + self.log_dir = log_dir + self.pcap_dir = pcap_dir + self.switch_json = switch_json + self.bmv2_exe = bmv2_exe + + + def run_exercise(self): + """ Sets up the mininet instance, programs the switches, + and starts the mininet CLI. This is the main method to run after + initializing the object. + """ + # Initialize mininet with the topology specified by the config + self.create_network() + self.net.start() + sleep(1) + + # some programming that must happen after the net has started + self.program_hosts() + self.program_switches() + + # wait for that to finish. Not sure how to do this better + sleep(1) + + self.do_net_cli() + # stop right after the CLI is exited + self.net.stop() + + + def parse_links(self, unparsed_links): + """ Given a list of links descriptions of the form [node1, node2, latency, bandwidth] + with the latency and bandwidth being optional, parses these descriptions + into dictionaries and store them as self.links + """ + links = [] + for link in unparsed_links: + # make sure each link's endpoints are ordered alphabetically + s, t, = link[0], link[1] + if s > t: + s,t = t,s + + link_dict = {'node1':s, + 'node2':t, + 'latency':'0ms', + 'bandwidth':None + } + if len(link) > 2: + link_dict['latency'] = self.formatLatency(link[2]) + if len(link) > 3: + link_dict['bandwidth'] = link[3] + + if link_dict['node1'][0] == 'h': + assert link_dict['node2'][0] == 's', 'Hosts should be connected to switches, not ' + str(link_dict['node2']) + links.append(link_dict) + return links + + + def create_network(self): + """ Create the mininet network object, and store it as self.net. + + Side effects: + - Mininet topology instance stored as self.topo + - Mininet instance stored as self.net + """ + self.logger("Building mininet topology.") + + self.topo = ExerciseTopo(self.hosts, self.switches.keys(), self.links, self.log_dir) + + switchClass = configureP4Switch( + sw_path=self.bmv2_exe, + json_path=self.switch_json, + log_console=True, + pcap_dump=self.pcap_dir) + + self.net = Mininet(topo = self.topo, + link = TCLink, + host = P4Host, + switch = switchClass, + controller = None) + + + def program_switches(self): + """ If any command files were provided for the switches, + this method will start up the CLI on each switch and use the + contents of the command files as input. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + cli = 'simple_switch_CLI' + for sw_name, sw_dict in self.switches.iteritems(): + if 'cli_input' not in sw_dict: continue + # get the port for this particular switch's thrift server + sw_obj = self.net.get(sw_name) + thrift_port = sw_obj.thrift_port + + cli_input_commands = sw_dict['cli_input'] + self.logger('Configuring switch %s with file %s' % (sw_name, cli_input_commands)) + with open(cli_input_commands, 'r') as fin: + cli_outfile = '%s/%s_cli_output.log'%(self.log_dir, sw_name) + with open(cli_outfile, 'w') as fout: + subprocess.Popen([cli, '--thrift-port', str(thrift_port)], + stdin=fin, stdout=fout) + + def program_hosts(self): + """ Adds static ARP entries and default routes to each mininet host. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + for host_name in self.topo.hosts(): + h = self.net.get(host_name) + h_iface = h.intfs.values()[0] + link = h_iface.link + + sw_iface = link.intf1 if link.intf1 != h_iface else link.intf2 + # phony IP to lie to the host about + host_id = int(host_name[1:]) + sw_ip = '10.0.%d.254' % host_id + + # Ensure each host's interface name is unique, or else + # mininet cannot shutdown gracefully + h.defaultIntf().rename('%s-eth0' % host_name) + # static arp entries and default routes + h.cmd('arp -i %s -s %s %s' % (h_iface.name, sw_ip, sw_iface.mac)) + h.cmd('ethtool --offload %s rx off tx off' % h_iface.name) + h.cmd('ip route add %s dev %s' % (sw_ip, h_iface.name)) + h.setDefaultRoute("via %s" % sw_ip) + + + def do_net_cli(self): + """ Starts up the mininet CLI and prints some helpful output. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + for s in self.net.switches: + s.describe() + for h in self.net.hosts: + h.describe() + self.logger("Starting mininet CLI") + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + print('') + print('======================================================================') + print('Welcome to the BMV2 Mininet CLI!') + print('======================================================================') + print('Your P4 program is installed into the BMV2 software switch') + print('and your initial configuration is loaded. You can interact') + print('with the network using the mininet CLI below.') + print('') + if self.switch_json: + print('To inspect or change the switch configuration, connect to') + print('its CLI from your host operating system using this command:') + print(' simple_switch_CLI --thrift-port ') + print('') + print('To view a switch log, run this command from your host OS:') + print(' tail -f %s/.log' % self.log_dir) + print('') + print('To view the switch output pcap, check the pcap files in %s:' % self.pcap_dir) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap') + print('') + + CLI(self.net) + + +def get_args(): + cwd = os.getcwd() + default_logs = os.path.join(cwd, 'logs') + default_pcaps = os.path.join(cwd, 'pcaps') + parser = argparse.ArgumentParser() + parser.add_argument('-q', '--quiet', help='Suppress log messages.', + action='store_true', required=False, default=False) + parser.add_argument('-t', '--topo', help='Path to topology json', + type=str, required=False, default='./topology.json') + parser.add_argument('-l', '--log-dir', type=str, required=False, default=default_logs) + parser.add_argument('-p', '--pcap-dir', type=str, required=False, default=default_pcaps) + parser.add_argument('-j', '--switch_json', type=str, required=False) + parser.add_argument('-b', '--behavioral-exe', help='Path to behavioral executable', + type=str, required=False, default='simple_switch') + return parser.parse_args() + + +if __name__ == '__main__': + # from mininet.log import setLogLevel + # setLogLevel("info") + + args = get_args() + exercise = ExerciseRunner(args.topo, args.log_dir, args.pcap_dir, + args.switch_json, args.behavioral_exe, args.quiet) + + exercise.run_exercise() +