diff --git a/README.md b/README.md index 6502fd58b..5d637fa1a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ you get started with P4 programming, organized into several modules: * [Calculator](./exercises/calc) * [Load Balancing](./exercises/load_balance) * [Quality of Service](./exercises/qos) +* [Mirroring](./exercises/mirroring) 5. Stateful Packet Processing * [Firewall](./exercises/firewall) diff --git a/exercises/mirroring/Makefile b/exercises/mirroring/Makefile new file mode 100644 index 000000000..23bdc5b99 --- /dev/null +++ b/exercises/mirroring/Makefile @@ -0,0 +1,3 @@ +BMV2_SWITCH_EXE = simple_switch_grpc + +include ../../utils/Makefile diff --git a/exercises/mirroring/README.md b/exercises/mirroring/README.md new file mode 100644 index 000000000..da4e3c6eb --- /dev/null +++ b/exercises/mirroring/README.md @@ -0,0 +1,119 @@ +# Port Mirroring + +## Introduction + +The objective of this exercise is to write a P4 program that mirrors all the incoming packets to a specific port where a collector is located at. + +Upon receiving a packet, your program should make a copy of the corresponding packet and send it to the collector host. Your switch will have a single table, which we have populated with static rules. You will only need to implement the logic for cloning packets. + +We will use a simple topology for this exercise. It is a single switch connected to three hosts, h1, h2 and h3 where h3 acts as the collector connected to the mirroring port (port 3) as follow: + +``` + h1 h3 (Collector) + \ / + \ / + s1 + / + / + h2 + +``` +> **Spoiler alert:** There is a reference solution in the `solution` +> sub-directory. Feel free to compare your implementation to the +> reference. + +## Step 1: Run the (incomplete) starter code + +The directory with this README also contains a skeleton P4 program, +`mirroring.p4`, which forwards packets between h1 and h2. Your job will be to +extend this skeleton program to mirror all the packets to the collector host, h3. + +Before that, let's compile the incomplete `mirroring.p4` and bring +up a switch in Mininet to test its behavior. + +1. In your shell, run: + ```bash + make run + ``` + This will: + * compile `mirroring.p4`, and + * start the topology in Mininet and configure all switches with + the appropriate P4 program + table entries, and + * configure all hosts with the commands listed in + [topology.json](topology.json) + +2. You should now see a Mininet command prompt. Bring up the terminal for h3. + ```bash + mininet> Xterm h3 + ``` + Since this is the collector host, run `tcpdump` to observe incoming mirrored packets. + ``` + root@p4:~/tutorials/exercise/mirroring# tcpdump -i eth0 + ``` + +2. You should now see a Mininet command prompt. Try to ping between + hosts in the topology: + ```bash + mininet> h1 ping h2 + mininet> pingall + ``` + If the packets are mirrored properly, you should observe the corresponding packets on `tcpdump`. +3. Type `exit` to leave each xterm and the Mininet command line. + Then, to stop mininet: + ```bash + make stop + ``` + And to delete all pcaps, build files, and logs: + ```bash + make clean + ``` + +No packets should be received by h3, since the `mirror` action is not implemented yet. +Your job is to extend this file so it mirrors packets to the collector host. + +## Step 2: Implement Port Mirroring +1. **TODO:** An action (called `mirror`) that: + 1. Invokes the `clone` extern of the V1Model. + 2. Passes the appropriate CloneType and session ID as the parameter to the `clone` method. +2. **TODO:** Call the `mirror` action in your program so that it mirrors all arriving packets. +3. **TODO:** Add port 3 to your specified session. + 1. In a new terminal, start the `simple_switch_CLI` + 2. Execute the command `mirroring_add` followed the session ID the port number. + +## Step 3: Run your solution + +Follow the instructions from Step 1. This time, you should be able to +observe packets being mirrored to h3. And, you're done! + +### Useful Resources +Check out the resources below that contains further details/explanations on `clone/clone3`. +- [V1Model](https://github.com/p4lang/p4c/blob/master/p4include/v1model.p4) +- [BMv2](https://github.com/p4lang/behavioral-model/blob/master/docs/simple_switch.md) +- Guide on [V1Model Special Ops](https://github.com/jafingerhut/p4-guide/blob/master/v1model-special-ops/v1model-special-ops.p4) + +### Food for thought + +Questions to consider: +- + - What is the difference between clone and clone3? + +### Troubleshooting + +There are several problems that might manifest as you develop your program: + +1. `mirroring.p4` might fail to compile. In this case, `make run` will +report the error emitted from the compiler and halt. + +2. `mirroring.p4` might compile, but the switch might not process packets in the desired +way. The `logs/s1.log` file contain detailed logs describing how each switch processes each packet. The output is +detailed and can help pinpoint logic errors in your implementation. + +#### Cleaning up Mininet + +In the latter two cases above, `make run` may leave a Mininet instance +running in the background. Use the following command to clean up +these instances: + +```bash +make stop +``` \ No newline at end of file diff --git a/exercises/mirroring/include/headers.p4 b/exercises/mirroring/include/headers.p4 new file mode 100644 index 000000000..2cc918831 --- /dev/null +++ b/exercises/mirroring/include/headers.p4 @@ -0,0 +1,36 @@ +/************************************************************************* +*********************** 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 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 { +} + +struct headers { + ethernet_t ethernet; + ipv4_t ipv4; +} \ No newline at end of file diff --git a/exercises/mirroring/include/parsers.p4 b/exercises/mirroring/include/parsers.p4 new file mode 100644 index 000000000..3a4fc6740 --- /dev/null +++ b/exercises/mirroring/include/parsers.p4 @@ -0,0 +1,28 @@ +const bit<16> TYPE_IPV4 = 0x800; + +/************************************************************************* +*********************** 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_IPV4 : parse_ipv4; + } + } + + state parse_ipv4 { + packet.extract(hdr.ipv4); + transition accept; + } + +} \ No newline at end of file diff --git a/exercises/mirroring/mirroring.p4 b/exercises/mirroring/mirroring.p4 new file mode 100644 index 000000000..c0d254955 --- /dev/null +++ b/exercises/mirroring/mirroring.p4 @@ -0,0 +1,100 @@ +#include +#include + +#include "include/headers.p4" +#include "include/parsers.p4" + +/************************************************************************* +************ 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) { + + // TODO: Identify the CloneType to be used + // TODO: Define `mirror` action to clone incoming packets to the + // mirroring port (port 3) of a specific session. + + action drop() { + mark_to_drop(standard_metadata); + } + + action normal_forward(egressSpec_t port) { + standard_metadata.egress_spec = port; + } + + table lpm_forward { + key = { + hdr.ipv4.dstAddr : lpm; + } + actions = { + normal_forward; + NoAction; + } + default_action = NoAction; + const entries = { + 0x0a000001 &&& 0xFFFFFFFF : normal_forward(9w1); + 0x0a000002 &&& 0xFFFFFFFF : normal_forward(9w2); + } + } + + apply { + + // TODO: Call `mirror` action to mirror packets + + if(hdr.ipv4.isValid()){ + lpm_forward.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 { } +} + +/************************************************************************* +*********************** D E P A R S E R ******************************* +*************************************************************************/ + +control MyDeparser(packet_out packet, in headers hdr) { + apply { + packet.emit(hdr.ethernet); + packet.emit(hdr.ipv4); + } +} + +/************************************************************************* +*********************** S W I T C H ******************************* +*************************************************************************/ + +V1Switch( +MyParser(), +MyVerifyChecksum(), +MyIngress(), +MyEgress(), +MyComputeChecksum(), +MyDeparser() +) main; diff --git a/exercises/mirroring/solution/commands b/exercises/mirroring/solution/commands new file mode 100644 index 000000000..cf5143c76 --- /dev/null +++ b/exercises/mirroring/solution/commands @@ -0,0 +1 @@ +mirroring_add 0 3 \ No newline at end of file diff --git a/exercises/mirroring/solution/mirroring.p4 b/exercises/mirroring/solution/mirroring.p4 new file mode 100644 index 000000000..7faf4cdfc --- /dev/null +++ b/exercises/mirroring/solution/mirroring.p4 @@ -0,0 +1,104 @@ +#include +#include + +#include "include/headers.p4" +#include "include/parsers.p4" + +/************************************************************************* +************ 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) { + + // TODO: Identify the CloneType to be used + // TODO: Define `mirror` action to clone incoming packets to the + // mirroring port (port 3) of session 0. + action mirror() { + clone(CloneType.I2E, 0); // We'll be using session 0. + } + + action drop() { + mark_to_drop(standard_metadata); + } + + action normal_forward(egressSpec_t port) { + standard_metadata.egress_spec = port; + } + + table lpm_forward { + key = { + hdr.ipv4.dstAddr : lpm; + } + actions = { + normal_forward; + NoAction; + } + default_action = NoAction; + const entries = { + 0x0a000001 &&& 0xFFFFFFFF : normal_forward(9w1); + 0x0a000002 &&& 0xFFFFFFFF : normal_forward(9w2); + } + } + + apply { + + // TODO: Call `mirror` action to mirror packets + mirror(); // For all packets, it will be mirrored. + + if(hdr.ipv4.isValid()){ + lpm_forward.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 { } +} + +/************************************************************************* +*********************** D E P A R S E R ******************************* +*************************************************************************/ + +control MyDeparser(packet_out packet, in headers hdr) { + apply { + packet.emit(hdr.ethernet); + packet.emit(hdr.ipv4); + } +} + +/************************************************************************* +*********************** S W I T C H ******************************* +*************************************************************************/ + +V1Switch( +MyParser(), +MyVerifyChecksum(), +MyIngress(), +MyEgress(), +MyComputeChecksum(), +MyDeparser() +) main; diff --git a/exercises/mirroring/topology.json b/exercises/mirroring/topology.json new file mode 100644 index 000000000..6bd0ede0f --- /dev/null +++ b/exercises/mirroring/topology.json @@ -0,0 +1,23 @@ +{ + "hosts": { + "h1": { + "ip": "10.0.0.1/24", "mac": "08:00:00:00:01:11", + "commands":["arp -i eth0 -s 10.0.0.2 08:00:00:00:02:22"] + }, + "h2": { + "ip": "10.0.0.2/24", "mac": "08:00:00:00:02:22", + "commands":["arp -i eth0 -s 10.0.0.1 08:00:00:00:01:11"] + }, + "h3": { + "ip": "10.0.0.3/24", "mac": "08:00:00:00:03:33", + "commands": ["arp -i eth0 -s 10.0.0.3 08:00:00:00:01:11", + "arp -i eth0 -s 10.0.0.2 08:00:00:00:02:22"] + } + }, + "switches": { + "s1": { } + }, + "links": [ + ["h1", "s1-p1"], ["h2", "s1-p2"], ["h3", "s1-p3"] + ] +} diff --git a/exercises/mirroring/utils/Makefile b/exercises/mirroring/utils/Makefile new file mode 100644 index 000000000..2846b612b --- /dev/null +++ b/exercises/mirroring/utils/Makefile @@ -0,0 +1,50 @@ +BUILD_DIR = build +PCAP_DIR = pcaps +LOG_DIR = logs + +P4C = p4c-bm2-ss +P4C_ARGS += --p4runtime-files $(BUILD_DIR)/$(basename $@).p4.p4info.txt + +RUN_SCRIPT = ../../utils/run_exercise.py + +ifndef TOPO +TOPO = topology.json +endif + +source = $(wildcard *.p4) +compiled_json := $(source:.p4=.json) + +ifndef DEFAULT_PROG +DEFAULT_PROG = $(wildcard *.p4) +endif +DEFAULT_JSON = $(BUILD_DIR)/$(DEFAULT_PROG:.p4=.json) + +# Define NO_P4 to start BMv2 without a program +ifndef NO_P4 +run_args += -j $(DEFAULT_JSON) +endif + +# Set BMV2_SWITCH_EXE to override the BMv2 target +ifdef BMV2_SWITCH_EXE +run_args += -b $(BMV2_SWITCH_EXE) +endif + +all: run + +run: build + sudo python $(RUN_SCRIPT) -t $(TOPO) $(run_args) + +stop: + sudo mn -c + +build: dirs $(compiled_json) + +%.json: %.p4 + $(P4C) --p4v 16 $(P4C_ARGS) -o $(BUILD_DIR)/$@ $< + +dirs: + mkdir -p $(BUILD_DIR) $(PCAP_DIR) $(LOG_DIR) + +clean: stop + rm -f *.pcap + rm -rf $(BUILD_DIR) $(PCAP_DIR) $(LOG_DIR) diff --git a/exercises/mirroring/utils/cheat_sheet_src/main.tex b/exercises/mirroring/utils/cheat_sheet_src/main.tex new file mode 100644 index 000000000..f9db04cb5 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/main.tex @@ -0,0 +1,220 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P4 Cheat Sheet +% +% By P4.org +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +\documentclass{article} + +\usepackage{fontspec} +\setmainfont{Utopia} +\setsansfont{Free Helvetian} +\setmonofont{Liberation Mono} + +\usepackage[landscape]{geometry} +\usepackage{url} +\usepackage{multicol} +\usepackage{amsmath} +\usepackage{amsfonts} +\usepackage{tikz} +\usetikzlibrary{shapes} +\usetikzlibrary{decorations.pathmorphing} +\usepackage{amsmath,amssymb} + +\usepackage{colortbl} +\usepackage{xcolor} +\usepackage{mathtools} +\usepackage{amsmath,amssymb} +\usepackage{enumitem} + +% Define Colors +\usepackage{color} +\definecolor{eclipseBlue}{RGB}{42,0.0,255} +\definecolor{eclipseGreen}{RGB}{63,127,95} +\definecolor{eclipsePurple}{RGB}{127,0,85} + +\usepackage{listings} + +% Define Language +\lstdefinelanguage{p4} +{ + % list of keywords + morekeywords={ + action, apply, bit, bool, const, control, default, else, enum, error, extern, exit, false, header, if, in, inout, int, match_kind, package, parser, out, return, select, state, struct, switch, table, transition, true, tuple, typedef, varbit, verify, void, + }, + sensitive=true, % keywords are case-sensitive + morecomment=[l]{//}, % l is for line comment + morecomment=[s]{/*}{*/}, % s is for start and end delimiter + morestring=[b]" % defines that strings are enclosed in double quotes +} + +% Set Language +\lstset{ + language={p4}, + basicstyle=\small\ttfamily, % Global Code Style + captionpos=b, % Position of the Caption (t for top, b for bottom) + extendedchars=true, % Allows 256 instead of 128 ASCII characters + tabsize=2, % number of spaces indented when discovering a tab + columns=fixed, % make all characters equal width + keepspaces=true, % does not ignore spaces to fit width, convert tabs to spaces + showstringspaces=false, % lets spaces in strings appear as real spaces + breaklines=true, % wrap lines if they don't fit + commentstyle=\color{eclipseBlue}, % style of comments + keywordstyle=\color{eclipsePurple}, % style of keywords + stringstyle=\color{eclipseGreen}, % style of strings +} + +\title{P4 Cheat Sheet} +\usepackage[brazilian]{babel} + +\advance\topmargin-.8in +\advance\textheight3in +\advance\textwidth3in +\advance\oddsidemargin-1.5in +\advance\evensidemargin-1.5in +\parindent0pt +\parskip2pt +\newcommand{\hr}{\centerline{\rule{3.5in}{1pt}}} +%\colorbox[HTML]{e4e4e4}{\makebox[\textwidth-2\fboxsep][l]{texto} +\begin{document} + +\begin{center}{\huge{\bf \textsf{P4 Language Cheat Sheet}}}\\[.5em] +%{\large By P4.org} +\end{center} +\begin{multicols*}{3} + +\tikzstyle{mybox} = [draw=black, fill=white, very thick, + rectangle, rounded corners, inner sep=10pt, inner ysep=10pt] +\tikzstyle{fancytitle} =[fill=black, text=white, font=\bfseries] +\tikzstyle{mybox2} = [draw=black, fill=white, very thick, rectangle split, + rectangle split parts=2, + rounded corners, inner sep=10pt, inner ysep=10pt] +\tikzstyle{fancytitle2} =[fill=black, text=white, font=\bfseries] + +%------------ DATA TYPES --------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/data_types.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Basic Data Types}; +\end{tikzpicture} + +%------------ P4 Parsing --------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/parsers.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Parsing}; +\end{tikzpicture} + +%------------ Expressions --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/expressions.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Statements \& Expressions}; +\end{tikzpicture} + +%------------ Actions --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/actions.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Actions}; +\end{tikzpicture} + +%------------ Tables --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/tables.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Tables}; +\end{tikzpicture} + +%------------ Control Flow --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/control_flow.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Control Flow}; +\end{tikzpicture} + +%------------ Deparsing --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/deparsing.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Deparsing}; +\end{tikzpicture} + +%------------ Header Stacks --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/header_stack.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Header Stacks}; +\end{tikzpicture} + +%------------ Advanced Parsing --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/adv_parsing.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {Advanced Parsing}; +\end{tikzpicture} + +%------------ V1Model - Architecture --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/architecture.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {V1Model - Architecture}; +\end{tikzpicture} + +%------------ V1Model - Standard Metadata --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/v1model_std_metadata.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {V1Model - Standard Metadata}; +\end{tikzpicture} + +%------------ V1Model - Counter Externs --------------------- +\begin{tikzpicture} +\node [mybox] (box){% + \begin{minipage}{0.3\textwidth} + \lstinputlisting{src/counters.txt} + \end{minipage} +}; +\node[fancytitle, right=10pt] at (box.north west) {V1Model - Counters \& Registers}; +\end{tikzpicture} + + + +\end{multicols*} +\end{document} +Contact GitHub API Training Shop Blog About +© 2016 GitHub, Inc. Terms Privacy Security Status Help \ No newline at end of file diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/actions.txt b/exercises/mirroring/utils/cheat_sheet_src/src/actions.txt new file mode 100644 index 000000000..63801a7b5 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/actions.txt @@ -0,0 +1,28 @@ +// Inputs provided by control-plane +action set_next_hop(bit<32> next_hop) { + if (next_hop == 0) { + metadata.next_hop = hdr.ipv4.dst; + } else { + metadata.next_hop = next_hop; + } +} + +// Inputs provided by data-plane +action swap_mac(inout bit<48> x, + inout bit<48> y) { + bit<48> tmp = x; + x = y; + y = tmp; +} + +// Inputs provided by control/data-plane +action forward(in bit<9> p, bit<48> d) { + standard_metadata.egress_spec = p; + headers.ethernet.dstAddr = d; +} + +// Remove header from packet +action decap_ip_ip() { + hdr.ipv4 = hdr.inner_ipv4; + hdr.inner_ipv4.setInvalid(); +} diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/adv_parsing.txt b/exercises/mirroring/utils/cheat_sheet_src/src/adv_parsing.txt new file mode 100644 index 000000000..2b1167b3a --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/adv_parsing.txt @@ -0,0 +1,23 @@ +// common defns for IPv4 and IPv6 +header ip46_t { + bit<4> version; + bit<4> reserved; +} + +// header stack parsing +state parse_labels { + packet.extract(hdr.labels.next); + transition select(hdr.labels.last.bos) { + 0: parse_labels; // create loop + 1: guess_labels_payload; + } +} + +// lookahead parsing +state guess_labels_payload { + transition select(packet.lookahead().version) { + 4 : parse_inner_ipv4; + 6 : parse_inner_ipv6; + default : parse_inner_ethernet; + } +} diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/architecture.txt b/exercises/mirroring/utils/cheat_sheet_src/src/architecture.txt new file mode 100644 index 000000000..5fb4010f3 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/architecture.txt @@ -0,0 +1,46 @@ +// common externs +extern void truncate(in bit<32> length); +extern void resubmit(in T x); +extern void recirculate(in T x); +enum CloneType { I2E, E2I } +extern void clone(in CloneType type, + in bit<32> session); + +// v1model pipeline elements +parser Parser( + packet_in pkt, + out H hdr, + inout M meta, + inout standard_metadata_t std_meta +); +control VerifyChecksum( + inout H hdr, + inout M meta +); +control Ingress( + inout H hdr, + inout M meta, + inout standard_metadata_t std_meta +); +control Egress( + inout H hdr, + inout M meta, + inout standard_metadata_t std_meta +); +control ComputeChecksum( + inout H hdr, + inout M meta +); +control Deparser( + packet_out b, in H hdr +); + +// v1model switch +package V1Switch( + Parser p, + VerifyChecksum vr, + Ingress ig, + Egress eg, + ComputeChecksum ck, + Deparser d +); diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/control_flow.txt b/exercises/mirroring/utils/cheat_sheet_src/src/control_flow.txt new file mode 100644 index 000000000..95d83e232 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/control_flow.txt @@ -0,0 +1,15 @@ +apply { + // branch on header validity + if (hdr.ipv4.isValid()) { + ipv4_lpm.apply(); + } + // branch on table hit result + if (local_ip_table.apply().hit) { + send_to_cpu(); + } + // branch on table action invocation + switch (table1.apply().action_run) { + action1: { table2.apply(); } + action2: { table3.apply(); } + } +} \ No newline at end of file diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/counters.txt b/exercises/mirroring/utils/cheat_sheet_src/src/counters.txt new file mode 100644 index 000000000..1583eea28 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/counters.txt @@ -0,0 +1,19 @@ +// counters +counter(8192, CounterType.packets) c; + +action count(bit<32> index) { + //increment counter at index + c.count(index); +} + +// registers +register>(16384) r; + +action ipg(out bit<48> ival, bit<32> x) { + bit<48> last; + bit<48> now; + r.read(last, x); + now = std_meta.ingress_global_timestamp; + ival = now - last; + r.write(x, now); +} diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/data_types.txt b/exercises/mirroring/utils/cheat_sheet_src/src/data_types.txt new file mode 100644 index 000000000..7029d02c7 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/data_types.txt @@ -0,0 +1,22 @@ +// typedef: introduces alternate type name +typedef bit<48> macAddr_t; +typedef bit<32> ip4Addr_t; + +// headers: ordered collection of members +// operations test and set validity bits: +// isValid(), setValid(), setInvalid() +header ethernet_t { + macAddr_t dstAddr; + macAddr_t srcAddr; + bit<16> type; +} + +// variable declaration and member access +ethernet_t ethernet; +macAddr_t src = ethernet.srcAddr; + +// struct: unordered collection of members +struct headers_t { + ethernet_t ethernet; +} + diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/deparsing.txt b/exercises/mirroring/utils/cheat_sheet_src/src/deparsing.txt new file mode 100644 index 000000000..f6468dc14 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/deparsing.txt @@ -0,0 +1,9 @@ +// packet_out: extern for output packet +extern packet_out { + void emit(in T hdr); +} + +apply { + // insert headers into pkt if valid + packet.emit(hdr.ethernet); +} \ No newline at end of file diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/expressions.txt b/exercises/mirroring/utils/cheat_sheet_src/src/expressions.txt new file mode 100644 index 000000000..5c1688d31 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/expressions.txt @@ -0,0 +1,13 @@ +// Local metadata declaration, assignment +bit<16> tmp1; bit<16> tmp2; +tmp1 = hdr.ethernet.type; + +// bit slicing, concatenation +tmp2 = tmp1[7:0] ++ tmp1[15:8]; + +// addition, subtraction, casts +tmp2 = tmp1 + tmp1 - (bit<16>)tmp1[7:0]; + +// bitwise operators +tmp2 = (~tmp1 & tmp1) | (tmp1 ^ tmp1); +tmp2 = tmp1 << 3; \ No newline at end of file diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/header_stack.txt b/exercises/mirroring/utils/cheat_sheet_src/src/header_stack.txt new file mode 100644 index 000000000..85328f906 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/header_stack.txt @@ -0,0 +1,21 @@ +// header stack declaration +header label_t { + bit<20> label; + bit bos; +} +struct header_t { + label_t[10] labels; +} +header_t hdr; + +// remove from header stack +action pop_label() { + hdr.labels.pop_front(1); +} + +// add to header stack +action push_label(in bit<20> label) { + hdr.labels.push_front(1); + hdr.labels[0].setValid(); + hdr.labels[0] = { label, 0}; +} diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/parsers.txt b/exercises/mirroring/utils/cheat_sheet_src/src/parsers.txt new file mode 100644 index 000000000..8fbe6edd3 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/parsers.txt @@ -0,0 +1,22 @@ +// packet_in: extern for input packet +extern packet_in { + void extract(out T hdr); + void extract(out T hdr,in bit<32> n); + T lookahead(); + void advance(in bit<32> n); + bit<32> length(); +} + +// parser: begins in special "start" state +state start { + transition parse_ethernet; +} + +// User-defined parser state +state parse_ethernet { + packet.extract(hdr.ethernet); + transition select(hdr.ethernet.type) { + 0x800: parse_ipv4; + default: accept; + } +} \ No newline at end of file diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/tables.txt b/exercises/mirroring/utils/cheat_sheet_src/src/tables.txt new file mode 100644 index 000000000..c7df56ee6 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/tables.txt @@ -0,0 +1,16 @@ +table ipv4_lpm { + key = { + hdr.ipv4.dstAddr : lpm; + // standard match kinds: + // exact, ternary, lpm + } + // actions that can be invoked + actions = { + ipv4_forward; + drop; + NoAction; + } + // table properties + size = 1024; + default_action = NoAction(); +} \ No newline at end of file diff --git a/exercises/mirroring/utils/cheat_sheet_src/src/v1model_std_metadata.txt b/exercises/mirroring/utils/cheat_sheet_src/src/v1model_std_metadata.txt new file mode 100644 index 000000000..5b1022677 --- /dev/null +++ b/exercises/mirroring/utils/cheat_sheet_src/src/v1model_std_metadata.txt @@ -0,0 +1,22 @@ +struct standard_metadata_t { + bit<9> ingress_port; + bit<9> egress_spec; + bit<9> egress_port; + bit<32> clone_spec; + bit<32> instance_type; + bit<1> drop; + bit<16> recirculate_port; + bit<32> packet_length; + bit<32> enq_timestamp; + bit<19> enq_qdepth; + bit<32> deq_timedelta; + bit<19> deq_qdepth; + bit<48> ingress_global_timestamp; + bit<48> egress_global_timestamp; + bit<32> lf_field_list; + bit<16> mcast_grp; + bit<32> resubmit_flag; + bit<16> egress_rid; + bit<1> checksum_error; + bit<32> recirculate_flag; +} \ No newline at end of file diff --git a/exercises/mirroring/utils/mininet/appcontroller.py b/exercises/mirroring/utils/mininet/appcontroller.py new file mode 100644 index 000000000..2cf97323b --- /dev/null +++ b/exercises/mirroring/utils/mininet/appcontroller.py @@ -0,0 +1,104 @@ +import subprocess + +from shortest_path import ShortestPath + +class AppController: + + def __init__(self, manifest=None, target=None, topo=None, net=None, links=None): + self.manifest = manifest + self.target = target + self.conf = manifest['targets'][target] + self.topo = topo + self.net = net + self.links = links + + def read_entries(self, filename): + entries = [] + with open(filename, 'r') as f: + for line in f: + line = line.strip() + if line == '': continue + entries.append(line) + return entries + + def add_entries(self, thrift_port=9090, sw=None, entries=None): + assert entries + if sw: thrift_port = sw.thrift_port + + print '\n'.join(entries) + p = subprocess.Popen(['simple_switch_CLI', '--thrift-port', str(thrift_port)], stdin=subprocess.PIPE) + p.communicate(input='\n'.join(entries)) + + def read_register(self, register, idx, thrift_port=9090, sw=None): + if sw: thrift_port = sw.thrift_port + p = subprocess.Popen(['simple_switch_CLI', '--thrift-port', str(thrift_port)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate(input="register_read %s %d" % (register, idx)) + reg_val = filter(lambda l: ' %s[%d]' % (register, idx) in l, stdout.split('\n'))[0].split('= ', 1)[1] + return long(reg_val) + + def start(self): + shortestpath = ShortestPath(self.links) + entries = {} + for sw in self.topo.switches(): + entries[sw] = [] + if 'switches' in self.conf and sw in self.conf['switches'] and 'entries' in self.conf['switches'][sw]: + extra_entries = self.conf['switches'][sw]['entries'] + if type(extra_entries) == list: # array of entries + entries[sw] += extra_entries + else: # path to file that contains entries + entries[sw] += self.read_entries(extra_entries) + #entries[sw] += [ + # 'table_set_default send_frame _drop', + # 'table_set_default forward _drop', + # 'table_set_default ipv4_lpm _drop'] + + for host_name in self.topo._host_links: + h = self.net.get(host_name) + for link in self.topo._host_links[host_name].values(): + sw = link['sw'] + #entries[sw].append('table_add send_frame rewrite_mac %d => %s' % (link['sw_port'], link['sw_mac'])) + #entries[sw].append('table_add forward set_dmac %s => %s' % (link['host_ip'], link['host_mac'])) + #entries[sw].append('table_add ipv4_lpm set_nhop %s/32 => %s %d' % (link['host_ip'], link['host_ip'], link['sw_port'])) + iface = h.intfNames()[link['idx']] + # use mininet to set ip and mac to let it know the change + h.setIP(link['host_ip'], 24) + h.setMAC(link['host_mac']) + #h.cmd('ifconfig %s %s hw ether %s' % (iface, link['host_ip'], link['host_mac'])) + h.cmd('arp -i %s -s %s %s' % (iface, link['sw_ip'], link['sw_mac'])) + h.cmd('ethtool --offload %s rx off tx off' % iface) + h.cmd('ip route add %s dev %s' % (link['sw_ip'], iface)) + h.setDefaultRoute("via %s" % link['sw_ip']) + + for h in self.net.hosts: + h_link = self.topo._host_links[h.name].values()[0] + for sw in self.net.switches: + path = shortestpath.get(sw.name, h.name, exclude=lambda n: n[0]=='h') + if not path: continue + if not path[1][0] == 's': continue # next hop is a switch + sw_link = self.topo._sw_links[sw.name][path[1]] + #entries[sw.name].append('table_add send_frame rewrite_mac %d => %s' % (sw_link[0]['port'], sw_link[0]['mac'])) + #entries[sw.name].append('table_add forward set_dmac %s => %s' % (h_link['host_ip'], sw_link[1]['mac'])) + #entries[sw.name].append('table_add ipv4_lpm set_nhop %s/32 => %s %d' % (h_link['host_ip'], h_link['host_ip'], sw_link[0]['port'])) + + for h2 in self.net.hosts: + if h == h2: continue + path = shortestpath.get(h.name, h2.name, exclude=lambda n: n[0]=='h') + if not path: continue + h_link = self.topo._host_links[h.name][path[1]] + h2_link = self.topo._host_links[h2.name].values()[0] + h.cmd('ip route add %s via %s' % (h2_link['host_ip'], h_link['sw_ip'])) + + + print "**********" + print "Configuring entries in p4 tables" + for sw_name in entries: + print + print "Configuring switch... %s" % sw_name + sw = self.net.get(sw_name) + if entries[sw_name]: + self.add_entries(sw=sw, entries=entries[sw_name]) + print "Configuration complete." + print "**********" + + def stop(self): + pass diff --git a/exercises/mirroring/utils/mininet/apptopo.py b/exercises/mirroring/utils/mininet/apptopo.py new file mode 100644 index 000000000..3491a3d29 --- /dev/null +++ b/exercises/mirroring/utils/mininet/apptopo.py @@ -0,0 +1,70 @@ +from mininet.topo import Topo + +class AppTopo(Topo): + + def __init__(self, links, latencies={}, manifest=None, target=None, + log_dir="/tmp", bws={}, **opts): + Topo.__init__(self, **opts) + + nodes = sum(map(list, zip(*links)), []) + host_names = sorted(list(set(filter(lambda n: n[0] == 'h', nodes)))) + sw_names = sorted(list(set(filter(lambda n: n[0] == 's', nodes)))) + sw_ports = dict([(sw, []) for sw in sw_names]) + + self._host_links = {} + self._sw_links = dict([(sw, {}) for sw in sw_names]) + + for sw_name in sw_names: + self.addSwitch(sw_name, log_file="%s/%s.log" %(log_dir, sw_name)) + + for host_name in host_names: + host_num = int(host_name[1:]) + + self.addHost(host_name) + + self._host_links[host_name] = {} + host_links = filter(lambda l: l[0]==host_name or l[1]==host_name, links) + + sw_idx = 0 + for link in host_links: + sw = link[0] if link[0] != host_name else link[1] + sw_num = int(sw[1:]) + assert sw[0]=='s', "Hosts should be connected to switches, not " + str(sw) + host_ip = "10.0.%d.%d" % (sw_num, host_num) + host_mac = '00:00:00:00:%02x:%02x' % (sw_num, host_num) + delay_key = ''.join([host_name, sw]) + delay = latencies[delay_key] if delay_key in latencies else '0ms' + bw = bws[delay_key] if delay_key in bws else None + sw_ports[sw].append(host_name) + self._host_links[host_name][sw] = dict( + idx=sw_idx, + host_mac = host_mac, + host_ip = host_ip, + sw = sw, + sw_mac = "00:00:00:00:%02x:%02x" % (sw_num, host_num), + sw_ip = "10.0.%d.%d" % (sw_num, 254), + sw_port = sw_ports[sw].index(host_name)+1 + ) + self.addLink(host_name, sw, delay=delay, bw=bw, + addr1=host_mac, addr2=self._host_links[host_name][sw]['sw_mac']) + sw_idx += 1 + + for link in links: # only check switch-switch links + sw1, sw2 = link + if sw1[0] != 's' or sw2[0] != 's': continue + + delay_key = ''.join(sorted([sw1, sw2])) + delay = latencies[delay_key] if delay_key in latencies else '0ms' + bw = bws[delay_key] if delay_key in bws else None + + self.addLink(sw1, sw2, delay=delay, bw=bw)#, max_queue_size=10) + sw_ports[sw1].append(sw2) + sw_ports[sw2].append(sw1) + + sw1_num, sw2_num = int(sw1[1:]), int(sw2[1:]) + sw1_port = dict(mac="00:00:00:%02x:%02x:00" % (sw1_num, sw2_num), port=sw_ports[sw1].index(sw2)+1) + sw2_port = dict(mac="00:00:00:%02x:%02x:00" % (sw2_num, sw1_num), port=sw_ports[sw2].index(sw1)+1) + + self._sw_links[sw1][sw2] = [sw1_port, sw2_port] + self._sw_links[sw2][sw1] = [sw2_port, sw1_port] + diff --git a/exercises/mirroring/utils/mininet/multi_switch_mininet.py b/exercises/mirroring/utils/mininet/multi_switch_mininet.py new file mode 100755 index 000000000..0bb406f61 --- /dev/null +++ b/exercises/mirroring/utils/mininet/multi_switch_mininet.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python2 + +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import signal +import os +import sys +import subprocess +import argparse +import json +import importlib +import re +from time import sleep + +from mininet.net import Mininet +from mininet.topo import Topo +from mininet.link import TCLink +from mininet.log import setLogLevel, info +from mininet.cli import CLI + +from p4_mininet import P4Switch, P4Host +import apptopo +import appcontroller + +parser = argparse.ArgumentParser(description='Mininet demo') +parser.add_argument('--behavioral-exe', help='Path to behavioral executable', + type=str, action="store", required=True) +parser.add_argument('--thrift-port', help='Thrift server port for table updates', + type=int, action="store", default=9090) +parser.add_argument('--bmv2-log', help='verbose messages in log file', action="store_true") +parser.add_argument('--cli', help="start the mininet cli", action="store_true") +parser.add_argument('--auto-control-plane', help='enable automatic control plane population', action="store_true") +parser.add_argument('--json', help='Path to JSON config file', + type=str, action="store", required=True) +parser.add_argument('--pcap-dump', help='Dump packets on interfaces to pcap files', + action="store_true") +parser.add_argument('--manifest', '-m', help='Path to manifest file', + type=str, action="store", required=True) +parser.add_argument('--target', '-t', help='Target in manifest file to run', + type=str, action="store", required=True) +parser.add_argument('--log-dir', '-l', help='Location to save output to', + type=str, action="store", required=True) +parser.add_argument('--cli-message', help='Message to print before starting CLI', + type=str, action="store", required=False, default=False) + + +args = parser.parse_args() + + +next_thrift_port = args.thrift_port + +def run_command(command): + return os.WEXITSTATUS(os.system(command)) + +def configureP4Switch(**switch_args): + class ConfiguredP4Switch(P4Switch): + def __init__(self, *opts, **kwargs): + global next_thrift_port + kwargs.update(switch_args) + kwargs['thrift_port'] = next_thrift_port + next_thrift_port += 1 + P4Switch.__init__(self, *opts, **kwargs) + return ConfiguredP4Switch + + +def main(): + + with open(args.manifest, 'r') as f: + manifest = json.load(f) + + conf = manifest['targets'][args.target] + params = conf['parameters'] if 'parameters' in conf else {} + + os.environ.update(dict(map(lambda (k,v): (k, str(v)), params.iteritems()))) + + def formatParams(s): + for param in params: + s = re.sub('\$'+param+'(\W|$)', str(params[param]) + r'\1', s) + s = s.replace('${'+param+'}', str(params[param])) + return s + + AppTopo = apptopo.AppTopo + AppController = appcontroller.AppController + + if 'topo_module' in conf: + sys.path.insert(0, os.path.dirname(args.manifest)) + topo_module = importlib.import_module(conf['topo_module']) + AppTopo = topo_module.CustomAppTopo + + if 'controller_module' in conf: + sys.path.insert(0, os.path.dirname(args.manifest)) + controller_module = importlib.import_module(conf['controller_module']) + AppController = controller_module.CustomAppController + + if not os.path.isdir(args.log_dir): + if os.path.exists(args.log_dir): raise Exception('Log dir exists and is not a dir') + os.mkdir(args.log_dir) + os.environ['P4APP_LOGDIR'] = args.log_dir + + + links = [l[:2] for l in conf['links']] + latencies = dict([(''.join(sorted(l[:2])), l[2]) for l in conf['links'] if len(l)>=3]) + bws = dict([(''.join(sorted(l[:2])), l[3]) for l in conf['links'] if len(l)>=4]) + + for host_name in sorted(conf['hosts'].keys()): + host = conf['hosts'][host_name] + if 'latency' not in host: continue + for a, b in links: + if a != host_name and b != host_name: continue + other = a if a != host_name else b + latencies[host_name+other] = host['latency'] + + for l in latencies: + if isinstance(latencies[l], (str, unicode)): + latencies[l] = formatParams(latencies[l]) + else: + latencies[l] = str(latencies[l]) + "ms" + + bmv2_log = args.bmv2_log or ('bmv2_log' in conf and conf['bmv2_log']) + pcap_dump = args.pcap_dump or ('pcap_dump' in conf and conf['pcap_dump']) + + topo = AppTopo(links, latencies, manifest=manifest, target=args.target, + log_dir=args.log_dir, bws=bws) + switchClass = configureP4Switch( + sw_path=args.behavioral_exe, + json_path=args.json, + log_console=bmv2_log, + pcap_dump=pcap_dump) + net = Mininet(topo = topo, + link = TCLink, + host = P4Host, + switch = switchClass, + controller = None) + net.start() + + sleep(1) + + controller = None + if args.auto_control_plane or 'controller_module' in conf: + controller = AppController(manifest=manifest, target=args.target, + topo=topo, net=net, links=links) + controller.start() + + + for h in net.hosts: + h.describe() + + if args.cli_message is not None: + with open(args.cli_message, 'r') as message_file: + print message_file.read() + + if args.cli or ('cli' in conf and conf['cli']): + CLI(net) + + stdout_files = dict() + return_codes = [] + host_procs = [] + + + def formatCmd(cmd): + for h in net.hosts: + cmd = cmd.replace(h.name, h.defaultIntf().updateIP()) + return cmd + + def _wait_for_exit(p, host): + print p.communicate() + if p.returncode is None: + p.wait() + print p.communicate() + return_codes.append(p.returncode) + if host_name in stdout_files: + stdout_files[host_name].flush() + stdout_files[host_name].close() + + print '\n'.join(map(lambda (k,v): "%s: %s"%(k,v), params.iteritems())) + '\n' + + for host_name in sorted(conf['hosts'].keys()): + host = conf['hosts'][host_name] + if 'cmd' not in host: continue + + h = net.get(host_name) + stdout_filename = os.path.join(args.log_dir, h.name + '.stdout') + stdout_files[h.name] = open(stdout_filename, 'w') + cmd = formatCmd(host['cmd']) + print h.name, cmd + p = h.popen(cmd, stdout=stdout_files[h.name], shell=True, preexec_fn=os.setpgrp) + if 'startup_sleep' in host: sleep(host['startup_sleep']) + + if 'wait' in host and host['wait']: + _wait_for_exit(p, host_name) + else: + host_procs.append((p, host_name)) + + for p, host_name in host_procs: + if 'wait' in conf['hosts'][host_name] and conf['hosts'][host_name]['wait']: + _wait_for_exit(p, host_name) + + + for p, host_name in host_procs: + if 'wait' in conf['hosts'][host_name] and conf['hosts'][host_name]['wait']: + continue + if p.returncode is None: + run_command('pkill -INT -P %d' % p.pid) + sleep(0.2) + rc = run_command('pkill -0 -P %d' % p.pid) # check if it's still running + if rc == 0: # the process group is still running, send TERM + sleep(1) # give it a little more time to exit gracefully + run_command('pkill -TERM -P %d' % p.pid) + _wait_for_exit(p, host_name) + + if 'after' in conf and 'cmd' in conf['after']: + cmds = conf['after']['cmd'] if type(conf['after']['cmd']) == list else [conf['after']['cmd']] + for cmd in cmds: + os.system(cmd) + + if controller: controller.stop() + + net.stop() + +# if bmv2_log: +# os.system('bash -c "cp /tmp/p4s.s*.log \'%s\'"' % args.log_dir) +# if pcap_dump: +# os.system('bash -c "cp *.pcap \'%s\'"' % args.log_dir) + + bad_codes = [rc for rc in return_codes if rc != 0] + if len(bad_codes): sys.exit(1) + +if __name__ == '__main__': + setLogLevel( 'info' ) + main() diff --git a/exercises/mirroring/utils/mininet/p4_mininet.py b/exercises/mirroring/utils/mininet/p4_mininet.py new file mode 100644 index 000000000..8abe79f3a --- /dev/null +++ b/exercises/mirroring/utils/mininet/p4_mininet.py @@ -0,0 +1,161 @@ +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from mininet.net import Mininet +from mininet.node import Switch, Host +from mininet.log import setLogLevel, info, error, debug +from mininet.moduledeps import pathCheck +from sys import exit +from time import sleep +import os +import tempfile +import socket + +class P4Host(Host): + def config(self, **params): + r = super(P4Host, self).config(**params) + + for off in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload %s %s off" % (self.defaultIntf().name, off) + self.cmd(cmd) + + # disable IPv6 + self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1") + + return r + + def describe(self, sw_addr=None, sw_mac=None): + print "**********" + print "Network configuration for: %s" % self.name + print "Default interface: %s\t%s\t%s" %( + self.defaultIntf().name, + self.defaultIntf().IP(), + self.defaultIntf().MAC() + ) + if sw_addr is not None or sw_mac is not None: + print "Default route to switch: %s (%s)" % (sw_addr, sw_mac) + print "**********" + +class P4Switch(Switch): + """P4 virtual switch""" + device_id = 0 + + def __init__(self, name, sw_path = None, json_path = None, + log_file = None, + thrift_port = None, + pcap_dump = False, + log_console = False, + verbose = False, + device_id = None, + enable_debugger = False, + **kwargs): + Switch.__init__(self, name, **kwargs) + assert(sw_path) + assert(json_path) + # make sure that the provided sw_path is valid + pathCheck(sw_path) + # make sure that the provided JSON file exists + if not os.path.isfile(json_path): + error("Invalid JSON file.\n") + exit(1) + self.sw_path = sw_path + self.json_path = json_path + self.verbose = verbose + self.log_file = log_file + if self.log_file is None: + self.log_file = "/tmp/p4s.{}.log".format(self.name) + self.output = open(self.log_file, 'w') + self.thrift_port = thrift_port + self.pcap_dump = pcap_dump + self.enable_debugger = enable_debugger + self.log_console = log_console + if device_id is not None: + self.device_id = device_id + P4Switch.device_id = max(P4Switch.device_id, device_id) + else: + self.device_id = P4Switch.device_id + P4Switch.device_id += 1 + self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) + + @classmethod + def setup(cls): + pass + + def check_switch_started(self, pid): + """While the process is running (pid exists), we check if the Thrift + server has been started. If the Thrift server is ready, we assume that + the switch was started successfully. This is only reliable if the Thrift + server is started at the end of the init process""" + while True: + if not os.path.exists(os.path.join("/proc", str(pid))): + return False + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(0.5) + result = sock.connect_ex(("localhost", self.thrift_port)) + if result == 0: + return True + + def start(self, controllers): + "Start up a new P4 switch" + info("Starting P4 switch {}.\n".format(self.name)) + args = [self.sw_path] + for port, intf in self.intfs.items(): + if not intf.IP(): + args.extend(['-i', str(port) + "@" + intf.name]) + if self.pcap_dump: + args.append("--pcap") + # args.append("--useFiles") + if self.thrift_port: + args.extend(['--thrift-port', str(self.thrift_port)]) + if self.nanomsg: + args.extend(['--nanolog', self.nanomsg]) + args.extend(['--device-id', str(self.device_id)]) + P4Switch.device_id += 1 + args.append(self.json_path) + if self.enable_debugger: + args.append("--debugger") + if self.log_console: + args.append("--log-console") + info(' '.join(args) + "\n") + + pid = None + with tempfile.NamedTemporaryFile() as f: + # self.cmd(' '.join(args) + ' > /dev/null 2>&1 &') + self.cmd(' '.join(args) + ' >' + self.log_file + ' 2>&1 & echo $! >> ' + f.name) + pid = int(f.read()) + debug("P4 switch {} PID is {}.\n".format(self.name, pid)) + sleep(1) + if not self.check_switch_started(pid): + error("P4 switch {} did not start correctly." + "Check the switch log file.\n".format(self.name)) + exit(1) + info("P4 switch {} has been started.\n".format(self.name)) + + def stop(self): + "Terminate P4 switch." + self.output.flush() + self.cmd('kill %' + self.sw_path) + self.cmd('wait') + self.deleteIntfs() + + def attach(self, intf): + "Connect a data port" + assert(0) + + def detach(self, intf): + "Disconnect a data port" + assert(0) diff --git a/exercises/mirroring/utils/mininet/shortest_path.py b/exercises/mirroring/utils/mininet/shortest_path.py new file mode 100644 index 000000000..971b1b446 --- /dev/null +++ b/exercises/mirroring/utils/mininet/shortest_path.py @@ -0,0 +1,78 @@ +class ShortestPath: + + def __init__(self, edges=[]): + self.neighbors = {} + for edge in edges: + self.addEdge(*edge) + + def addEdge(self, a, b): + if a not in self.neighbors: self.neighbors[a] = [] + if b not in self.neighbors[a]: self.neighbors[a].append(b) + + if b not in self.neighbors: self.neighbors[b] = [] + if a not in self.neighbors[b]: self.neighbors[b].append(a) + + def get(self, a, b, exclude=lambda node: False): + # Shortest path from a to b + return self._recPath(a, b, [], exclude) + + def _recPath(self, a, b, visited, exclude): + if a == b: return [a] + new_visited = visited + [a] + paths = [] + for neighbor in self.neighbors[a]: + if neighbor in new_visited: continue + if exclude(neighbor) and neighbor != b: continue + path = self._recPath(neighbor, b, new_visited, exclude) + if path: paths.append(path) + + paths.sort(key=len) + return [a] + paths[0] if len(paths) else None + +if __name__ == '__main__': + + edges = [ + (1, 2), + (1, 3), + (1, 5), + (2, 4), + (3, 4), + (3, 5), + (3, 6), + (4, 6), + (5, 6), + (7, 8) + + ] + sp = ShortestPath(edges) + + assert sp.get(1, 1) == [1] + assert sp.get(2, 2) == [2] + + assert sp.get(1, 2) == [1, 2] + assert sp.get(2, 1) == [2, 1] + + assert sp.get(1, 3) == [1, 3] + assert sp.get(3, 1) == [3, 1] + + assert sp.get(4, 6) == [4, 6] + assert sp.get(6, 4) == [6, 4] + + assert sp.get(2, 6) == [2, 4, 6] + assert sp.get(6, 2) == [6, 4, 2] + + assert sp.get(1, 6) in [[1, 3, 6], [1, 5, 6]] + assert sp.get(6, 1) in [[6, 3, 1], [6, 5, 1]] + + assert sp.get(2, 5) == [2, 1, 5] + assert sp.get(5, 2) == [5, 1, 2] + + assert sp.get(4, 5) in [[4, 3, 5], [4, 6, 5]] + assert sp.get(5, 4) in [[5, 3, 4], [6, 6, 4]] + + assert sp.get(7, 8) == [7, 8] + assert sp.get(8, 7) == [8, 7] + + assert sp.get(1, 7) == None + assert sp.get(7, 2) == None + diff --git a/exercises/mirroring/utils/mininet/single_switch_mininet.py b/exercises/mirroring/utils/mininet/single_switch_mininet.py new file mode 100755 index 000000000..e2e76366c --- /dev/null +++ b/exercises/mirroring/utils/mininet/single_switch_mininet.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python2 + +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from mininet.net import Mininet +from mininet.topo import Topo +from mininet.log import setLogLevel, info +from mininet.cli import CLI + +from p4_mininet import P4Switch, P4Host + +import argparse +from subprocess import PIPE, Popen +from time import sleep + +parser = argparse.ArgumentParser(description='Mininet demo') +parser.add_argument('--behavioral-exe', help='Path to behavioral executable', + type=str, action="store", required=True) +parser.add_argument('--thrift-port', help='Thrift server port for table updates', + type=int, action="store", default=9090) +parser.add_argument('--num-hosts', help='Number of hosts to connect to switch', + type=int, action="store", default=2) +parser.add_argument('--mode', choices=['l2', 'l3'], type=str, default='l3') +parser.add_argument('--json', help='Path to JSON config file', + type=str, action="store", required=True) +parser.add_argument('--log-file', help='Path to write the switch log file', + type=str, action="store", required=False) +parser.add_argument('--pcap-dump', help='Dump packets on interfaces to pcap files', + type=str, action="store", required=False, default=False) +parser.add_argument('--switch-config', help='simple_switch_CLI script to configure switch', + type=str, action="store", required=False, default=False) +parser.add_argument('--cli-message', help='Message to print before starting CLI', + type=str, action="store", required=False, default=False) + +args = parser.parse_args() + + +class SingleSwitchTopo(Topo): + "Single switch connected to n (< 256) hosts." + def __init__(self, sw_path, json_path, log_file, + thrift_port, pcap_dump, n, **opts): + # Initialize topology and default options + Topo.__init__(self, **opts) + + switch = self.addSwitch('s1', + sw_path = sw_path, + json_path = json_path, + log_console = True, + log_file = log_file, + thrift_port = thrift_port, + enable_debugger = False, + pcap_dump = pcap_dump) + + for h in xrange(n): + host = self.addHost('h%d' % (h + 1), + ip = "10.0.%d.10/24" % h, + mac = '00:04:00:00:00:%02x' %h) + print "Adding host", str(host) + self.addLink(host, switch) + +def main(): + num_hosts = args.num_hosts + mode = args.mode + + topo = SingleSwitchTopo(args.behavioral_exe, + args.json, + args.log_file, + args.thrift_port, + args.pcap_dump, + num_hosts) + net = Mininet(topo = topo, + host = P4Host, + switch = P4Switch, + controller = None) + net.start() + + + sw_mac = ["00:aa:bb:00:00:%02x" % n for n in xrange(num_hosts)] + + sw_addr = ["10.0.%d.1" % n for n in xrange(num_hosts)] + + for n in xrange(num_hosts): + h = net.get('h%d' % (n + 1)) + if mode == "l2": + h.setDefaultRoute("dev %s" % h.defaultIntf().name) + else: + h.setARP(sw_addr[n], sw_mac[n]) + h.setDefaultRoute("dev %s via %s" % (h.defaultIntf().name, sw_addr[n])) + + for n in xrange(num_hosts): + h = net.get('h%d' % (n + 1)) + h.describe(sw_addr[n], sw_mac[n]) + + sleep(1) + + if args.switch_config is not None: + print + print "Reading switch configuration script:", args.switch_config + with open(args.switch_config, 'r') as config_file: + switch_config = config_file.read() + + print "Configuring switch..." + proc = Popen(["simple_switch_CLI"], stdin=PIPE) + proc.communicate(input=switch_config) + + print "Configuration complete." + print + + print "Ready !" + + if args.cli_message is not None: + with open(args.cli_message, 'r') as message_file: + print message_file.read() + + CLI( net ) + net.stop() + +if __name__ == '__main__': + setLogLevel( 'info' ) + main() diff --git a/exercises/mirroring/utils/netstat.py b/exercises/mirroring/utils/netstat.py new file mode 100644 index 000000000..bb12ffdf5 --- /dev/null +++ b/exercises/mirroring/utils/netstat.py @@ -0,0 +1,21 @@ +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import psutil +def check_listening_on_port(port): + for c in psutil.net_connections(kind='inet'): + if c.status == 'LISTEN' and c.laddr[1] == port: + return True + return False diff --git a/exercises/mirroring/utils/p4_mininet.py b/exercises/mirroring/utils/p4_mininet.py new file mode 100644 index 000000000..d70322a13 --- /dev/null +++ b/exercises/mirroring/utils/p4_mininet.py @@ -0,0 +1,165 @@ +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from mininet.net import Mininet +from mininet.node import Switch, Host +from mininet.log import setLogLevel, info, error, debug +from mininet.moduledeps import pathCheck +from sys import exit +import os +import tempfile +import socket +from time import sleep + +from netstat import check_listening_on_port + +SWITCH_START_TIMEOUT = 10 # seconds + +class P4Host(Host): + def config(self, **params): + r = super(Host, self).config(**params) + + self.defaultIntf().rename("eth0") + + for off in ["rx", "tx", "sg"]: + cmd = "/sbin/ethtool --offload eth0 %s off" % off + self.cmd(cmd) + + # disable IPv6 + self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1") + self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1") + + return r + + def describe(self): + print "**********" + print self.name + print "default interface: %s\t%s\t%s" %( + self.defaultIntf().name, + self.defaultIntf().IP(), + self.defaultIntf().MAC() + ) + print "**********" + +class P4Switch(Switch): + """P4 virtual switch""" + device_id = 0 + + def __init__(self, name, sw_path = None, json_path = None, + thrift_port = None, + pcap_dump = False, + log_console = False, + log_file = None, + verbose = False, + device_id = None, + enable_debugger = False, + **kwargs): + Switch.__init__(self, name, **kwargs) + assert(sw_path) + assert(json_path) + # make sure that the provided sw_path is valid + pathCheck(sw_path) + # make sure that the provided JSON file exists + if not os.path.isfile(json_path): + error("Invalid JSON file.\n") + exit(1) + self.sw_path = sw_path + self.json_path = json_path + self.verbose = verbose + logfile = "/tmp/p4s.{}.log".format(self.name) + self.output = open(logfile, 'w') + self.thrift_port = thrift_port + if check_listening_on_port(self.thrift_port): + error('%s cannot bind port %d because it is bound by another process\n' % (self.name, self.grpc_port)) + exit(1) + self.pcap_dump = pcap_dump + self.enable_debugger = enable_debugger + self.log_console = log_console + if log_file is not None: + self.log_file = log_file + else: + self.log_file = "/tmp/p4s.{}.log".format(self.name) + if device_id is not None: + self.device_id = device_id + P4Switch.device_id = max(P4Switch.device_id, device_id) + else: + self.device_id = P4Switch.device_id + P4Switch.device_id += 1 + self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) + + @classmethod + def setup(cls): + pass + + def check_switch_started(self, pid): + """While the process is running (pid exists), we check if the Thrift + server has been started. If the Thrift server is ready, we assume that + the switch was started successfully. This is only reliable if the Thrift + server is started at the end of the init process""" + while True: + if not os.path.exists(os.path.join("/proc", str(pid))): + return False + if check_listening_on_port(self.thrift_port): + return True + sleep(0.5) + + def start(self, controllers): + "Start up a new P4 switch" + info("Starting P4 switch {}.\n".format(self.name)) + args = [self.sw_path] + for port, intf in self.intfs.items(): + if not intf.IP(): + args.extend(['-i', str(port) + "@" + intf.name]) + if self.pcap_dump: + args.append("--pcap %s" % self.pcap_dump) + if self.thrift_port: + args.extend(['--thrift-port', str(self.thrift_port)]) + if self.nanomsg: + args.extend(['--nanolog', self.nanomsg]) + args.extend(['--device-id', str(self.device_id)]) + P4Switch.device_id += 1 + args.append(self.json_path) + if self.enable_debugger: + args.append("--debugger") + if self.log_console: + args.append("--log-console") + info(' '.join(args) + "\n") + + pid = None + with tempfile.NamedTemporaryFile() as f: + # self.cmd(' '.join(args) + ' > /dev/null 2>&1 &') + self.cmd(' '.join(args) + ' >' + self.log_file + ' 2>&1 & echo $! >> ' + f.name) + pid = int(f.read()) + debug("P4 switch {} PID is {}.\n".format(self.name, pid)) + if not self.check_switch_started(pid): + error("P4 switch {} did not start correctly.\n".format(self.name)) + exit(1) + info("P4 switch {} has been started.\n".format(self.name)) + + def stop(self): + "Terminate P4 switch." + self.output.flush() + self.cmd('kill %' + self.sw_path) + self.cmd('wait') + self.deleteIntfs() + + def attach(self, intf): + "Connect a data port" + assert(0) + + def detach(self, intf): + "Disconnect a data port" + assert(0) diff --git a/exercises/mirroring/utils/p4apprunner.py b/exercises/mirroring/utils/p4apprunner.py new file mode 100755 index 000000000..36b9eeab8 --- /dev/null +++ b/exercises/mirroring/utils/p4apprunner.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python2 +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import argparse +from collections import OrderedDict +import json +import os +import sys +import tarfile + +parser = argparse.ArgumentParser(description='p4apprunner') +parser.add_argument('--build-dir', help='Directory to build in.', + type=str, action='store', required=False, default='/tmp') +parser.add_argument('--quiet', help='Suppress log messages.', + action='store_true', required=False, default=False) +parser.add_argument('--manifest', help='Path to manifest file.', + type=str, action='store', required=False, default='./p4app.json') +parser.add_argument('app', help='.p4app package to run.', type=str) +parser.add_argument('target', help=('Target to run. Defaults to the first target ' + 'in the package.'), + nargs='?', type=str) + +args = parser.parse_args() + +def log(*items): + if args.quiet != True: + print(*items) + +def log_error(*items): + print(*items, file=sys.stderr) + +def run_command(command): + log('>', command) + return os.WEXITSTATUS(os.system(command)) + +class Manifest: + def __init__(self, program_file, language, target, target_config): + self.program_file = program_file + self.language = language + self.target = target + self.target_config = target_config + +def read_manifest(manifest_file): + manifest = json.load(manifest_file, object_pairs_hook=OrderedDict) + + if 'program' not in manifest: + log_error('No program defined in manifest.') + sys.exit(1) + program_file = manifest['program'] + + if 'language' not in manifest: + log_error('No language defined in manifest.') + sys.exit(1) + language = manifest['language'] + + if 'targets' not in manifest or len(manifest['targets']) < 1: + log_error('No targets defined in manifest.') + sys.exit(1) + + if args.target is not None: + chosen_target = args.target + elif 'default-target' in manifest: + chosen_target = manifest['default-target'] + else: + chosen_target = manifest['targets'].keys()[0] + + if chosen_target not in manifest['targets']: + log_error('Target not found in manifest:', chosen_target) + sys.exit(1) + + return Manifest(program_file, language, chosen_target, manifest['targets'][chosen_target]) + + +def run_compile_bmv2(manifest): + if 'run-before-compile' in manifest.target_config: + commands = manifest.target_config['run-before-compile'] + if not isinstance(commands, list): + log_error('run-before-compile should be a list:', commands) + sys.exit(1) + for command in commands: + run_command(command) + + compiler_args = [] + + if manifest.language == 'p4-14': + compiler_args.append('--p4v 14') + elif manifest.language == 'p4-16': + compiler_args.append('--p4v 16') + else: + log_error('Unknown language:', manifest.language) + sys.exit(1) + + if 'compiler-flags' in manifest.target_config: + flags = manifest.target_config['compiler-flags'] + if not isinstance(flags, list): + log_error('compiler-flags should be a list:', flags) + sys.exit(1) + compiler_args.extend(flags) + + # Compile the program. + output_file = manifest.program_file + '.json' + compiler_args.append('"%s"' % manifest.program_file) + compiler_args.append('-o "%s"' % output_file) + rv = run_command('p4c-bm2-ss %s' % ' '.join(compiler_args)) + + if 'run-after-compile' in manifest.target_config: + commands = manifest.target_config['run-after-compile'] + if not isinstance(commands, list): + log_error('run-after-compile should be a list:', commands) + sys.exit(1) + for command in commands: + run_command(command) + + if rv != 0: + log_error('Compile failed.') + sys.exit(1) + + return output_file + +def run_mininet(manifest): + output_file = run_compile_bmv2(manifest) + + # Run the program using the BMV2 Mininet simple switch. + switch_args = [] + + # We'll place the switch's log file in current (build) folder. + cwd = os.getcwd() + log_file = os.path.join(cwd, manifest.program_file + '.log') + print ("*** Log file %s" % log_file) + switch_args.append('--log-file "%s"' % log_file) + + pcap_dir = os.path.join(cwd) + print ("*** Pcap folder %s" % pcap_dir) + switch_args.append('--pcap-dump "%s" '% pcap_dir) + + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + message_file = 'mininet_message.txt' + with open(message_file, 'w') as message: + + print(file=message) + print('======================================================================', + file=message) + print('Welcome to the BMV2 Mininet CLI!', file=message) + print('======================================================================', + file=message) + print('Your P4 program is installed into the BMV2 software switch', file=message) + print('and your initial configuration is loaded. You can interact', file=message) + print('with the network using the mininet CLI below.', file=message) + print(file=message) + print('To inspect or change the switch configuration, connect to', file=message) + print('its CLI from your host operating system using this command:', file=message) + print(' simple_switch_CLI', file=message) + print(file=message) + print('To view the switch log, run this command from your host OS:', file=message) + print(' tail -f %s' % log_file, file=message) + print(file=message) + print('To view the switch output pcap, check the pcap files in %s:' % pcap_dir, file=message) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap', file=message) + print(file=message) +# print('To run the switch debugger, run this command from your host OS:', file=message) +# print(' bm_p4dbg' , file=message) +# print(file=message) + + switch_args.append('--cli-message "%s"' % message_file) + + if 'num-hosts' in manifest.target_config: + switch_args.append('--num-hosts %s' % manifest.target_config['num-hosts']) + + if 'switch-config' in manifest.target_config: + switch_args.append('--switch-config "%s"' % manifest.target_config['switch-config']) + + switch_args.append('--behavioral-exe "%s"' % 'simple_switch') + switch_args.append('--json "%s"' % output_file) + + program = '"%s/mininet/single_switch_mininet.py"' % sys.path[0] + return run_command('python2 %s %s' % (program, ' '.join(switch_args))) + +def run_multiswitch(manifest): + output_file = run_compile_bmv2(manifest) + + script_args = [] + cwd = os.getcwd() + log_dir = os.path.join(cwd, cwd + '/logs') + print ("*** Log directory %s" % log_dir) + script_args.append('--log-dir "%s"' % log_dir) + pcap_dir = os.path.join(cwd) + print ("*** Pcap directory %s" % cwd) + script_args.append('--manifest "%s"' % args.manifest) + script_args.append('--target "%s"' % manifest.target) + if 'auto-control-plane' in manifest.target_config and manifest.target_config['auto-control-plane']: + script_args.append('--auto-control-plane' ) + script_args.append('--behavioral-exe "%s"' % 'simple_switch') + script_args.append('--json "%s"' % output_file) + #script_args.append('--cli') + + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + message_file = 'mininet_message.txt' + with open(message_file, 'w') as message: + + print(file=message) + print('======================================================================', + file=message) + print('Welcome to the BMV2 Mininet CLI!', file=message) + print('======================================================================', + file=message) + print('Your P4 program is installed into the BMV2 software switch', file=message) + print('and your initial configuration is loaded. You can interact', file=message) + print('with the network using the mininet CLI below.', file=message) + print(file=message) + print('To inspect or change the switch configuration, connect to', file=message) + print('its CLI from your host operating system using this command:', file=message) + print(' simple_switch_CLI --thrift-port ', file=message) + print(file=message) + print('To view a switch log, run this command from your host OS:', file=message) + print(' tail -f %s/.log' % log_dir, file=message) + print(file=message) + print('To view the switch output pcap, check the pcap files in %s:' % pcap_dir, file=message) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap', file=message) + print(file=message) +# print('To run the switch debugger, run this command from your host OS:', file=message) +# print(' bm_p4dbg' , file=message) +# print(file=message) + + script_args.append('--cli-message "%s"' % message_file) + + program = '"%s/mininet/multi_switch_mininet.py"' % sys.path[0] + return run_command('python2 %s %s' % (program, ' '.join(script_args))) + +def run_stf(manifest): + output_file = run_compile_bmv2(manifest) + + if not 'test' in manifest.target_config: + log_error('No STF test file provided.') + sys.exit(1) + stf_file = manifest.target_config['test'] + + # Run the program using the BMV2 STF interpreter. + stf_args = [] + stf_args.append('-v') + stf_args.append(os.path.join(args.build_dir, output_file)) + stf_args.append(os.path.join(args.build_dir, stf_file)) + + program = '"%s/stf/bmv2stf.py"' % sys.path[0] + rv = run_command('python2 %s %s' % (program, ' '.join(stf_args))) + if rv != 0: + sys.exit(1) + return rv + +def run_custom(manifest): + output_file = run_compile_bmv2(manifest) + python_path = 'PYTHONPATH=$PYTHONPATH:/scripts/mininet/' + script_args = [] + script_args.append('--behavioral-exe "%s"' % 'simple_switch') + script_args.append('--json "%s"' % output_file) + script_args.append('--cli "%s"' % 'simple_switch_CLI') + if not 'program' in manifest.target_config: + log_error('No mininet program file provided.') + sys.exit(1) + program = manifest.target_config['program'] + rv = run_command('%s python2 %s %s' % (python_path, program, ' '.join(script_args))) + + if rv != 0: + sys.exit(1) + return rv + +def main(): + log('Entering build directory.') + os.chdir(args.build_dir) + + # A '.p4app' package is really just a '.tar.gz' archive. Extract it so we + # can process its contents. + log('Extracting package.') + tar = tarfile.open(args.app) + tar.extractall() + tar.close() + + log('Reading package manifest.') + with open(args.manifest, 'r') as manifest_file: + manifest = read_manifest(manifest_file) + + # Dispatch to the backend implementation for this target. + backend = manifest.target + if 'use' in manifest.target_config: + backend = manifest.target_config['use'] + + if backend == 'mininet': + rc = run_mininet(manifest) + elif backend == 'multiswitch': + rc = run_multiswitch(manifest) + elif backend == 'stf': + rc = run_stf(manifest) + elif backend == 'custom': + rc = run_custom(manifest) + elif backend == 'compile-bmv2': + run_compile_bmv2(manifest) + rc = 0 + else: + log_error('Target specifies unknown backend:', backend) + sys.exit(1) + + sys.exit(rc) + +if __name__ == '__main__': + main() diff --git a/exercises/mirroring/utils/p4runtime_lib/__init__.py b/exercises/mirroring/utils/p4runtime_lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/mirroring/utils/p4runtime_lib/bmv2.py b/exercises/mirroring/utils/p4runtime_lib/bmv2.py new file mode 100644 index 000000000..7f483f476 --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_lib/bmv2.py @@ -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) diff --git a/exercises/mirroring/utils/p4runtime_lib/convert.py b/exercises/mirroring/utils/p4runtime_lib/convert.py new file mode 100644 index 000000000..0375e1747 --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_lib/convert.py @@ -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 diff --git a/exercises/mirroring/utils/p4runtime_lib/error_utils.py b/exercises/mirroring/utils/p4runtime_lib/error_utils.py new file mode 100644 index 000000000..487c98e6f --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_lib/error_utils.py @@ -0,0 +1,92 @@ +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import sys + +from google.rpc import status_pb2, code_pb2 +import grpc +from p4.v1 import p4runtime_pb2 +from p4.v1 import p4runtime_pb2_grpc + +# Used to indicate that the gRPC error Status object returned by the server has +# an incorrect format. +class P4RuntimeErrorFormatException(Exception): + def __init__(self, message): + super(P4RuntimeErrorFormatException, self).__init__(message) + + +# Parse the binary details of the gRPC error. This is required to print some +# helpful debugging information in tha case of batched Write / Read +# requests. Returns None if there are no useful binary details and throws +# P4RuntimeErrorFormatException if the error is not formatted +# properly. Otherwise, returns a list of tuples with the first element being the +# index of the operation in the batch that failed and the second element being +# the p4.Error Protobuf message. +def parseGrpcErrorBinaryDetails(grpc_error): + if grpc_error.code() != grpc.StatusCode.UNKNOWN: + return None + + error = None + # The gRPC Python package does not have a convenient way to access the + # binary details for the error: they are treated as trailing metadata. + for meta in grpc_error.trailing_metadata(): + if meta[0] == "grpc-status-details-bin": + error = status_pb2.Status() + error.ParseFromString(meta[1]) + break + if error is None: # no binary details field + return None + if len(error.details) == 0: + # binary details field has empty Any details repeated field + return None + + indexed_p4_errors = [] + for idx, one_error_any in enumerate(error.details): + p4_error = p4runtime_pb2.Error() + if not one_error_any.Unpack(p4_error): + raise P4RuntimeErrorFormatException( + "Cannot convert Any message to p4.Error") + if p4_error.canonical_code == code_pb2.OK: + continue + indexed_p4_errors += [(idx, p4_error)] + + return indexed_p4_errors + + +# P4Runtime uses a 3-level message in case of an error during the processing of +# a write batch. This means that some care is required when printing the +# exception if we do not want to end-up with a non-helpful message in case of +# failure as only the first level will be printed. In this function, we extract +# the nested error message when present (one for each operation included in the +# batch) in order to print error code + user-facing message. See P4Runtime +# documentation for more details on error-reporting. +def printGrpcError(grpc_error): + print "gRPC Error", grpc_error.details(), + status_code = grpc_error.code() + print "({})".format(status_code.name), + traceback = sys.exc_info()[2] + print "[{}:{}]".format( + traceback.tb_frame.f_code.co_filename, traceback.tb_lineno) + if status_code != grpc.StatusCode.UNKNOWN: + return + p4_errors = parseGrpcErrorBinaryDetails(grpc_error) + if p4_errors is None: + return + print "Errors in batch:" + for idx, p4_error in p4_errors: + code_name = code_pb2._CODE.values_by_number[ + p4_error.canonical_code].name + print "\t* At index {}: {}, '{}'\n".format( + idx, code_name, p4_error.message) diff --git a/exercises/mirroring/utils/p4runtime_lib/helper.py b/exercises/mirroring/utils/p4runtime_lib/helper.py new file mode 100644 index 000000000..e2dc2f733 --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_lib/helper.py @@ -0,0 +1,190 @@ +# 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.v1 import p4runtime_pb2 +from p4.config.v1 import p4info_pb2 + +from 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.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, (has: %r)" % (action_name, name if name is not None else id, a.params)) + + 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=None, + default_action=False, + action_name=None, + action_params=None, + priority=None): + table_entry = p4runtime_pb2.TableEntry() + table_entry.table_id = self.get_tables_id(table_name) + + if priority is not None: + table_entry.priority = priority + + 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 default_action: + table_entry.is_default_action = True + + 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 diff --git a/exercises/mirroring/utils/p4runtime_lib/simple_controller.py b/exercises/mirroring/utils/p4runtime_lib/simple_controller.py new file mode 100755 index 000000000..4f130ba72 --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_lib/simple_controller.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python2 +# +# 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 argparse +import json +import os +import sys + +import bmv2 +import helper + + +def error(msg): + print >> sys.stderr, ' - ERROR! ' + msg + +def info(msg): + print >> sys.stdout, ' - ' + msg + + +class ConfException(Exception): + pass + + +def main(): + parser = argparse.ArgumentParser(description='P4Runtime Simple Controller') + + parser.add_argument('-a', '--p4runtime-server-addr', + help='address and port of the switch\'s P4Runtime server (e.g. 192.168.0.1:50051)', + type=str, action="store", required=True) + parser.add_argument('-d', '--device-id', + help='Internal device ID to use in P4Runtime messages', + type=int, action="store", required=True) + parser.add_argument('-p', '--proto-dump-file', + help='path to file where to dump protobuf messages sent to the switch', + type=str, action="store", required=True) + parser.add_argument("-c", '--runtime-conf-file', + help="path to input runtime configuration file (JSON)", + type=str, action="store", required=True) + + args = parser.parse_args() + + if not os.path.exists(args.runtime_conf_file): + parser.error("File %s does not exist!" % args.runtime_conf_file) + workdir = os.path.dirname(os.path.abspath(args.runtime_conf_file)) + with open(args.runtime_conf_file, 'r') as sw_conf_file: + program_switch(addr=args.p4runtime_server_addr, + device_id=args.device_id, + sw_conf_file=sw_conf_file, + workdir=workdir, + proto_dump_fpath=args.proto_dump_file) + + +def check_switch_conf(sw_conf, workdir): + required_keys = ["p4info"] + files_to_check = ["p4info"] + target_choices = ["bmv2"] + + if "target" not in sw_conf: + raise ConfException("missing key 'target'") + target = sw_conf['target'] + if target not in target_choices: + raise ConfException("unknown target '%s'" % target) + + if target == 'bmv2': + required_keys.append("bmv2_json") + files_to_check.append("bmv2_json") + + for conf_key in required_keys: + if conf_key not in sw_conf or len(sw_conf[conf_key]) == 0: + raise ConfException("missing key '%s' or empty value" % conf_key) + + for conf_key in files_to_check: + real_path = os.path.join(workdir, sw_conf[conf_key]) + if not os.path.exists(real_path): + raise ConfException("file does not exist %s" % real_path) + + +def program_switch(addr, device_id, sw_conf_file, workdir, proto_dump_fpath): + sw_conf = json_load_byteified(sw_conf_file) + try: + check_switch_conf(sw_conf=sw_conf, workdir=workdir) + except ConfException as e: + error("While parsing input runtime configuration: %s" % str(e)) + return + + info('Using P4Info file %s...' % sw_conf['p4info']) + p4info_fpath = os.path.join(workdir, sw_conf['p4info']) + p4info_helper = helper.P4InfoHelper(p4info_fpath) + + target = sw_conf['target'] + + info("Connecting to P4Runtime server on %s (%s)..." % (addr, target)) + + if target == "bmv2": + sw = bmv2.Bmv2SwitchConnection(address=addr, device_id=device_id, + proto_dump_file=proto_dump_fpath) + else: + raise Exception("Don't know how to connect to target %s" % target) + + try: + sw.MasterArbitrationUpdate() + + if target == "bmv2": + info("Setting pipeline config (%s)..." % sw_conf['bmv2_json']) + bmv2_json_fpath = os.path.join(workdir, sw_conf['bmv2_json']) + sw.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, + bmv2_json_file_path=bmv2_json_fpath) + else: + raise Exception("Should not be here") + + if 'table_entries' in sw_conf: + table_entries = sw_conf['table_entries'] + info("Inserting %d table entries..." % len(table_entries)) + for entry in table_entries: + info(tableEntryToString(entry)) + insertTableEntry(sw, entry, p4info_helper) + finally: + sw.shutdown() + + +def insertTableEntry(sw, flow, p4info_helper): + table_name = flow['table'] + match_fields = flow.get('match') # None if not found + action_name = flow['action_name'] + default_action = flow.get('default_action') # None if not found + action_params = flow['action_params'] + priority = flow.get('priority') # None if not found + + table_entry = p4info_helper.buildTableEntry( + table_name=table_name, + match_fields=match_fields, + default_action=default_action, + action_name=action_name, + action_params=action_params, + priority=priority) + + sw.WriteTableEntry(table_entry) + + +# object hook for josn library, use str instead of unicode object +# https://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-of-unicode-from-json +def json_load_byteified(file_handle): + return _byteify(json.load(file_handle, object_hook=_byteify), + ignore_dicts=True) + + +def _byteify(data, ignore_dicts=False): + # if this is a unicode string, return its string representation + if isinstance(data, unicode): + return data.encode('utf-8') + # if this is a list of values, return list of byteified values + if isinstance(data, list): + return [_byteify(item, ignore_dicts=True) for item in data] + # if this is a dictionary, return dictionary of byteified keys and values + # but only if we haven't already byteified it + if isinstance(data, dict) and not ignore_dicts: + return { + _byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True) + for key, value in data.iteritems() + } + # if it's anything else, return it in its original form + return data + + +def tableEntryToString(flow): + if 'match' in flow: + match_str = ['%s=%s' % (match_name, str(flow['match'][match_name])) for match_name in + flow['match']] + match_str = ', '.join(match_str) + elif 'default_action' in flow and flow['default_action']: + match_str = '(default action)' + else: + match_str = '(any)' + params = ['%s=%s' % (param_name, str(flow['action_params'][param_name])) for param_name in + flow['action_params']] + params = ', '.join(params) + return "%s: %s => %s(%s)" % ( + flow['table'], match_str, flow['action_name'], params) + + +if __name__ == '__main__': + main() diff --git a/exercises/mirroring/utils/p4runtime_lib/switch.py b/exercises/mirroring/utils/p4runtime_lib/switch.py new file mode 100644 index 000000000..3f662f2dd --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_lib/switch.py @@ -0,0 +1,172 @@ +# 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 Queue import Queue +from abc import abstractmethod +from datetime import datetime + +import grpc +from p4.v1 import p4runtime_pb2 +from p4.v1 import p4runtime_pb2_grpc +from p4.tmp import p4config_pb2 + +MSG_LOG_MAX_LEN = 1024 + +# List of all active connections +connections = [] + +def ShutdownAllSwitchConnections(): + for c in connections: + c.shutdown() + +class SwitchConnection(object): + + def __init__(self, name=None, address='127.0.0.1:50051', device_id=0, + proto_dump_file=None): + self.name = name + self.address = address + self.device_id = device_id + self.p4info = None + self.channel = grpc.insecure_channel(self.address) + if proto_dump_file is not None: + interceptor = GrpcRequestLogger(proto_dump_file) + self.channel = grpc.intercept_channel(self.channel, interceptor) + self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel) + self.requests_stream = IterableQueue() + self.stream_msg_resp = self.client_stub.StreamChannel(iter(self.requests_stream)) + self.proto_dump_file = proto_dump_file + connections.append(self) + + @abstractmethod + def buildDeviceConfig(self, **kwargs): + return p4config_pb2.P4DeviceConfig() + + def shutdown(self): + self.requests_stream.close() + self.stream_msg_resp.cancel() + + def MasterArbitrationUpdate(self, dry_run=False, **kwargs): + request = p4runtime_pb2.StreamMessageRequest() + request.arbitration.device_id = self.device_id + request.arbitration.election_id.high = 0 + request.arbitration.election_id.low = 1 + + if dry_run: + print "P4Runtime MasterArbitrationUpdate: ", request + else: + self.requests_stream.put(request) + for item in self.stream_msg_resp: + return item # just one + + def SetForwardingPipelineConfig(self, p4info, dry_run=False, **kwargs): + device_config = self.buildDeviceConfig(**kwargs) + request = p4runtime_pb2.SetForwardingPipelineConfigRequest() + request.election_id.low = 1 + 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 "P4Runtime 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 + request.election_id.low = 1 + update = request.updates.add() + if table_entry.is_default_action: + update.type = p4runtime_pb2.Update.MODIFY + else: + update.type = p4runtime_pb2.Update.INSERT + update.entity.table_entry.CopyFrom(table_entry) + if dry_run: + print "P4Runtime 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 "P4Runtime 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 = index + if dry_run: + print "P4Runtime Read:", request + else: + for response in self.client_stub.Read(request): + yield response + + +class GrpcRequestLogger(grpc.UnaryUnaryClientInterceptor, + grpc.UnaryStreamClientInterceptor): + """Implementation of a gRPC interceptor that logs request to a file""" + + def __init__(self, log_file): + self.log_file = log_file + with open(self.log_file, 'w') as f: + # Clear content if it exists. + f.write("") + + def log_message(self, method_name, body): + with open(self.log_file, 'a') as f: + ts = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + msg = str(body) + f.write("\n[%s] %s\n---\n" % (ts, method_name)) + if len(msg) < MSG_LOG_MAX_LEN: + f.write(str(body)) + else: + f.write("Message too long (%d bytes)! Skipping log...\n" % len(msg)) + f.write('---\n') + + def intercept_unary_unary(self, continuation, client_call_details, request): + self.log_message(client_call_details.method, request) + return continuation(client_call_details, request) + + def intercept_unary_stream(self, continuation, client_call_details, request): + self.log_message(client_call_details.method, request) + return continuation(client_call_details, request) + +class IterableQueue(Queue): + _sentinel = object() + + def __iter__(self): + return iter(self.get, self._sentinel) + + def close(self): + self.put(self._sentinel) diff --git a/exercises/mirroring/utils/p4runtime_switch.py b/exercises/mirroring/utils/p4runtime_switch.py new file mode 100644 index 000000000..1f89373f2 --- /dev/null +++ b/exercises/mirroring/utils/p4runtime_switch.py @@ -0,0 +1,137 @@ +# Copyright 2017-present Barefoot Networks, Inc. +# Copyright 2017-present Open Networking Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import sys, os, tempfile, socket +from time import sleep + +from mininet.node import Switch +from mininet.moduledeps import pathCheck +from mininet.log import info, error, debug + +from p4_mininet import P4Switch, SWITCH_START_TIMEOUT +from netstat import check_listening_on_port + +class P4RuntimeSwitch(P4Switch): + "BMv2 switch with gRPC support" + next_grpc_port = 50051 + next_thrift_port = 9090 + + def __init__(self, name, sw_path = None, json_path = None, + grpc_port = None, + thrift_port = None, + pcap_dump = False, + log_console = False, + verbose = False, + device_id = None, + enable_debugger = False, + log_file = None, + **kwargs): + Switch.__init__(self, name, **kwargs) + assert (sw_path) + self.sw_path = sw_path + # make sure that the provided sw_path is valid + pathCheck(sw_path) + + if json_path is not None: + # make sure that the provided JSON file exists + if not os.path.isfile(json_path): + error("Invalid JSON file: {}\n".format(json_path)) + exit(1) + self.json_path = json_path + else: + self.json_path = None + + if grpc_port is not None: + self.grpc_port = grpc_port + else: + self.grpc_port = P4RuntimeSwitch.next_grpc_port + P4RuntimeSwitch.next_grpc_port += 1 + + if thrift_port is not None: + self.thrift_port = thrift_port + else: + self.thrift_port = P4RuntimeSwitch.next_thrift_port + P4RuntimeSwitch.next_thrift_port += 1 + + if check_listening_on_port(self.grpc_port): + error('%s cannot bind port %d because it is bound by another process\n' % (self.name, self.grpc_port)) + exit(1) + + self.verbose = verbose + logfile = "/tmp/p4s.{}.log".format(self.name) + self.output = open(logfile, 'w') + self.pcap_dump = pcap_dump + self.enable_debugger = enable_debugger + self.log_console = log_console + if log_file is not None: + self.log_file = log_file + else: + self.log_file = "/tmp/p4s.{}.log".format(self.name) + if device_id is not None: + self.device_id = device_id + P4Switch.device_id = max(P4Switch.device_id, device_id) + else: + self.device_id = P4Switch.device_id + P4Switch.device_id += 1 + self.nanomsg = "ipc:///tmp/bm-{}-log.ipc".format(self.device_id) + + + def check_switch_started(self, pid): + for _ in range(SWITCH_START_TIMEOUT * 2): + if not os.path.exists(os.path.join("/proc", str(pid))): + return False + if check_listening_on_port(self.grpc_port): + return True + sleep(0.5) + + def start(self, controllers): + info("Starting P4 switch {}.\n".format(self.name)) + args = [self.sw_path] + for port, intf in self.intfs.items(): + if not intf.IP(): + args.extend(['-i', str(port) + "@" + intf.name]) + if self.pcap_dump: + args.append("--pcap %s" % self.pcap_dump) + if self.nanomsg: + args.extend(['--nanolog', self.nanomsg]) + args.extend(['--device-id', str(self.device_id)]) + P4Switch.device_id += 1 + if self.json_path: + args.append(self.json_path) + else: + args.append("--no-p4") + if self.enable_debugger: + args.append("--debugger") + if self.log_console: + args.append("--log-console") + if self.thrift_port: + args.append('--thrift-port ' + str(self.thrift_port)) + if self.grpc_port: + args.append("-- --grpc-server-addr 0.0.0.0:" + str(self.grpc_port)) + cmd = ' '.join(args) + info(cmd + "\n") + + + pid = None + with tempfile.NamedTemporaryFile() as f: + self.cmd(cmd + ' >' + self.log_file + ' 2>&1 & echo $! >> ' + f.name) + pid = int(f.read()) + debug("P4 switch {} PID is {}.\n".format(self.name, pid)) + if not self.check_switch_started(pid): + error("P4 switch {} did not start correctly.\n".format(self.name)) + exit(1) + info("P4 switch {} has been started.\n".format(self.name)) + diff --git a/exercises/mirroring/utils/run_exercise.py b/exercises/mirroring/utils/run_exercise.py new file mode 100755 index 000000000..f44068978 --- /dev/null +++ b/exercises/mirroring/utils/run_exercise.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python2 +# Copyright 2013-present Barefoot Networks, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Adapted by Robert MacDavid (macdavid@cs.princeton.edu) from scripts found in +# the p4app repository (https://github.com/p4lang/p4app) +# +# We encourage you to dissect this script to better understand the BMv2/Mininet +# environment used by the P4 tutorial. +# +import os, sys, json, subprocess, re, argparse +from time import sleep + +from p4_mininet import P4Switch, P4Host + +from mininet.net import Mininet +from mininet.topo import Topo +from mininet.link import TCLink +from mininet.cli import CLI + +from p4runtime_switch import P4RuntimeSwitch +import p4runtime_lib.simple_controller + +def configureP4Switch(**switch_args): + """ Helper class that is called by mininet to initialize + the virtual P4 switches. The purpose is to ensure each + switch's thrift server is using a unique port. + """ + if "sw_path" in switch_args and 'grpc' in switch_args['sw_path']: + # If grpc appears in the BMv2 switch target, we assume will start P4Runtime + class ConfiguredP4RuntimeSwitch(P4RuntimeSwitch): + def __init__(self, *opts, **kwargs): + kwargs.update(switch_args) + P4RuntimeSwitch.__init__(self, *opts, **kwargs) + + def describe(self): + print "%s -> gRPC port: %d" % (self.name, self.grpc_port) + + return ConfiguredP4RuntimeSwitch + else: + class ConfiguredP4Switch(P4Switch): + next_thrift_port = 9090 + def __init__(self, *opts, **kwargs): + global next_thrift_port + kwargs.update(switch_args) + kwargs['thrift_port'] = ConfiguredP4Switch.next_thrift_port + ConfiguredP4Switch.next_thrift_port += 1 + P4Switch.__init__(self, *opts, **kwargs) + + def describe(self): + print "%s -> Thrift port: %d" % (self.name, self.thrift_port) + + return ConfiguredP4Switch + + +class ExerciseTopo(Topo): + """ The mininet topology class for the P4 tutorial exercises. + """ + def __init__(self, hosts, switches, links, log_dir, bmv2_exe, pcap_dir, **opts): + Topo.__init__(self, **opts) + host_links = [] + switch_links = [] + + # assumes host always comes first for host<-->switch links + for link in links: + if link['node1'][0] == 'h': + host_links.append(link) + else: + switch_links.append(link) + + for sw, params in switches.iteritems(): + if "program" in params: + switchClass = configureP4Switch( + sw_path=bmv2_exe, + json_path=params["program"], + log_console=True, + pcap_dump=pcap_dir) + else: + # add default switch + switchClass = None + self.addSwitch(sw, log_file="%s/%s.log" %(log_dir, sw), cls=switchClass) + + for link in host_links: + host_name = link['node1'] + sw_name, sw_port = self.parse_switch_node(link['node2']) + host_ip = hosts[host_name]['ip'] + host_mac = hosts[host_name]['mac'] + self.addHost(host_name, ip=host_ip, mac=host_mac) + self.addLink(host_name, sw_name, + delay=link['latency'], bw=link['bandwidth'], + port2=sw_port) + + for link in switch_links: + sw1_name, sw1_port = self.parse_switch_node(link['node1']) + sw2_name, sw2_port = self.parse_switch_node(link['node2']) + self.addLink(sw1_name, sw2_name, + port1=sw1_port, port2=sw2_port, + delay=link['latency'], bw=link['bandwidth']) + + + def parse_switch_node(self, node): + assert(len(node.split('-')) == 2) + sw_name, sw_port = node.split('-') + try: + sw_port = int(sw_port[1]) + except: + raise Exception('Invalid switch node in topology file: {}'.format(node)) + return sw_name, sw_port + + +class ExerciseRunner: + """ + Attributes: + log_dir : string // directory for mininet log files + pcap_dir : string // directory for mininet switch pcap files + quiet : bool // determines if we print logger messages + + hosts : dict // mininet host names and their associated properties + switches : dict // mininet switch names and their associated properties + links : list // list of mininet link properties + + switch_json : string // json of the compiled p4 example + bmv2_exe : string // name or path of the p4 switch binary + + topo : Topo object // The mininet topology instance + net : Mininet object // The mininet instance + + """ + def logger(self, *items): + if not self.quiet: + print(' '.join(items)) + + def format_latency(self, l): + """ Helper method for parsing link latencies from the topology json. """ + if isinstance(l, (str, unicode)): + return l + else: + return str(l) + "ms" + + + def __init__(self, topo_file, log_dir, pcap_dir, + switch_json, bmv2_exe='simple_switch', quiet=False): + """ Initializes some attributes and reads the topology json. Does not + actually run the exercise. Use run_exercise() for that. + + Arguments: + topo_file : string // A json file which describes the exercise's + mininet topology. + log_dir : string // Path to a directory for storing exercise logs + pcap_dir : string // Ditto, but for mininet switch pcap files + switch_json : string // Path to a compiled p4 json for bmv2 + bmv2_exe : string // Path to the p4 behavioral binary + quiet : bool // Enable/disable script debug messages + """ + + self.quiet = quiet + self.logger('Reading topology file.') + with open(topo_file, 'r') as f: + topo = json.load(f) + self.hosts = topo['hosts'] + self.switches = topo['switches'] + self.links = self.parse_links(topo['links']) + + # Ensure all the needed directories exist and are directories + for dir_name in [log_dir, pcap_dir]: + if not os.path.isdir(dir_name): + if os.path.exists(dir_name): + raise Exception("'%s' exists and is not a directory!" % dir_name) + os.mkdir(dir_name) + self.log_dir = log_dir + self.pcap_dir = pcap_dir + self.switch_json = switch_json + self.bmv2_exe = bmv2_exe + + + def run_exercise(self): + """ Sets up the mininet instance, programs the switches, + and starts the mininet CLI. This is the main method to run after + initializing the object. + """ + # Initialize mininet with the topology specified by the config + self.create_network() + self.net.start() + sleep(1) + + # some programming that must happen after the net has started + self.program_hosts() + self.program_switches() + + # wait for that to finish. Not sure how to do this better + sleep(1) + + self.do_net_cli() + # stop right after the CLI is exited + self.net.stop() + + + def parse_links(self, unparsed_links): + """ Given a list of links descriptions of the form [node1, node2, latency, bandwidth] + with the latency and bandwidth being optional, parses these descriptions + into dictionaries and store them as self.links + """ + links = [] + for link in unparsed_links: + # make sure each link's endpoints are ordered alphabetically + s, t, = link[0], link[1] + if s > t: + s,t = t,s + + link_dict = {'node1':s, + 'node2':t, + 'latency':'0ms', + 'bandwidth':None + } + if len(link) > 2: + link_dict['latency'] = self.format_latency(link[2]) + if len(link) > 3: + link_dict['bandwidth'] = link[3] + + if link_dict['node1'][0] == 'h': + assert link_dict['node2'][0] == 's', 'Hosts should be connected to switches, not ' + str(link_dict['node2']) + links.append(link_dict) + return links + + + def create_network(self): + """ Create the mininet network object, and store it as self.net. + + Side effects: + - Mininet topology instance stored as self.topo + - Mininet instance stored as self.net + """ + self.logger("Building mininet topology.") + + defaultSwitchClass = configureP4Switch( + sw_path=self.bmv2_exe, + json_path=self.switch_json, + log_console=True, + pcap_dump=self.pcap_dir) + + self.topo = ExerciseTopo(self.hosts, self.switches, self.links, self.log_dir, self.bmv2_exe, self.pcap_dir) + + self.net = Mininet(topo = self.topo, + link = TCLink, + host = P4Host, + switch = defaultSwitchClass, + controller = None) + + def program_switch_p4runtime(self, sw_name, sw_dict): + """ This method will use P4Runtime to program the switch using the + content of the runtime JSON file as input. + """ + sw_obj = self.net.get(sw_name) + grpc_port = sw_obj.grpc_port + device_id = sw_obj.device_id + runtime_json = sw_dict['runtime_json'] + self.logger('Configuring switch %s using P4Runtime with file %s' % (sw_name, runtime_json)) + with open(runtime_json, 'r') as sw_conf_file: + outfile = '%s/%s-p4runtime-requests.txt' %(self.log_dir, sw_name) + p4runtime_lib.simple_controller.program_switch( + addr='127.0.0.1:%d' % grpc_port, + device_id=device_id, + sw_conf_file=sw_conf_file, + workdir=os.getcwd(), + proto_dump_fpath=outfile) + + def program_switch_cli(self, sw_name, sw_dict): + """ This method will start up the CLI and use the contents of the + command files as input. + """ + cli = 'simple_switch_CLI' + # get the port for this particular switch's thrift server + sw_obj = self.net.get(sw_name) + thrift_port = sw_obj.thrift_port + + cli_input_commands = sw_dict['cli_input'] + self.logger('Configuring switch %s with file %s' % (sw_name, cli_input_commands)) + with open(cli_input_commands, 'r') as fin: + cli_outfile = '%s/%s_cli_output.log'%(self.log_dir, sw_name) + with open(cli_outfile, 'w') as fout: + subprocess.Popen([cli, '--thrift-port', str(thrift_port)], + stdin=fin, stdout=fout) + + def program_switches(self): + """ This method will program each switch using the BMv2 CLI and/or + P4Runtime, depending if any command or runtime JSON files were + provided for the switches. + """ + for sw_name, sw_dict in self.switches.iteritems(): + if 'cli_input' in sw_dict: + self.program_switch_cli(sw_name, sw_dict) + if 'runtime_json' in sw_dict: + self.program_switch_p4runtime(sw_name, sw_dict) + + def program_hosts(self): + """ Execute any commands provided in the topology.json file on each Mininet host + """ + for host_name, host_info in self.hosts.items(): + h = self.net.get(host_name) + if "commands" in host_info: + for cmd in host_info["commands"]: + h.cmd(cmd) + + + def do_net_cli(self): + """ Starts up the mininet CLI and prints some helpful output. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + for s in self.net.switches: + s.describe() + for h in self.net.hosts: + h.describe() + self.logger("Starting mininet CLI") + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + print('') + print('======================================================================') + print('Welcome to the BMV2 Mininet CLI!') + print('======================================================================') + print('Your P4 program is installed into the BMV2 software switch') + print('and your initial runtime configuration is loaded. You can interact') + print('with the network using the mininet CLI below.') + print('') + if self.switch_json: + print('To inspect or change the switch configuration, connect to') + print('its CLI from your host operating system using this command:') + print(' simple_switch_CLI --thrift-port ') + print('') + print('To view a switch log, run this command from your host OS:') + print(' tail -f %s/.log' % self.log_dir) + print('') + print('To view the switch output pcap, check the pcap files in %s:' % self.pcap_dir) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap') + print('') + if 'grpc' in self.bmv2_exe: + print('To view the P4Runtime requests sent to the switch, check the') + print('corresponding txt file in %s:' % self.log_dir) + print(' for example run: cat %s/s1-p4runtime-requests.txt' % self.log_dir) + print('') + + CLI(self.net) + + +def get_args(): + cwd = os.getcwd() + default_logs = os.path.join(cwd, 'logs') + default_pcaps = os.path.join(cwd, 'pcaps') + parser = argparse.ArgumentParser() + parser.add_argument('-q', '--quiet', help='Suppress log messages.', + action='store_true', required=False, default=False) + parser.add_argument('-t', '--topo', help='Path to topology json', + type=str, required=False, default='./topology.json') + parser.add_argument('-l', '--log-dir', type=str, required=False, default=default_logs) + parser.add_argument('-p', '--pcap-dir', type=str, required=False, default=default_pcaps) + parser.add_argument('-j', '--switch_json', type=str, required=False) + parser.add_argument('-b', '--behavioral-exe', help='Path to behavioral executable', + type=str, required=False, default='simple_switch') + return parser.parse_args() + + +if __name__ == '__main__': + # from mininet.log import setLogLevel + # setLogLevel("info") + + args = get_args() + exercise = ExerciseRunner(args.topo, args.log_dir, args.pcap_dir, + args.switch_json, args.behavioral_exe, args.quiet) + + exercise.run_exercise() +