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:
robh2
2018-03-04 03:50:22 -05:00
committed by Nate Foster
parent 89277b5542
commit 97f31560ca
103 changed files with 8930 additions and 0 deletions

View 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

View 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.
![topology](../basic_tunnel/topo.png)
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)!

View 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;

View 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)

View 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)

View 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

View 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

View 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

View 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)

View 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"]
]
}