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
This commit is contained in:
parent
494706bd60
commit
e7e6899d5c
1
Teaching/Stanford_CS344_2018/.gitignore
vendored
Normal file
1
Teaching/Stanford_CS344_2018/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
simple_router.config
|
126
Teaching/Stanford_CS344_2018/README.md
Normal file
126
Teaching/Stanford_CS344_2018/README.md
Normal file
@ -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.
|
||||
|
26
Teaching/Stanford_CS344_2018/filter_reset.sh
Executable file
26
Teaching/Stanford_CS344_2018/filter_reset.sh
Executable file
@ -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
|
83
Teaching/Stanford_CS344_2018/header.p4
Normal file
83
Teaching/Stanford_CS344_2018/header.p4
Normal file
@ -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__
|
10
Teaching/Stanford_CS344_2018/p4app.json
Normal file
10
Teaching/Stanford_CS344_2018/p4app.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"program": "simple_router.p4",
|
||||
"language": "p4-16",
|
||||
"targets": {
|
||||
"mininet": {
|
||||
"num-hosts": 2,
|
||||
"switch-config": "simple_router.config"
|
||||
}
|
||||
}
|
||||
}
|
51
Teaching/Stanford_CS344_2018/parser.p4
Normal file
51
Teaching/Stanford_CS344_2018/parser.p4
Normal file
@ -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);
|
||||
}
|
||||
}
|
6
Teaching/Stanford_CS344_2018/run.sh
Executable file
6
Teaching/Stanford_CS344_2018/run.sh
Executable file
@ -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
|
21
Teaching/Stanford_CS344_2018/setup.py
Normal file
21
Teaching/Stanford_CS344_2018/setup.py
Normal file
@ -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))
|
||||
|
12
Teaching/Stanford_CS344_2018/simple_router.config.template
Normal file
12
Teaching/Stanford_CS344_2018/simple_router.config.template
Normal file
@ -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
|
210
Teaching/Stanford_CS344_2018/simple_router.p4
Normal file
210
Teaching/Stanford_CS344_2018/simple_router.p4
Normal file
@ -0,0 +1,210 @@
|
||||
#include <core.p4>
|
||||
#include <v1model.p4>
|
||||
|
||||
#include "header.p4"
|
||||
#include "parser.p4"
|
||||
|
||||
const bit<16> MAX_ADDRESS = 0x1F;
|
||||
const bit<16> THRESHOLD_COUNT = 8;
|
||||
|
||||
register<bit<48>>(32w1) last_reset_time;
|
||||
register<bit<32>>(32w32) hashtable_a0;
|
||||
register<bit<32>>(32w32) hashtable_a1;
|
||||
register<bit<32>>(32w32) hashtable_a2;
|
||||
register<bit<32>>(32w32) hashtable_a3;
|
||||
register<bit<32>>(32w32) hashtable_b0;
|
||||
register<bit<32>>(32w32) hashtable_b1;
|
||||
register<bit<32>>(32w32) hashtable_b2;
|
||||
register<bit<32>>(32w32) hashtable_b3;
|
||||
register<bit<1>>(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<bit<32>> 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;
|
222
Teaching/Stanford_CS344_2018/solution/simple_router.p4
Normal file
222
Teaching/Stanford_CS344_2018/solution/simple_router.p4
Normal file
@ -0,0 +1,222 @@
|
||||
#include <core.p4>
|
||||
#include <v1model.p4>
|
||||
|
||||
#include "header.p4"
|
||||
#include "parser.p4"
|
||||
|
||||
const bit<16> MAX_ADDRESS = 0x1F;
|
||||
const bit<16> THRESHOLD_COUNT = 8;
|
||||
|
||||
register<bit<48>>(32w1) last_reset_time;
|
||||
register<bit<32>>(32w32) hashtable_a0;
|
||||
register<bit<32>>(32w32) hashtable_a1;
|
||||
register<bit<32>>(32w32) hashtable_a2;
|
||||
register<bit<32>>(32w32) hashtable_a3;
|
||||
register<bit<32>>(32w32) hashtable_b0;
|
||||
register<bit<32>>(32w32) hashtable_b1;
|
||||
register<bit<32>>(32w32) hashtable_b2;
|
||||
register<bit<32>>(32w32) hashtable_b3;
|
||||
register<bit<1>>(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<bit<32>> 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;
|
42
Teaching/utils/Makefile
Normal file
42
Teaching/utils/Makefile
Normal file
@ -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)
|
93
Teaching/utils/mininet/appcontroller.py
Normal file
93
Teaching/utils/mininet/appcontroller.py
Normal file
@ -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
|
70
Teaching/utils/mininet/apptopo.py
Normal file
70
Teaching/utils/mininet/apptopo.py
Normal file
@ -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]
|
||||
|
243
Teaching/utils/mininet/multi_switch_mininet.py
Executable file
243
Teaching/utils/mininet/multi_switch_mininet.py
Executable file
@ -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()
|
161
Teaching/utils/mininet/p4_mininet.py
Normal file
161
Teaching/utils/mininet/p4_mininet.py
Normal file
@ -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)
|
78
Teaching/utils/mininet/shortest_path.py
Normal file
78
Teaching/utils/mininet/shortest_path.py
Normal file
@ -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
|
||||
|
133
Teaching/utils/mininet/single_switch_mininet.py
Executable file
133
Teaching/utils/mininet/single_switch_mininet.py
Executable file
@ -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()
|
21
Teaching/utils/netstat.py
Normal file
21
Teaching/utils/netstat.py
Normal file
@ -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
|
162
Teaching/utils/p4_mininet.py
Normal file
162
Teaching/utils/p4_mininet.py
Normal file
@ -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)
|
320
Teaching/utils/p4apprunner.py
Executable file
320
Teaching/utils/p4apprunner.py
Executable file
@ -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 <switch 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/<switchname>.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()
|
122
Teaching/utils/p4runtime_switch.py
Normal file
122
Teaching/utils/p4runtime_switch.py
Normal file
@ -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))
|
||||
|
382
Teaching/utils/run_exercise.py
Executable file
382
Teaching/utils/run_exercise.py
Executable file
@ -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<string> // list of mininet host names
|
||||
switches : dict<string, dict> // mininet host names and their associated properties
|
||||
links : list<dict> // 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 <switch thrift port>')
|
||||
print('')
|
||||
print('To view a switch log, run this command from your host OS:')
|
||||
print(' tail -f %s/<switchname>.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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user