From bce45ca8e697036c58a0c04ee1e9fe6f7cde1aa7 Mon Sep 17 00:00:00 2001 From: Robert MacDavid Date: Wed, 1 Nov 2017 17:43:30 -0400 Subject: [PATCH] New build script, and now building mininet from source (#69) * moved basic exercise to new run script * forgot some files * CLI output now goes to logs * typo, and forgot topo file * fixed net.stop() bug * added temporary change to makefile * typos hard --- P4D2_2017_Fall/exercises/basic/Makefile | 1 + P4D2_2017_Fall/exercises/basic/README.md | 12 +- P4D2_2017_Fall/exercises/basic/p4app.json | 32 -- P4D2_2017_Fall/exercises/basic/run.sh | 5 - P4D2_2017_Fall/exercises/basic/topology.json | 16 + P4D2_2017_Fall/utils/Makefile | 32 ++ P4D2_2017_Fall/utils/run_exercise.py | 344 +++++++++++++++++++ P4D2_2017_Fall/vm/root-bootstrap.sh | 1 - P4D2_2017_Fall/vm/user-bootstrap.sh | 6 + 9 files changed, 409 insertions(+), 40 deletions(-) create mode 100644 P4D2_2017_Fall/exercises/basic/Makefile delete mode 100644 P4D2_2017_Fall/exercises/basic/p4app.json delete mode 100755 P4D2_2017_Fall/exercises/basic/run.sh create mode 100644 P4D2_2017_Fall/exercises/basic/topology.json create mode 100644 P4D2_2017_Fall/utils/Makefile create mode 100755 P4D2_2017_Fall/utils/run_exercise.py diff --git a/P4D2_2017_Fall/exercises/basic/Makefile b/P4D2_2017_Fall/exercises/basic/Makefile new file mode 100644 index 0000000..f378756 --- /dev/null +++ b/P4D2_2017_Fall/exercises/basic/Makefile @@ -0,0 +1 @@ +include ../../utils/Makefile diff --git a/P4D2_2017_Fall/exercises/basic/README.md b/P4D2_2017_Fall/exercises/basic/README.md index e5203e8..51f1a44 100644 --- a/P4D2_2017_Fall/exercises/basic/README.md +++ b/P4D2_2017_Fall/exercises/basic/README.md @@ -32,7 +32,7 @@ up a switch in Mininet to test its behavior. 1. In your shell, run: ```bash - ./run.sh + make run ``` This will: * compile `basic.p4`, and @@ -57,6 +57,14 @@ server. In `h2`'s xterm, start the server: ``` The message will not be received. 5. 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 + ``` The message was not received because each switch is programmed according to `basic.p4`, which drops all packets on arrival. @@ -154,7 +162,7 @@ running in the background. Use the following command to clean up these instances: ```bash -mn -c +make stop ``` ## Next Steps diff --git a/P4D2_2017_Fall/exercises/basic/p4app.json b/P4D2_2017_Fall/exercises/basic/p4app.json deleted file mode 100644 index 92df556..0000000 --- a/P4D2_2017_Fall/exercises/basic/p4app.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "program": "basic.p4", - "language": "p4-16", - "targets": { - "multiswitch": { - "auto-control-plane": true, - "cli": true, - "pcap_dump": true, - "bmv2_log": true, - "links": [["h1", "s1"], ["s1", "s2"], ["s1", "s3"], ["s3", "s2"], ["s2", "h2"], ["s3", "h3"]], - "hosts": { - "h1": { - }, - "h2": { - }, - "h3": { - } - }, - "switches": { - "s1": { - "entries": "s1-commands.txt" - }, - "s2": { - "entries": "s2-commands.txt" - }, - "s3": { - "entries": "s3-commands.txt" - } - } - } - } -} diff --git a/P4D2_2017_Fall/exercises/basic/run.sh b/P4D2_2017_Fall/exercises/basic/run.sh deleted file mode 100755 index d5c1947..0000000 --- a/P4D2_2017_Fall/exercises/basic/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -P4APPRUNNER=../../utils/p4apprunner.py -mkdir -p build -tar -czf build/p4app.tgz * --exclude='build' -#cd build -sudo python $P4APPRUNNER p4app.tgz --build-dir ./build diff --git a/P4D2_2017_Fall/exercises/basic/topology.json b/P4D2_2017_Fall/exercises/basic/topology.json new file mode 100644 index 0000000..e33477c --- /dev/null +++ b/P4D2_2017_Fall/exercises/basic/topology.json @@ -0,0 +1,16 @@ +{ + "hosts": [ + "h1", + "h2", + "h3" + ], + "switches": { + "s1": { "cli_input" : "s1-commands.txt" }, + "s2": { "cli_input" : "s2-commands.txt" }, + "s3": { "cli_input" : "s3-commands.txt" } + }, + "links": [ + ["h1", "s1"], ["s1", "s2"], ["s1", "s3"], + ["s3", "s2"], ["s2", "h2"], ["s3", "h3"] + ] +} diff --git a/P4D2_2017_Fall/utils/Makefile b/P4D2_2017_Fall/utils/Makefile new file mode 100644 index 0000000..0591ad4 --- /dev/null +++ b/P4D2_2017_Fall/utils/Makefile @@ -0,0 +1,32 @@ +BUILD_DIR = build +PCAP_DIR = pcaps +LOG_DIR = logs + +TOPO = topology.json +P4C = p4c-bm2-ss +RUN_SCRIPT = ../../utils/run_exercise.py + +source := $(wildcard *.p4) +outfile := $(source:.p4=.json) + +compiled_json := $(BUILD_DIR)/$(outfile) + +all: run + +run: build + sudo python $(RUN_SCRIPT) -t $(TOPO) -j $(compiled_json) + +stop: + sudo mn -c + +build: dirs $(compiled_json) + +$(BUILD_DIR)/%.json: %.p4 + $(P4C) --p4v 16 -o $@ $< + +dirs: + mkdir -p $(BUILD_DIR) $(PCAP_DIR) $(LOG_DIR) + +clean: stop + rm -f *.pcap + rm -rf $(BUILD_DIR) $(PCAP_DIR) $(LOG_DIR) diff --git a/P4D2_2017_Fall/utils/run_exercise.py b/P4D2_2017_Fall/utils/run_exercise.py new file mode 100755 index 0000000..55ae9a1 --- /dev/null +++ b/P4D2_2017_Fall/utils/run_exercise.py @@ -0,0 +1,344 @@ +#!/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 + +# this path is needed to import p4_mininet.py from the bmv2 repo +sys.path.append('/home/vagrant/behavioral-model/mininet') +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 + + + +first_thrift_port = 9090 +next_thrift_port = first_thrift_port + +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. + """ + 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 + + +class ExerciseTopo(Topo): + """ The mininet topology class for the P4 tutorial exercises. + A custom class is used because the exercises make a few topology + assumptions, mostly about the IP and MAC addresses. + """ + def __init__(self, hosts, switches, links, log_dir, **opts): + Topo.__init__(self, **opts) + host_links = [] + switch_links = [] + + for link in links: + if link['node1'][0] == 'h': + host_links.append(link) + else: + switch_links.append(link) + + link_sort_key = lambda x: x['node1'] + x['node2'] + # Links must be added in a sorted order so bmv2 port numbers are predictable + host_links.sort(key=link_sort_key) + switch_links.sort(key=link_sort_key) + + for sw in switches: + self.addSwitch(sw, log_file="%s/%s.log" %(log_dir, sw)) + + for link in host_links: + host_name = link['node1'] + host_sw = link['node2'] + host_num = int(host_name[1:]) + sw_num = int(host_sw[1:]) + host_ip = "10.0.%d.%d" % (sw_num, host_num) + host_mac = '00:00:00:00:%02x:%02x' % (sw_num, host_num) + # Each host IP should be /24, so all exercise traffic will use the + # default gateway (the switch) without sending ARP requests. + self.addHost(host_name, ip=host_ip+'/24', mac=host_mac) + self.addLink(host_name, host_sw, + delay=link['latency'], bw=link['bandwidth'], + addr1=host_mac, addr2=host_mac) + + for link in switch_links: + self.addLink(link['node1'], link['node2'], + delay=link['latency'], bw=link['bandwidth']) + + +class ExerciseRunner: + """ + Attributes: + log_dir : string // directory for mininet log files + pcap_dir : string // directory for mininet switch pcap files + quiet : bool // determines if we print logger messages + + hosts : list // list of mininet host names + switches : dict // mininet host names and their associated properties + links : list // list of mininet link properties + + switch_json : string // json of the compiled p4 example + bmv2_exe : string // name or path of the p4 switch binary + + topo : Topo object // The mininet topology instance + net : Mininet object // The mininet instance + + """ + def logger(self, *items): + if not self.quiet: + print(' '.join(items)) + + def formatLatency(l): + """ Helper method for parsing link latencies from the topology json. """ + if isinstance(latencies[l], (str, unicode)): + return l + else: + return str(l) + "ms" + + + def __init__(self, topo_file, log_dir, pcap_dir, + switch_json, bmv2_exe='simple_switch', quiet=False): + """ Initializes some attributes and reads the topology json. Does not + actually run the exercise. Use run_exercise() for that. + + Arguments: + topo_file : string // A json file which describes the exercise's + mininet topology. + log_dir : string // Path to a directory for storing exercise logs + pcap_dir : string // Ditto, but for mininet switch pcap files + switch_json : string // Path to a compiled p4 json for bmv2 + bmv2_exe : string // Path to the p4 behavioral binary + quiet : bool // Enable/disable script debug messages + """ + + self.quiet = quiet + self.logger('Reading topology file.') + with open(topo_file, 'r') as f: + topo = json.load(f) + self.hosts = topo['hosts'] + self.switches = topo['switches'] + self.links = self.parse_links(topo['links']) + + # Ensure all the needed directories exist and are directories + for dir_name in [log_dir, pcap_dir]: + if not os.path.isdir(dir_name): + if os.path.exists(dir_name): + raise Exception("'%s' exists and is not a directory!" % dir_name) + os.mkdir(dir_name) + self.log_dir = log_dir + self.pcap_dir = pcap_dir + self.switch_json = switch_json + self.bmv2_exe = bmv2_exe + + + def run_exercise(self): + """ Sets up the mininet instance, programs the switches, + and starts the mininet CLI. This is the main method to run after + initializing the object. + """ + # Initialize mininet with the topology specified by the config + self.create_network() + self.net.start() + sleep(1) + + # some programming that must happen after the net has started + self.program_hosts() + self.program_switches() + + # wait for that to finish. Not sure how to do this better + sleep(1) + + self.do_net_cli() + # stop right after the CLI is exited + self.net.stop() + + + def parse_links(self, unparsed_links): + """ Given a list of links descriptions of the form [node1, node2, latency, bandwidth] + with the latency and bandwidth being optional, parses these descriptions + into dictionaries and store them as self.links + """ + links = [] + for link in unparsed_links: + # make sure each link's endpoints are ordered alphabetically + s, t, = link[0], link[1] + if s > t: + s,t = t,s + + link_dict = {'node1':s, + 'node2':t, + 'latency':'0ms', + 'bandwidth':None + } + if len(link) > 2: + link_dict['latency'] = self.formatLatency(link[2]) + if len(link) > 3: + link_dict['bandwidth'] = link[3] + + if link_dict['node1'][0] == 'h': + assert link_dict['node2'][0] == 's', 'Hosts should be connected to switches, not ' + str(link_dict['node2']) + links.append(link_dict) + return links + + + def create_network(self): + """ Create the mininet network object, and store it as self.net. + + Side effects: + - Mininet topology instance stored as self.topo + - Mininet instance stored as self.net + """ + self.logger("Building mininet topology.") + + self.topo = ExerciseTopo(self.hosts, self.switches.keys(), self.links, self.log_dir) + + switchClass = configureP4Switch( + sw_path=self.bmv2_exe, + json_path=self.switch_json, + log_console=True, + pcap_dump=self.pcap_dir) + + self.net = Mininet(topo = self.topo, + link = TCLink, + host = P4Host, + switch = switchClass, + controller = None) + + + def program_switches(self): + """ If any command files were provided for the switches, + this method will start up the CLI on each switch and use the + contents of the command files as input. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + cli = 'simple_switch_CLI' + for sw_name, sw_dict in self.switches.iteritems(): + if 'cli_input' not in sw_dict: continue + # get the port for this particular switch's thrift server + sw_obj = self.net.get(sw_name) + thrift_port = sw_obj.thrift_port + + cli_input_commands = sw_dict['cli_input'] + self.logger('Configuring switch %s with file %s' % (sw_name, cli_input_commands)) + with open(cli_input_commands, 'r') as fin: + cli_outfile = '%s/%s_cli_output.log'%(self.log_dir, sw_name) + with open(cli_outfile, 'w') as fout: + subprocess.Popen([cli, '--thrift-port', str(thrift_port)], + stdin=fin, stdout=fout) + + def program_hosts(self): + """ Adds static ARP entries and default routes to each mininet host. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + for host_name in self.topo.hosts(): + h = self.net.get(host_name) + h_iface = h.intfs.values()[0] + link = h_iface.link + + sw_iface = link.intf1 if link.intf1 != h_iface else link.intf2 + # phony IP to lie to the host about + host_id = int(host_name[1:]) + sw_ip = '10.0.%d.254' % host_id + + # Ensure each host's interface name is unique, or else + # mininet cannot shutdown gracefully + h.defaultIntf().rename('%s-eth0' % host_name) + # static arp entries and default routes + h.cmd('arp -i %s -s %s %s' % (h_iface.name, sw_ip, sw_iface.mac)) + h.cmd('ethtool --offload %s rx off tx off' % h_iface.name) + h.cmd('ip route add %s dev %s' % (sw_ip, h_iface.name)) + h.setDefaultRoute("via %s" % sw_ip) + + + def do_net_cli(self): + """ Starts up the mininet CLI and prints some helpful output. + + Assumes: + - A mininet instance is stored as self.net and self.net.start() has + been called. + """ + self.logger("Starting mininet CLI") + for h in self.net.hosts: + h.describe() + # Generate a message that will be printed by the Mininet CLI to make + # interacting with the simple switch a little easier. + print('') + print('======================================================================') + print('Welcome to the BMV2 Mininet CLI!') + print('======================================================================') + print('Your P4 program is installed into the BMV2 software switch') + print('and your initial configuration is loaded. You can interact') + print('with the network using the mininet CLI below.') + print('') + print('To inspect or change the switch configuration, connect to') + print('its CLI from your host operating system using this command:') + print(' simple_switch_CLI --thrift-port ') + print('') + print('To view a switch log, run this command from your host OS:') + print(' tail -f %s/.log' % self.log_dir) + print('') + print('To view the switch output pcap, check the pcap files in %s:' % self.pcap_dir) + print(' for example run: sudo tcpdump -xxx -r s1-eth1.pcap') + print('') + + CLI(self.net) + + +def get_args(): + cwd = os.getcwd() + default_logs = os.path.join(cwd, 'logs') + default_pcaps = os.path.join(cwd, 'pcaps') + parser = argparse.ArgumentParser() + parser.add_argument('-q', '--quiet', help='Suppress log messages.', + action='store_true', required=False, default=False) + parser.add_argument('-t', '--topo', help='Path to topology json', + type=str, required=False, default='./topology.json') + parser.add_argument('-l', '--log-dir', type=str, required=False, default=default_logs) + parser.add_argument('-p', '--pcap-dir', type=str, required=False, default=default_pcaps) + parser.add_argument('-j', '--switch_json', type=str, required=True) + 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__': + args = get_args() + exercise = ExerciseRunner(args.topo, args.log_dir, args.pcap_dir, + args.switch_json, args.behavioral_exe, args.quiet) + + exercise.run_exercise() + diff --git a/P4D2_2017_Fall/vm/root-bootstrap.sh b/P4D2_2017_Fall/vm/root-bootstrap.sh index 4d0c034..21f71a7 100755 --- a/P4D2_2017_Fall/vm/root-bootstrap.sh +++ b/P4D2_2017_Fall/vm/root-bootstrap.sh @@ -47,7 +47,6 @@ apt-get install -y --no-install-recommends \ libtool \ lubuntu-desktop \ make \ - mininet \ mktemp \ pkg-config \ python \ diff --git a/P4D2_2017_Fall/vm/user-bootstrap.sh b/P4D2_2017_Fall/vm/user-bootstrap.sh index 19d9e46..63b00ff 100644 --- a/P4D2_2017_Fall/vm/user-bootstrap.sh +++ b/P4D2_2017_Fall/vm/user-bootstrap.sh @@ -13,6 +13,12 @@ GRPC_COMMIT="tags/v1.3.0" NUM_CORES=`grep -c ^processor /proc/cpuinfo` +# Mininet +git clone git://github.com/mininet/mininet mininet +cd mininet +sudo ./util/install.sh -nwv +cd .. + # Protobuf git clone https://github.com/google/protobuf.git cd protobuf