Adding P4D2_2018_East Folder (#116)
* Copying P4D2 Fall 2017 into P4D2 2018 East. * Updated P4D2_2018_East VM. Added vagrant URL workaround, cdrom to VM. Updated to latest commits of BMV2, p4c, PI. Known issue with p4runtime exercise. * Applied patch from @antoninbas in and updated solution
This commit is contained in:
5
P4D2_2018_East/exercises/p4runtime/Makefile
Normal file
5
P4D2_2018_East/exercises/p4runtime/Makefile
Normal file
@@ -0,0 +1,5 @@
|
||||
BMV2_SWITCH_EXE = simple_switch_grpc
|
||||
NO_P4 = true
|
||||
P4C_ARGS = --p4runtime-file $(basename $@).p4info --p4runtime-format text
|
||||
|
||||
include ../../utils/Makefile
|
||||
|
After Width: | Height: | Size: 154 B |
188
P4D2_2018_East/exercises/p4runtime/README.md
Normal file
188
P4D2_2018_East/exercises/p4runtime/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Implementing a Control Plane using P4 Runtime
|
||||
|
||||
## Introduction
|
||||
|
||||
In this exercise, we will be using P4 Runtime to send flow entries to the
|
||||
switch instead of using the switch's CLI. We will be building on the same P4
|
||||
program that you used in the [basic_tunnel](../basic_tunnel) exercise. The
|
||||
P4 program has been renamed to `advanced_tunnel.py` and has been augmented
|
||||
with two counters (`ingressTunnelCounter`, `egressTunnelCounter`) and
|
||||
two new actions (`myTunnel_ingress`, `myTunnel_egress`).
|
||||
|
||||
You will use the starter program, `mycontroller.py`, and a few helper
|
||||
libraries in the `p4runtime_lib` directory to create the table entries
|
||||
necessary to tunnel traffic between host 1 and 2.
|
||||
|
||||
> **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 starter code for this assignment is in a file called `mycontroller.py`,
|
||||
and it will install only some of the rules that you need to tunnel traffic between
|
||||
two hosts.
|
||||
|
||||
Let's first compile the new P4 program, start the network, use `mycontroller.py`
|
||||
to install a few rules, and look at the `ingressTunnelCounter` to see that things
|
||||
are working as expected.
|
||||
|
||||
1. In your shell, run:
|
||||
```bash
|
||||
make
|
||||
```
|
||||
This will:
|
||||
* compile `advanced_tunnel.p4`,
|
||||
* start a Mininet instance with three switches (`s1`, `s2`, `s3`)
|
||||
configured in a triangle, each connected to one host (`h1`, `h2`, `h3`), and
|
||||
* assign IPs of `10.0.1.1`, `10.0.2.2`, `10.0.3.3` to the respective hosts.
|
||||
|
||||
2. You should now see a Mininet command prompt. Start a ping between h1 and h2:
|
||||
```bash
|
||||
mininet> h1 ping h2
|
||||
```
|
||||
Because there are no rules on the switches, you should **not** receive any
|
||||
replies yet. You should leave the ping running in this shell.
|
||||
|
||||
3. Open another shell and run the starter code:
|
||||
```bash
|
||||
cd ~/tutorials/P4D2_2017_Fall/exercises/p4runtime
|
||||
./mycontroller.py
|
||||
```
|
||||
This will install the `advanced_tunnel.p4` program on the switches and push the
|
||||
tunnel ingress rules.
|
||||
The program prints the tunnel ingress and egress counters every 2 seconds.
|
||||
You should see the ingress tunnel counter for s1 increasing:
|
||||
```
|
||||
s1 ingressTunnelCounter 100: 2 packets
|
||||
```
|
||||
The other counters should remain at zero.
|
||||
|
||||
4. Press `Ctrl-C` to the second shell to stop `mycontroller.py`
|
||||
|
||||
Each switch is currently mapping traffic into tunnels based on the destination IP
|
||||
address. Your job is to write the rules that forward the traffic between the switches
|
||||
based on the tunnel ID.
|
||||
|
||||
### Potential Issues
|
||||
|
||||
If you see the following error message when running `mycontroller.py`, then
|
||||
the gRPC server is not running on one or more switches.
|
||||
|
||||
```
|
||||
p4@p4:~/tutorials/P4D2_2017_Fall/exercises/p4runtime$ ./mycontroller.py
|
||||
...
|
||||
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with (StatusCode.UNAVAILABLE, Connect Failed)>
|
||||
```
|
||||
|
||||
You can check to see which of gRPC ports are listening on the machine by running:
|
||||
```bash
|
||||
sudo netstat -lpnt
|
||||
```
|
||||
|
||||
The easiest solution is to enter `Ctrl-D` or `exit` in the `mininet>` prompt,
|
||||
and re-run `make`.
|
||||
|
||||
### 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. In this case,
|
||||
`mycontroller.py` implements our control plane, instead of installing static
|
||||
table entries like we have in the previous exercises.
|
||||
|
||||
**Important:** A P4 program also defines the interface between the
|
||||
switch pipeline and control plane. This interface is defined in the
|
||||
`advanced_tunnel.p4info` file. The table entries that you build in `mycontroller.py`
|
||||
refer to specific tables, keys, and actions by name, and we use a P4Info helper
|
||||
to convert the names into the IDs that are required for P4 Runtime. Any changes
|
||||
in the P4 program that add or rename tables, keys, or actions will need to be
|
||||
reflected in your table entries.
|
||||
|
||||
## Step 2: Implement Tunnel Forwarding
|
||||
|
||||
The `mycontroller.py` file is a basic controller plane that does the following:
|
||||
1. Establishes a gRPC connection to the switches for the P4 Runtime service.
|
||||
2. Pushes the P4 program to each switch.
|
||||
3. Writes tunnel ingress and tunnel egress rules for two tunnels between h1 and h2.
|
||||
4. Reads tunnel ingress and egress counters every 2 seconds.
|
||||
|
||||
It also contains comments marked with `TODO` which indicate the functionality
|
||||
that you need to implement.
|
||||
|
||||
Your job will be to write the tunnel transit rule in the `writeTunnelRules` function
|
||||
that will match on tunnel ID and forward packets to the next hop.
|
||||
|
||||

|
||||
|
||||
In this exercise, you will be interacting with some of the classes and methods in
|
||||
the `p4runtime_lib` directory. Here is a summary of each of the files in the directory:
|
||||
- `helper.py`
|
||||
- Contains the `P4InfoHelper` class which is used to parse the `p4info` files.
|
||||
- Provides translation methods from entity name to and from ID number.
|
||||
- Builds P4 program-dependent sections of P4 Runtime table entries.
|
||||
- `switch.py`
|
||||
- Contains the `SwitchConnection` class which grabs the gRPC client stub, and
|
||||
establishes connections to the switches.
|
||||
- Provides helper methods that construct the P4 Runtime protocol buffer messages
|
||||
and makes the P4 Runtime gRPC service calls.
|
||||
- `bmv2.py`
|
||||
- Contains `Bmv2SwitchConnection` which extends `SwitchConnections` and provides
|
||||
the BMv2-specific device payload to load the P4 program.
|
||||
- `convert.py`
|
||||
- Provides convenience methods to encode and decode from friendly strings and
|
||||
numbers to the byte strings required for the protocol buffer messages.
|
||||
- Used by `helper.py`
|
||||
|
||||
|
||||
## Step 3: Run your solution
|
||||
|
||||
Follow the instructions from Step 1. If your Mininet network is still running,
|
||||
you will just need to run the following in your second shell:
|
||||
```bash
|
||||
./my_controller.py
|
||||
```
|
||||
|
||||
You should start to see ICMP replies in your Mininet prompt, and you should start to
|
||||
see the values for all counters start to increment.
|
||||
|
||||
### Extra Credit and Food for Thought
|
||||
|
||||
You might notice that the rules that are printed by `mycontroller.py` contain the entity
|
||||
IDs rather than the table names. You can use the P4Info helper to translate these IDs
|
||||
into entry names.
|
||||
|
||||
Also, you may want to think about the following:
|
||||
- What assumptions about the topology are baked into your implementation? How would you
|
||||
need to change it for a more realistic network?
|
||||
|
||||
- Why are the byte counters different between the ingress and egress counters?
|
||||
|
||||
- What is the TTL in the ICMP replies? Why is it the value that it is?
|
||||
Hint: The default TTL is 64 for packets sent by the hosts.
|
||||
|
||||
If you are interested, you can find the protocol buffer and gRPC definitions here:
|
||||
- [P4 Runtime](https://github.com/p4lang/PI/blob/master/proto/p4/p4runtime.proto)
|
||||
- [P4 Info](https://github.com/p4lang/PI/blob/master/proto/p4/config/p4info.proto)
|
||||
|
||||
#### Cleaning up Mininet
|
||||
|
||||
If the Mininet shell crashes, it may leave a Mininet instance
|
||||
running in the background. Use the following command to clean up:
|
||||
```bash
|
||||
make clean
|
||||
```
|
||||
|
||||
#### Running the reference solution
|
||||
|
||||
To run the reference solution, you should run the following command from the
|
||||
`~/tutorials/P4D2_2017_Fall/exercises/p4runtime` directory:
|
||||
```bash
|
||||
solution/my_controller.py
|
||||
```
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
Congratulations, your implementation works! Move onto the next assignment
|
||||
[ecn](../ecn)!
|
||||
|
||||
238
P4D2_2018_East/exercises/p4runtime/advanced_tunnel.p4
Executable file
238
P4D2_2018_East/exercises/p4runtime/advanced_tunnel.p4
Executable file
@@ -0,0 +1,238 @@
|
||||
/* -*- P4_16 -*- */
|
||||
#include <core.p4>
|
||||
#include <v1model.p4>
|
||||
|
||||
const bit<16> TYPE_MYTUNNEL = 0x1212;
|
||||
const bit<16> TYPE_IPV4 = 0x800;
|
||||
const bit<32> MAX_TUNNEL_ID = 1 << 16;
|
||||
|
||||
/*************************************************************************
|
||||
*********************** H E A D E R S ***********************************
|
||||
*************************************************************************/
|
||||
|
||||
typedef bit<9> egressSpec_t;
|
||||
typedef bit<48> macAddr_t;
|
||||
typedef bit<32> ip4Addr_t;
|
||||
|
||||
header ethernet_t {
|
||||
macAddr_t dstAddr;
|
||||
macAddr_t srcAddr;
|
||||
bit<16> etherType;
|
||||
}
|
||||
|
||||
header myTunnel_t {
|
||||
bit<16> proto_id;
|
||||
bit<16> dst_id;
|
||||
}
|
||||
|
||||
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;
|
||||
ip4Addr_t srcAddr;
|
||||
ip4Addr_t dstAddr;
|
||||
}
|
||||
|
||||
struct metadata {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
struct headers {
|
||||
ethernet_t ethernet;
|
||||
myTunnel_t myTunnel;
|
||||
ipv4_t ipv4;
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*********************** P A R S E R ***********************************
|
||||
*************************************************************************/
|
||||
|
||||
parser MyParser(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) {
|
||||
TYPE_MYTUNNEL: parse_myTunnel;
|
||||
TYPE_IPV4: parse_ipv4;
|
||||
default: accept;
|
||||
}
|
||||
}
|
||||
|
||||
state parse_myTunnel {
|
||||
packet.extract(hdr.myTunnel);
|
||||
transition select(hdr.myTunnel.proto_id) {
|
||||
TYPE_IPV4: parse_ipv4;
|
||||
default: accept;
|
||||
}
|
||||
}
|
||||
|
||||
state parse_ipv4 {
|
||||
packet.extract(hdr.ipv4);
|
||||
transition accept;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
************ C H E C K S U M V E R I F I C A T I O N *************
|
||||
*************************************************************************/
|
||||
|
||||
control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
|
||||
apply { }
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************
|
||||
************** I N G R E S S P R O C E S S I N G *******************
|
||||
*************************************************************************/
|
||||
|
||||
control MyIngress(inout headers hdr,
|
||||
inout metadata meta,
|
||||
inout standard_metadata_t standard_metadata) {
|
||||
|
||||
counter(MAX_TUNNEL_ID, CounterType.packets_and_bytes) ingressTunnelCounter;
|
||||
counter(MAX_TUNNEL_ID, CounterType.packets_and_bytes) egressTunnelCounter;
|
||||
|
||||
action drop() {
|
||||
mark_to_drop();
|
||||
}
|
||||
|
||||
action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
|
||||
standard_metadata.egress_spec = port;
|
||||
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
|
||||
hdr.ethernet.dstAddr = dstAddr;
|
||||
hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
|
||||
}
|
||||
|
||||
action myTunnel_ingress(bit<16> dst_id) {
|
||||
hdr.myTunnel.setValid();
|
||||
hdr.myTunnel.dst_id = dst_id;
|
||||
hdr.myTunnel.proto_id = hdr.ethernet.etherType;
|
||||
hdr.ethernet.etherType = TYPE_MYTUNNEL;
|
||||
ingressTunnelCounter.count((bit<32>) hdr.myTunnel.dst_id);
|
||||
}
|
||||
|
||||
action myTunnel_forward(egressSpec_t port) {
|
||||
standard_metadata.egress_spec = port;
|
||||
}
|
||||
|
||||
action myTunnel_egress(macAddr_t dstAddr, egressSpec_t port) {
|
||||
standard_metadata.egress_spec = port;
|
||||
hdr.ethernet.dstAddr = dstAddr;
|
||||
hdr.ethernet.etherType = hdr.myTunnel.proto_id;
|
||||
hdr.myTunnel.setInvalid();
|
||||
egressTunnelCounter.count((bit<32>) hdr.myTunnel.dst_id);
|
||||
}
|
||||
|
||||
table ipv4_lpm {
|
||||
key = {
|
||||
hdr.ipv4.dstAddr: lpm;
|
||||
}
|
||||
actions = {
|
||||
ipv4_forward;
|
||||
myTunnel_ingress;
|
||||
drop;
|
||||
NoAction;
|
||||
}
|
||||
size = 1024;
|
||||
default_action = NoAction();
|
||||
}
|
||||
|
||||
table myTunnel_exact {
|
||||
key = {
|
||||
hdr.myTunnel.dst_id: exact;
|
||||
}
|
||||
actions = {
|
||||
myTunnel_forward;
|
||||
myTunnel_egress;
|
||||
drop;
|
||||
}
|
||||
size = 1024;
|
||||
default_action = drop();
|
||||
}
|
||||
|
||||
apply {
|
||||
if (hdr.ipv4.isValid() && !hdr.myTunnel.isValid()) {
|
||||
// Process only non-tunneled IPv4 packets.
|
||||
ipv4_lpm.apply();
|
||||
}
|
||||
|
||||
if (hdr.myTunnel.isValid()) {
|
||||
// Process all tunneled packets.
|
||||
myTunnel_exact.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
**************** E G R E S S P R O C E S S I N G *******************
|
||||
*************************************************************************/
|
||||
|
||||
control MyEgress(inout headers hdr,
|
||||
inout metadata meta,
|
||||
inout standard_metadata_t standard_metadata) {
|
||||
apply { }
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
************* C H E C K S U M C O M P U T A T I O N **************
|
||||
*************************************************************************/
|
||||
|
||||
control MyComputeChecksum(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);
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*********************** D E P A R S E R *******************************
|
||||
*************************************************************************/
|
||||
|
||||
control MyDeparser(packet_out packet, in headers hdr) {
|
||||
apply {
|
||||
packet.emit(hdr.ethernet);
|
||||
packet.emit(hdr.myTunnel);
|
||||
packet.emit(hdr.ipv4);
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*********************** S W I T C H *******************************
|
||||
*************************************************************************/
|
||||
|
||||
V1Switch(
|
||||
MyParser(),
|
||||
MyVerifyChecksum(),
|
||||
MyIngress(),
|
||||
MyEgress(),
|
||||
MyComputeChecksum(),
|
||||
MyDeparser()
|
||||
) main;
|
||||
183
P4D2_2018_East/exercises/p4runtime/mycontroller.py
Executable file
183
P4D2_2018_East/exercises/p4runtime/mycontroller.py
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python2
|
||||
import argparse
|
||||
import os
|
||||
from time import sleep
|
||||
|
||||
import p4runtime_lib.bmv2
|
||||
import p4runtime_lib.helper
|
||||
|
||||
SWITCH_TO_HOST_PORT = 1
|
||||
SWITCH_TO_SWITCH_PORT = 2
|
||||
|
||||
def writeTunnelRules(p4info_helper, ingress_sw, egress_sw, tunnel_id,
|
||||
dst_eth_addr, dst_ip_addr):
|
||||
'''
|
||||
Installs three rules:
|
||||
1) An tunnel ingress rule on the ingress switch in the ipv4_lpm table that
|
||||
encapsulates traffic into a tunnel with the specified ID
|
||||
2) A transit rule on the ingress switch that forwards traffic based on
|
||||
the specified ID
|
||||
3) An tunnel egress rule on the egress switch that decapsulates traffic
|
||||
with the specified ID and sends it to the host
|
||||
|
||||
:param p4info_helper: the P4Info helper
|
||||
:param ingress_sw: the ingress switch connection
|
||||
:param egress_sw: the egress switch connection
|
||||
:param tunnel_id: the specified tunnel ID
|
||||
:param dst_eth_addr: the destination IP to match in the ingress rule
|
||||
:param dst_ip_addr: the destination Ethernet address to write in the
|
||||
egress rule
|
||||
'''
|
||||
# 1) Tunnel Ingress Rule
|
||||
table_entry = p4info_helper.buildTableEntry(
|
||||
table_name="MyIngress.ipv4_lpm",
|
||||
match_fields={
|
||||
"hdr.ipv4.dstAddr": (dst_ip_addr, 32)
|
||||
},
|
||||
action_name="MyIngress.myTunnel_ingress",
|
||||
action_params={
|
||||
"dst_id": tunnel_id,
|
||||
})
|
||||
ingress_sw.WriteTableEntry(table_entry)
|
||||
print "Installed ingress tunnel rule on %s" % ingress_sw.name
|
||||
|
||||
# 2) Tunnel Transit Rule
|
||||
# The rule will need to be added to the myTunnel_exact table and match on
|
||||
# the tunnel ID (hdr.myTunnel.dst_id). Traffic will need to be forwarded
|
||||
# using the myTunnel_forward action on the port connected to the next switch.
|
||||
#
|
||||
# For our simple topology, switch 1 and switch 2 are connected using a
|
||||
# link attached to port 2 on both switches. We have defined a variable at
|
||||
# the top of the file, SWITCH_TO_SWITCH_PORT, that you can use as the output
|
||||
# port for this action.
|
||||
#
|
||||
# We will only need a transit rule on the ingress switch because we are
|
||||
# using a simple topology. In general, you'll need on transit rule for
|
||||
# each switch in the path (except the last switch, which has the egress rule),
|
||||
# and you will need to select the port dynamically for each switch based on
|
||||
# your topology.
|
||||
|
||||
# TODO build the transit rule
|
||||
# TODO install the transit rule on the ingress switch
|
||||
print "TODO Install transit tunnel rule"
|
||||
|
||||
# 3) Tunnel Egress Rule
|
||||
# For our simple topology, the host will always be located on the
|
||||
# SWITCH_TO_HOST_PORT (port 1).
|
||||
# In general, you will need to keep track of which port the host is
|
||||
# connected to.
|
||||
table_entry = p4info_helper.buildTableEntry(
|
||||
table_name="MyIngress.myTunnel_exact",
|
||||
match_fields={
|
||||
"hdr.myTunnel.dst_id": tunnel_id
|
||||
},
|
||||
action_name="MyIngress.myTunnel_egress",
|
||||
action_params={
|
||||
"dstAddr": dst_eth_addr,
|
||||
"port": SWITCH_TO_HOST_PORT
|
||||
})
|
||||
egress_sw.WriteTableEntry(table_entry)
|
||||
print "Installed egress tunnel rule on %s" % egress_sw.name
|
||||
|
||||
def readTableRules(p4info_helper, sw):
|
||||
'''
|
||||
Reads the table entries from all tables on the switch.
|
||||
|
||||
:param p4info_helper: the P4Info helper
|
||||
:param sw: the switch connection
|
||||
'''
|
||||
print '\n----- Reading tables rules for %s -----' % sw.name
|
||||
for response in sw.ReadTableEntries():
|
||||
for entity in response.entities:
|
||||
entry = entity.table_entry
|
||||
# TODO For extra credit, you can use the p4info_helper to translate
|
||||
# the IDs the entry to names
|
||||
print entry
|
||||
print '-----'
|
||||
|
||||
def printCounter(p4info_helper, sw, counter_name, index):
|
||||
'''
|
||||
Reads the specified counter at the specified index from the switch. In our
|
||||
program, the index is the tunnel ID. If the index is 0, it will return all
|
||||
values from the counter.
|
||||
|
||||
:param p4info_helper: the P4Info helper
|
||||
:param sw: the switch connection
|
||||
:param counter_name: the name of the counter from the P4 program
|
||||
:param index: the counter index (in our case, the tunnel ID)
|
||||
'''
|
||||
for response in sw.ReadCounters(p4info_helper.get_counters_id(counter_name), index):
|
||||
for entity in response.entities:
|
||||
counter = entity.counter_entry
|
||||
print "%s %s %d: %d packets (%d bytes)" % (
|
||||
sw.name, counter_name, index,
|
||||
counter.data.packet_count, counter.data.byte_count
|
||||
)
|
||||
|
||||
|
||||
def main(p4info_file_path, bmv2_file_path):
|
||||
# Instantiate a P4 Runtime helper from the p4info file
|
||||
p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path)
|
||||
|
||||
# Create a switch connection object for s1 and s2;
|
||||
# this is backed by a P4 Runtime gRPC connection
|
||||
s1 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s1',
|
||||
address='127.0.0.1:50051',
|
||||
device_id=0)
|
||||
s2 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s2',
|
||||
address='127.0.0.1:50052',
|
||||
device_id=1)
|
||||
|
||||
# Install the P4 program on the switches
|
||||
s1.SetForwardingPipelineConfig(p4info=p4info_helper.p4info,
|
||||
bmv2_json_file_path=bmv2_file_path)
|
||||
print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s1.name
|
||||
s2.SetForwardingPipelineConfig(p4info=p4info_helper.p4info,
|
||||
bmv2_json_file_path=bmv2_file_path)
|
||||
print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s2.name
|
||||
|
||||
# Write the rules that tunnel traffic from h1 to h2
|
||||
writeTunnelRules(p4info_helper, ingress_sw=s1, egress_sw=s2, tunnel_id=100,
|
||||
dst_eth_addr="00:00:00:00:02:02", dst_ip_addr="10.0.2.2")
|
||||
|
||||
# Write the rules that tunnel traffic from h2 to h1
|
||||
writeTunnelRules(p4info_helper, ingress_sw=s2, egress_sw=s1, tunnel_id=200,
|
||||
dst_eth_addr="00:00:00:00:01:01", dst_ip_addr="10.0.1.1")
|
||||
|
||||
# TODO Uncomment the following two lines to read table entries from s1 and s2
|
||||
#readTableRules(p4info_helper, s1)
|
||||
#readTableRules(p4info_helper, s2)
|
||||
|
||||
# Print the tunnel counters every 2 seconds
|
||||
try:
|
||||
while True:
|
||||
sleep(2)
|
||||
print '\n----- Reading tunnel counters -----'
|
||||
printCounter(p4info_helper, s1, "MyIngress.ingressTunnelCounter", 100)
|
||||
printCounter(p4info_helper, s2, "MyIngress.egressTunnelCounter", 100)
|
||||
printCounter(p4info_helper, s2, "MyIngress.ingressTunnelCounter", 200)
|
||||
printCounter(p4info_helper, s1, "MyIngress.egressTunnelCounter", 200)
|
||||
except KeyboardInterrupt:
|
||||
print " Shutting down."
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='P4Runtime Controller')
|
||||
parser.add_argument('--p4info', help='p4info proto in text format from p4c',
|
||||
type=str, action="store", required=False,
|
||||
default='./build/advanced_tunnel.p4info')
|
||||
parser.add_argument('--bmv2-json', help='BMv2 JSON file from p4c',
|
||||
type=str, action="store", required=False,
|
||||
default='./build/advanced_tunnel.json')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.p4info):
|
||||
parser.print_help()
|
||||
print "\np4info file not found: %s\nHave you run 'make'?" % args.p4info
|
||||
parser.exit(1)
|
||||
if not os.path.exists(args.bmv2_json):
|
||||
parser.print_help()
|
||||
print "\nBMv2 JSON file not found: %s\nHave you run 'make'?" % args.bmv2_json
|
||||
parser.exit(1)
|
||||
|
||||
main(args.p4info, args.bmv2_json)
|
||||
30
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/bmv2.py
Normal file
30
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/bmv2.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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.
|
||||
#
|
||||
from switch import SwitchConnection
|
||||
from p4.tmp import p4config_pb2
|
||||
|
||||
|
||||
def buildDeviceConfig(bmv2_json_file_path=None):
|
||||
"Builds the device config for BMv2"
|
||||
device_config = p4config_pb2.P4DeviceConfig()
|
||||
device_config.reassign = True
|
||||
with open(bmv2_json_file_path) as f:
|
||||
device_config.device_data = f.read()
|
||||
return device_config
|
||||
|
||||
|
||||
class Bmv2SwitchConnection(SwitchConnection):
|
||||
def buildDeviceConfig(self, **kwargs):
|
||||
return buildDeviceConfig(**kwargs)
|
||||
119
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/convert.py
Normal file
119
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/convert.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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 re
|
||||
import socket
|
||||
|
||||
import math
|
||||
|
||||
'''
|
||||
This package contains several helper functions for encoding to and decoding from byte strings:
|
||||
- integers
|
||||
- IPv4 address strings
|
||||
- Ethernet address strings
|
||||
'''
|
||||
|
||||
mac_pattern = re.compile('^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$')
|
||||
def matchesMac(mac_addr_string):
|
||||
return mac_pattern.match(mac_addr_string) is not None
|
||||
|
||||
def encodeMac(mac_addr_string):
|
||||
return mac_addr_string.replace(':', '').decode('hex')
|
||||
|
||||
def decodeMac(encoded_mac_addr):
|
||||
return ':'.join(s.encode('hex') for s in encoded_mac_addr)
|
||||
|
||||
ip_pattern = re.compile('^(\d{1,3}\.){3}(\d{1,3})$')
|
||||
def matchesIPv4(ip_addr_string):
|
||||
return ip_pattern.match(ip_addr_string) is not None
|
||||
|
||||
def encodeIPv4(ip_addr_string):
|
||||
return socket.inet_aton(ip_addr_string)
|
||||
|
||||
def decodeIPv4(encoded_ip_addr):
|
||||
return socket.inet_ntoa(encoded_ip_addr)
|
||||
|
||||
def bitwidthToBytes(bitwidth):
|
||||
return int(math.ceil(bitwidth / 8.0))
|
||||
|
||||
def encodeNum(number, bitwidth):
|
||||
byte_len = bitwidthToBytes(bitwidth)
|
||||
num_str = '%x' % number
|
||||
if number >= 2 ** bitwidth:
|
||||
raise Exception("Number, %d, does not fit in %d bits" % (number, bitwidth))
|
||||
return ('0' * (byte_len * 2 - len(num_str)) + num_str).decode('hex')
|
||||
|
||||
def decodeNum(encoded_number):
|
||||
return int(encoded_number.encode('hex'), 16)
|
||||
|
||||
def encode(x, bitwidth):
|
||||
'Tries to infer the type of `x` and encode it'
|
||||
byte_len = bitwidthToBytes(bitwidth)
|
||||
if (type(x) == list or type(x) == tuple) and len(x) == 1:
|
||||
x = x[0]
|
||||
encoded_bytes = None
|
||||
if type(x) == str:
|
||||
if matchesMac(x):
|
||||
encoded_bytes = encodeMac(x)
|
||||
elif matchesIPv4(x):
|
||||
encoded_bytes = encodeIPv4(x)
|
||||
else:
|
||||
# Assume that the string is already encoded
|
||||
encoded_bytes = x
|
||||
elif type(x) == int:
|
||||
encoded_bytes = encodeNum(x, bitwidth)
|
||||
else:
|
||||
raise Exception("Encoding objects of %r is not supported" % type(x))
|
||||
assert(len(encoded_bytes) == byte_len)
|
||||
return encoded_bytes
|
||||
|
||||
if __name__ == '__main__':
|
||||
# TODO These tests should be moved out of main eventually
|
||||
mac = "aa:bb:cc:dd:ee:ff"
|
||||
enc_mac = encodeMac(mac)
|
||||
assert(enc_mac == '\xaa\xbb\xcc\xdd\xee\xff')
|
||||
dec_mac = decodeMac(enc_mac)
|
||||
assert(mac == dec_mac)
|
||||
|
||||
ip = "10.0.0.1"
|
||||
enc_ip = encodeIPv4(ip)
|
||||
assert(enc_ip == '\x0a\x00\x00\x01')
|
||||
dec_ip = decodeIPv4(enc_ip)
|
||||
assert(ip == dec_ip)
|
||||
|
||||
num = 1337
|
||||
byte_len = 5
|
||||
enc_num = encodeNum(num, byte_len * 8)
|
||||
assert(enc_num == '\x00\x00\x00\x05\x39')
|
||||
dec_num = decodeNum(enc_num)
|
||||
assert(num == dec_num)
|
||||
|
||||
assert(matchesIPv4('10.0.0.1'))
|
||||
assert(not matchesIPv4('10.0.0.1.5'))
|
||||
assert(not matchesIPv4('1000.0.0.1'))
|
||||
assert(not matchesIPv4('10001'))
|
||||
|
||||
assert(encode(mac, 6 * 8) == enc_mac)
|
||||
assert(encode(ip, 4 * 8) == enc_ip)
|
||||
assert(encode(num, 5 * 8) == enc_num)
|
||||
assert(encode((num,), 5 * 8) == enc_num)
|
||||
assert(encode([num], 5 * 8) == enc_num)
|
||||
|
||||
num = 256
|
||||
byte_len = 2
|
||||
try:
|
||||
enc_num = encodeNum(num, 8)
|
||||
raise Exception("expected exception")
|
||||
except Exception as e:
|
||||
print e
|
||||
183
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/helper.py
Normal file
183
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/helper.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# 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 re
|
||||
|
||||
import google.protobuf.text_format
|
||||
from p4 import p4runtime_pb2
|
||||
from p4.config import p4info_pb2
|
||||
|
||||
from p4runtime_lib.convert import encode
|
||||
|
||||
class P4InfoHelper(object):
|
||||
def __init__(self, p4_info_filepath):
|
||||
p4info = p4info_pb2.P4Info()
|
||||
# Load the p4info file into a skeleton P4Info object
|
||||
with open(p4_info_filepath) as p4info_f:
|
||||
google.protobuf.text_format.Merge(p4info_f.read(), p4info)
|
||||
self.p4info = p4info
|
||||
|
||||
def get(self, entity_type, name=None, id=None):
|
||||
if name is not None and id is not None:
|
||||
raise AssertionError("name or id must be None")
|
||||
|
||||
for o in getattr(self.p4info, entity_type):
|
||||
pre = o.preamble
|
||||
if name:
|
||||
if (pre.name == name or pre.alias == name):
|
||||
return o
|
||||
else:
|
||||
if pre.id == id:
|
||||
return o
|
||||
|
||||
if name:
|
||||
raise AttributeError("Could not find %r of type %s" % (name, entity_type))
|
||||
else:
|
||||
raise AttributeError("Could not find id %r of type %s" % (id, entity_type))
|
||||
|
||||
def get_id(self, entity_type, name):
|
||||
return self.get(entity_type, name=name).preamble.id
|
||||
|
||||
def get_name(self, entity_type, id):
|
||||
return self.get(entity_type, id=id).preamble.name
|
||||
|
||||
def get_alias(self, entity_type, id):
|
||||
return self.get(entity_type, id=id).preamble.alias
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# Synthesize convenience functions for name to id lookups for top-level entities
|
||||
# e.g. get_tables_id(name_string) or get_actions_id(name_string)
|
||||
m = re.search("^get_(\w+)_id$", attr)
|
||||
if m:
|
||||
primitive = m.group(1)
|
||||
return lambda name: self.get_id(primitive, name)
|
||||
|
||||
# Synthesize convenience functions for id to name lookups
|
||||
# e.g. get_tables_name(id) or get_actions_name(id)
|
||||
m = re.search("^get_(\w+)_name$", attr)
|
||||
if m:
|
||||
primitive = m.group(1)
|
||||
return lambda id: self.get_name(primitive, id)
|
||||
|
||||
raise AttributeError("%r object has no attribute %r" % (self.__class__, attr))
|
||||
|
||||
def get_match_field(self, table_name, name=None, id=None):
|
||||
for t in self.p4info.tables:
|
||||
pre = t.preamble
|
||||
if pre.name == table_name:
|
||||
for mf in t.match_fields:
|
||||
if name is not None:
|
||||
if mf.name == name:
|
||||
return mf
|
||||
elif id is not None:
|
||||
if mf.id == id:
|
||||
return mf
|
||||
raise AttributeError("%r has no attribute %r" % (table_name, name if name is not None else id))
|
||||
|
||||
def get_match_field_id(self, table_name, match_field_name):
|
||||
return self.get_match_field(table_name, name=match_field_name).id
|
||||
|
||||
def get_match_field_name(self, table_name, match_field_id):
|
||||
return self.get_match_field(table_name, id=match_field_id).name
|
||||
|
||||
def get_match_field_pb(self, table_name, match_field_name, value):
|
||||
p4info_match = self.get_match_field(table_name, match_field_name)
|
||||
bitwidth = p4info_match.bitwidth
|
||||
p4runtime_match = p4runtime_pb2.FieldMatch()
|
||||
p4runtime_match.field_id = p4info_match.id
|
||||
match_type = p4info_match.match_type
|
||||
if match_type == p4info_pb2.MatchField.VALID:
|
||||
valid = p4runtime_match.valid
|
||||
valid.value = bool(value)
|
||||
elif match_type == p4info_pb2.MatchField.EXACT:
|
||||
exact = p4runtime_match.exact
|
||||
exact.value = encode(value, bitwidth)
|
||||
elif match_type == p4info_pb2.MatchField.LPM:
|
||||
lpm = p4runtime_match.lpm
|
||||
lpm.value = encode(value[0], bitwidth)
|
||||
lpm.prefix_len = value[1]
|
||||
elif match_type == p4info_pb2.MatchField.TERNARY:
|
||||
lpm = p4runtime_match.ternary
|
||||
lpm.value = encode(value[0], bitwidth)
|
||||
lpm.mask = encode(value[1], bitwidth)
|
||||
elif match_type == p4info_pb2.MatchField.RANGE:
|
||||
lpm = p4runtime_match.range
|
||||
lpm.low = encode(value[0], bitwidth)
|
||||
lpm.high = encode(value[1], bitwidth)
|
||||
else:
|
||||
raise Exception("Unsupported match type with type %r" % match_type)
|
||||
return p4runtime_match
|
||||
|
||||
def get_match_field_value(self, match_field):
|
||||
match_type = match_field.WhichOneof("field_match_type")
|
||||
if match_type == 'valid':
|
||||
return match_field.valid.value
|
||||
elif match_type == 'exact':
|
||||
return match_field.exact.value
|
||||
elif match_type == 'lpm':
|
||||
return (match_field.lpm.value, match_field.lpm.prefix_len)
|
||||
elif match_type == 'ternary':
|
||||
return (match_field.ternary.value, match_field.ternary.mask)
|
||||
elif match_type == 'range':
|
||||
return (match_field.range.low, match_field.range.high)
|
||||
else:
|
||||
raise Exception("Unsupported match type with type %r" % match_type)
|
||||
|
||||
def get_action_param(self, action_name, name=None, id=None):
|
||||
for a in self.p4info.actions:
|
||||
pre = a.preamble
|
||||
if pre.name == action_name:
|
||||
for p in a.params:
|
||||
if name is not None:
|
||||
if p.name == name:
|
||||
return p
|
||||
elif id is not None:
|
||||
if p.id == id:
|
||||
return p
|
||||
raise AttributeError("action %r has no param %r" % (action_name, name if name is not None else id))
|
||||
|
||||
def get_action_param_id(self, action_name, param_name):
|
||||
return self.get_action_param(action_name, name=param_name).id
|
||||
|
||||
def get_action_param_name(self, action_name, param_id):
|
||||
return self.get_action_param(action_name, id=param_id).name
|
||||
|
||||
def get_action_param_pb(self, action_name, param_name, value):
|
||||
p4info_param = self.get_action_param(action_name, param_name)
|
||||
p4runtime_param = p4runtime_pb2.Action.Param()
|
||||
p4runtime_param.param_id = p4info_param.id
|
||||
p4runtime_param.value = encode(value, p4info_param.bitwidth)
|
||||
return p4runtime_param
|
||||
|
||||
def buildTableEntry(self,
|
||||
table_name,
|
||||
match_fields={},
|
||||
action_name=None,
|
||||
action_params={}):
|
||||
table_entry = p4runtime_pb2.TableEntry()
|
||||
table_entry.table_id = self.get_tables_id(table_name)
|
||||
if match_fields:
|
||||
table_entry.match.extend([
|
||||
self.get_match_field_pb(table_name, match_field_name, value)
|
||||
for match_field_name, value in match_fields.iteritems()
|
||||
])
|
||||
if action_name:
|
||||
action = table_entry.action.action
|
||||
action.action_id = self.get_actions_id(action_name)
|
||||
if action_params:
|
||||
action.params.extend([
|
||||
self.get_action_param_pb(action_name, field_name, value)
|
||||
for field_name, value in action_params.iteritems()
|
||||
])
|
||||
return table_entry
|
||||
88
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/switch.py
Normal file
88
P4D2_2018_East/exercises/p4runtime/p4runtime_lib/switch.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
#
|
||||
from abc import abstractmethod
|
||||
|
||||
import grpc
|
||||
from p4 import p4runtime_pb2
|
||||
from p4.tmp import p4config_pb2
|
||||
|
||||
class SwitchConnection(object):
|
||||
def __init__(self, name, address='127.0.0.1:50051', device_id=0):
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.device_id = device_id
|
||||
self.p4info = None
|
||||
self.channel = grpc.insecure_channel(self.address)
|
||||
self.client_stub = p4runtime_pb2.P4RuntimeStub(self.channel)
|
||||
|
||||
@abstractmethod
|
||||
def buildDeviceConfig(self, **kwargs):
|
||||
return p4config_pb2.P4DeviceConfig()
|
||||
|
||||
def SetForwardingPipelineConfig(self, p4info, dry_run=False, **kwargs):
|
||||
device_config = self.buildDeviceConfig(**kwargs)
|
||||
request = p4runtime_pb2.SetForwardingPipelineConfigRequest()
|
||||
request.device_id = self.device_id
|
||||
config = request.config
|
||||
config.p4info.CopyFrom(p4info)
|
||||
config.p4_device_config = device_config.SerializeToString()
|
||||
request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT
|
||||
if dry_run:
|
||||
print "P4 Runtime SetForwardingPipelineConfig:", request
|
||||
else:
|
||||
self.client_stub.SetForwardingPipelineConfig(request)
|
||||
|
||||
def WriteTableEntry(self, table_entry, dry_run=False):
|
||||
request = p4runtime_pb2.WriteRequest()
|
||||
request.device_id = self.device_id
|
||||
update = request.updates.add()
|
||||
update.type = p4runtime_pb2.Update.INSERT
|
||||
update.entity.table_entry.CopyFrom(table_entry)
|
||||
if dry_run:
|
||||
print "P4 Runtime Write:", request
|
||||
else:
|
||||
self.client_stub.Write(request)
|
||||
|
||||
def ReadTableEntries(self, table_id=None, dry_run=False):
|
||||
request = p4runtime_pb2.ReadRequest()
|
||||
request.device_id = self.device_id
|
||||
entity = request.entities.add()
|
||||
table_entry = entity.table_entry
|
||||
if table_id is not None:
|
||||
table_entry.table_id = table_id
|
||||
else:
|
||||
table_entry.table_id = 0
|
||||
if dry_run:
|
||||
print "P4 Runtime Read:", request
|
||||
else:
|
||||
for response in self.client_stub.Read(request):
|
||||
yield response
|
||||
|
||||
def ReadCounters(self, counter_id=None, index=None, dry_run=False):
|
||||
request = p4runtime_pb2.ReadRequest()
|
||||
request.device_id = self.device_id
|
||||
entity = request.entities.add()
|
||||
counter_entry = entity.counter_entry
|
||||
if counter_id is not None:
|
||||
counter_entry.counter_id = counter_id
|
||||
else:
|
||||
counter_entry.counter_id = 0
|
||||
if index is not None:
|
||||
counter_entry.index = index
|
||||
if dry_run:
|
||||
print "P4 Runtime Read:", request
|
||||
else:
|
||||
for response in self.client_stub.Read(request):
|
||||
yield response
|
||||
206
P4D2_2018_East/exercises/p4runtime/solution/mycontroller.py
Executable file
206
P4D2_2018_East/exercises/p4runtime/solution/mycontroller.py
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python2
|
||||
import argparse
|
||||
import os
|
||||
from time import sleep
|
||||
|
||||
# NOTE: Appending to the PYTHON_PATH is only required in the `solution` directory.
|
||||
# It is not required for mycontroller.py in the top-level directory.
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
import p4runtime_lib.bmv2
|
||||
import p4runtime_lib.helper
|
||||
|
||||
SWITCH_TO_HOST_PORT = 1
|
||||
SWITCH_TO_SWITCH_PORT = 2
|
||||
|
||||
def writeTunnelRules(p4info_helper, ingress_sw, egress_sw, tunnel_id,
|
||||
dst_eth_addr, dst_ip_addr):
|
||||
'''
|
||||
Installs three rules:
|
||||
1) An tunnel ingress rule on the ingress switch in the ipv4_lpm table that
|
||||
encapsulates traffic into a tunnel with the specified ID
|
||||
2) A transit rule on the ingress switch that forwards traffic based on
|
||||
the specified ID
|
||||
3) An tunnel egress rule on the egress switch that decapsulates traffic
|
||||
with the specified ID and sends it to the host
|
||||
|
||||
:param p4info_helper: the P4Info helper
|
||||
:param ingress_sw: the ingress switch connection
|
||||
:param egress_sw: the egress switch connection
|
||||
:param tunnel_id: the specified tunnel ID
|
||||
:param dst_eth_addr: the destination IP to match in the ingress rule
|
||||
:param dst_ip_addr: the destination Ethernet address to write in the
|
||||
egress rule
|
||||
'''
|
||||
# 1) Tunnel Ingress Rule
|
||||
table_entry = p4info_helper.buildTableEntry(
|
||||
table_name="MyIngress.ipv4_lpm",
|
||||
match_fields={
|
||||
"hdr.ipv4.dstAddr": (dst_ip_addr, 32)
|
||||
},
|
||||
action_name="MyIngress.myTunnel_ingress",
|
||||
action_params={
|
||||
"dst_id": tunnel_id,
|
||||
})
|
||||
ingress_sw.WriteTableEntry(table_entry)
|
||||
print "Installed ingress tunnel rule on %s" % ingress_sw.name
|
||||
|
||||
# 2) Tunnel Transit Rule
|
||||
# The rule will need to be added to the myTunnel_exact table and match on
|
||||
# the tunnel ID (hdr.myTunnel.dst_id). Traffic will need to be forwarded
|
||||
# using the myTunnel_forward action on the port connected to the next switch.
|
||||
#
|
||||
# For our simple topology, switch 1 and switch 2 are connected using a
|
||||
# link attached to port 2 on both switches. We have defined a variable at
|
||||
# the top of the file, SWITCH_TO_SWITCH_PORT, that you can use as the output
|
||||
# port for this action.
|
||||
#
|
||||
# We will only need a transit rule on the ingress switch because we are
|
||||
# using a simple topology. In general, you'll need on transit rule for
|
||||
# each switch in the path (except the last switch, which has the egress rule),
|
||||
# and you will need to select the port dynamically for each switch based on
|
||||
# your topology.
|
||||
|
||||
table_entry = p4info_helper.buildTableEntry(
|
||||
table_name="MyIngress.myTunnel_exact",
|
||||
match_fields={
|
||||
"hdr.myTunnel.dst_id": tunnel_id
|
||||
},
|
||||
action_name="MyIngress.myTunnel_forward",
|
||||
action_params={
|
||||
"port": SWITCH_TO_SWITCH_PORT
|
||||
})
|
||||
ingress_sw.WriteTableEntry(table_entry)
|
||||
print "Installed transit tunnel rule on %s" % ingress_sw.name
|
||||
|
||||
# 3) Tunnel Egress Rule
|
||||
# For our simple topology, the host will always be located on the
|
||||
# SWITCH_TO_HOST_PORT (port 1).
|
||||
# In general, you will need to keep track of which port the host is
|
||||
# connected to.
|
||||
table_entry = p4info_helper.buildTableEntry(
|
||||
table_name="MyIngress.myTunnel_exact",
|
||||
match_fields={
|
||||
"hdr.myTunnel.dst_id": tunnel_id
|
||||
},
|
||||
action_name="MyIngress.myTunnel_egress",
|
||||
action_params={
|
||||
"dstAddr": dst_eth_addr,
|
||||
"port": SWITCH_TO_HOST_PORT
|
||||
})
|
||||
egress_sw.WriteTableEntry(table_entry)
|
||||
print "Installed egress tunnel rule on %s" % egress_sw.name
|
||||
|
||||
def readTableRules(p4info_helper, sw):
|
||||
'''
|
||||
Reads the table entries from all tables on the switch.
|
||||
|
||||
:param p4info_helper: the P4Info helper
|
||||
:param sw: the switch connection
|
||||
'''
|
||||
print '\n----- Reading tables rules for %s -----' % sw.name
|
||||
for response in sw.ReadTableEntries():
|
||||
for entity in response.entities:
|
||||
entry = entity.table_entry
|
||||
# TODO For extra credit, you can use the p4info_helper to translate
|
||||
# the IDs the entry to names
|
||||
table_name = p4info_helper.get_tables_name(entry.table_id)
|
||||
print '%s: ' % table_name,
|
||||
for m in entry.match:
|
||||
print p4info_helper.get_match_field_name(table_name, m.field_id),
|
||||
print '%r' % (p4info_helper.get_match_field_value(m),),
|
||||
action = entry.action.action
|
||||
action_name = p4info_helper.get_actions_name(action.action_id)
|
||||
print '->', action_name,
|
||||
for p in action.params:
|
||||
print p4info_helper.get_action_param_name(action_name, p.param_id),
|
||||
print '%r' % p.value,
|
||||
print
|
||||
|
||||
def printCounter(p4info_helper, sw, counter_name, index):
|
||||
'''
|
||||
Reads the specified counter at the specified index from the switch. In our
|
||||
program, the index is the tunnel ID. If the index is 0, it will return all
|
||||
values from the counter.
|
||||
|
||||
:param p4info_helper: the P4Info helper
|
||||
:param sw: the switch connection
|
||||
:param counter_name: the name of the counter from the P4 program
|
||||
:param index: the counter index (in our case, the tunnel ID)
|
||||
'''
|
||||
for response in sw.ReadCounters(p4info_helper.get_counters_id(counter_name), index):
|
||||
for entity in response.entities:
|
||||
counter = entity.counter_entry
|
||||
print "%s %s %d: %d packets (%d bytes)" % (
|
||||
sw.name, counter_name, index,
|
||||
counter.data.packet_count, counter.data.byte_count
|
||||
)
|
||||
|
||||
|
||||
def main(p4info_file_path, bmv2_file_path):
|
||||
# Instantiate a P4 Runtime helper from the p4info file
|
||||
p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path)
|
||||
|
||||
# Create a switch connection object for s1 and s2;
|
||||
# this is backed by a P4 Runtime gRPC connection
|
||||
s1 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s1',
|
||||
address='127.0.0.1:50051',
|
||||
device_id=0)
|
||||
s2 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s2',
|
||||
address='127.0.0.1:50052',
|
||||
device_id=1)
|
||||
|
||||
# Install the P4 program on the switches
|
||||
s1.SetForwardingPipelineConfig(p4info=p4info_helper.p4info,
|
||||
bmv2_json_file_path=bmv2_file_path)
|
||||
print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s1.name
|
||||
s2.SetForwardingPipelineConfig(p4info=p4info_helper.p4info,
|
||||
bmv2_json_file_path=bmv2_file_path)
|
||||
print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s2.name
|
||||
|
||||
# Write the rules that tunnel traffic from h1 to h2
|
||||
writeTunnelRules(p4info_helper, ingress_sw=s1, egress_sw=s2, tunnel_id=100,
|
||||
dst_eth_addr="00:00:00:00:02:02", dst_ip_addr="10.0.2.2")
|
||||
|
||||
# Write the rules that tunnel traffic from h2 to h1
|
||||
writeTunnelRules(p4info_helper, ingress_sw=s2, egress_sw=s1, tunnel_id=200,
|
||||
dst_eth_addr="00:00:00:00:01:01", dst_ip_addr="10.0.1.1")
|
||||
|
||||
# TODO Uncomment the following two lines to read table entries from s1 and s2
|
||||
readTableRules(p4info_helper, s1)
|
||||
readTableRules(p4info_helper, s2)
|
||||
|
||||
# Print the tunnel counters every 2 seconds
|
||||
try:
|
||||
while True:
|
||||
sleep(2)
|
||||
print '\n----- Reading tunnel counters -----'
|
||||
printCounter(p4info_helper, s1, "MyIngress.ingressTunnelCounter", 100)
|
||||
printCounter(p4info_helper, s2, "MyIngress.egressTunnelCounter", 100)
|
||||
printCounter(p4info_helper, s2, "MyIngress.ingressTunnelCounter", 200)
|
||||
printCounter(p4info_helper, s1, "MyIngress.egressTunnelCounter", 200)
|
||||
except KeyboardInterrupt:
|
||||
print " Shutting down."
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='P4Runtime Controller')
|
||||
parser.add_argument('--p4info', help='p4info proto in text format from p4c',
|
||||
type=str, action="store", required=False,
|
||||
default='./build/advanced_tunnel.p4info')
|
||||
parser.add_argument('--bmv2-json', help='BMv2 JSON file from p4c',
|
||||
type=str, action="store", required=False,
|
||||
default='./build/advanced_tunnel.json')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.p4info):
|
||||
parser.print_help()
|
||||
print "\np4info file not found: %s\nHave you run 'make'?" % args.p4info
|
||||
parser.exit(1)
|
||||
if not os.path.exists(args.bmv2_json):
|
||||
parser.print_help()
|
||||
print "\nBMv2 JSON file not found: %s\nHave you run 'make'?" % args.bmv2_json
|
||||
parser.exit(1)
|
||||
|
||||
main(args.p4info, args.bmv2_json)
|
||||
16
P4D2_2018_East/exercises/p4runtime/topology.json
Executable file
16
P4D2_2018_East/exercises/p4runtime/topology.json
Executable file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hosts": [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3"
|
||||
],
|
||||
"switches": {
|
||||
"s1": {},
|
||||
"s2": {},
|
||||
"s3": {}
|
||||
},
|
||||
"links": [
|
||||
["h1", "s1"], ["s1", "s2"], ["s1", "s3"],
|
||||
["s3", "s2"], ["s2", "h2"], ["s3", "h3"]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user