mirror of
https://github.com/opennetworkinglab/onos.git
synced 2026-05-05 04:06:49 +02:00
Various improvements to P4 dev env
- Better handling of BMv2 crashes in bmv2.py (with watchdog and logging) - bm-* commands for easy access to BMv2 log, CLI, etc Change-Id: I1c79acda641171566d8e1162442c7f377bb273fe
This commit is contained in:
parent
dbd74577b3
commit
f11513d1bc
@ -2,9 +2,11 @@ import os
|
||||
import socket
|
||||
import re
|
||||
import json
|
||||
import threading
|
||||
import urllib2
|
||||
|
||||
import time
|
||||
from contextlib import closing
|
||||
|
||||
from mininet.log import info, warn, error
|
||||
from mininet.node import Switch, Host
|
||||
@ -13,12 +15,13 @@ if 'ONOS_ROOT' not in os.environ:
|
||||
error("ERROR: environment var $ONOS_ROOT not set")
|
||||
exit()
|
||||
|
||||
BMV2_TARGET = 'simple_switch_grpc'
|
||||
SIMPLE_SWITCH_GRPC = 'simple_switch_grpc'
|
||||
ONOS_ROOT = os.environ["ONOS_ROOT"]
|
||||
CPU_PORT = 255
|
||||
PKT_BYTES_TO_DUMP = 80
|
||||
VALGRIND_PREFIX = 'valgrind --leak-check=yes'
|
||||
SWITCH_START_TIMEOUT = 5 #seconds
|
||||
SWITCH_START_TIMEOUT = 5 # seconds
|
||||
BMV2_LOG_LINES = 5
|
||||
|
||||
|
||||
def parseBoolean(value):
|
||||
if value in ['1', 1, 'true', 'True']:
|
||||
@ -27,6 +30,34 @@ def parseBoolean(value):
|
||||
return False
|
||||
|
||||
|
||||
def pickUnusedPort():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(('localhost', 0))
|
||||
addr, port = s.getsockname()
|
||||
s.close()
|
||||
return port
|
||||
|
||||
|
||||
def writeToFile(path, value):
|
||||
with open(path, "w") as f:
|
||||
f.write(str(value))
|
||||
|
||||
|
||||
def watchDog(sw):
|
||||
while True:
|
||||
if sw.stopped:
|
||||
return
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
if s.connect_ex(('127.0.0.1', sw.grpcPort)) == 0:
|
||||
time.sleep(1)
|
||||
else:
|
||||
warn("\n*** WARN: BMv2 instance %s (%s) died!\n"
|
||||
% (sw.deviceId, sw.name))
|
||||
sw.printBmv2Log()
|
||||
print ("-" * 80) + "\n"
|
||||
return
|
||||
|
||||
|
||||
class ONOSHost(Host):
|
||||
def __init__(self, name, inNamespace=True, **params):
|
||||
Host.__init__(self, name, inNamespace=inNamespace, **params)
|
||||
@ -34,7 +65,7 @@ class ONOSHost(Host):
|
||||
def config(self, **params):
|
||||
r = super(Host, self).config(**params)
|
||||
for off in ["rx", "tx", "sg"]:
|
||||
cmd = "/sbin/ethtool --offload %s %s off"\
|
||||
cmd = "/sbin/ethtool --offload %s %s off" \
|
||||
% (self.defaultIntf(), off)
|
||||
self.cmd(cmd)
|
||||
# disable IPv6
|
||||
@ -48,63 +79,51 @@ class ONOSBmv2Switch(Switch):
|
||||
"""BMv2 software switch with gRPC server"""
|
||||
|
||||
deviceId = 0
|
||||
instanceCount = 0
|
||||
|
||||
def __init__(self, name, json=None, debugger=False, loglevel="warn", elogger=False,
|
||||
persistent=False, grpcPort=None, thriftPort=None, netcfg=True, dryrun=False,
|
||||
pipeconfId="", pktdump=False, valgrind=False, netcfgDelay=0,
|
||||
**kwargs):
|
||||
def __init__(self, name, json=None, debugger=False, loglevel="warn",
|
||||
elogger=False, grpcPort=None, cpuPort=255,
|
||||
thriftPort=None, netcfg=True, dryrun=False, pipeconfId="",
|
||||
pktdump=False, valgrind=False, **kwargs):
|
||||
Switch.__init__(self, name, **kwargs)
|
||||
self.grpcPort = ONOSBmv2Switch.pickUnusedPort() if not grpcPort else grpcPort
|
||||
self.thriftPort = ONOSBmv2Switch.pickUnusedPort() if not thriftPort else thriftPort
|
||||
self.grpcPort = pickUnusedPort() if not grpcPort else grpcPort
|
||||
self.thriftPort = pickUnusedPort() if not thriftPort else thriftPort
|
||||
if self.dpid:
|
||||
self.deviceId = int(self.dpid, 0 if 'x' in self.dpid else 16)
|
||||
else:
|
||||
self.deviceId = ONOSBmv2Switch.deviceId
|
||||
ONOSBmv2Switch.deviceId += 1
|
||||
self.cpuPort = cpuPort
|
||||
self.json = json
|
||||
self.debugger = parseBoolean(debugger)
|
||||
self.loglevel = loglevel
|
||||
self.logfile = '/tmp/bmv2-%d.log' % self.deviceId
|
||||
# Important: Mininet removes all /tmp/*.log files in case of exceptions.
|
||||
# We want to be able to see the bmv2 log if anything goes wrong, hence
|
||||
# avoid the .log extension.
|
||||
self.logfile = '/tmp/bmv2-%d-log' % self.deviceId
|
||||
self.elogger = parseBoolean(elogger)
|
||||
self.pktdump = parseBoolean(pktdump)
|
||||
self.persistent = parseBoolean(persistent)
|
||||
self.netcfg = parseBoolean(netcfg)
|
||||
self.dryrun = parseBoolean(dryrun)
|
||||
self.valgrind = parseBoolean(valgrind)
|
||||
self.netcfgDelay = netcfgDelay
|
||||
self.netcfgfile = '/tmp/bmv2-%d-netcfg.json' % self.deviceId
|
||||
self.pipeconfId = pipeconfId
|
||||
if persistent:
|
||||
self.exectoken = "/tmp/bmv2-%d-exec-token" % self.deviceId
|
||||
self.cmd("touch %s" % self.exectoken)
|
||||
# Store thrift port for future uses.
|
||||
self.cmd("echo %d > /tmp/bmv2-%d-grpc-port" % (self.grpcPort, self.deviceId))
|
||||
|
||||
if 'longitude' in kwargs:
|
||||
self.longitude = kwargs['longitude']
|
||||
else:
|
||||
self.longitude = None
|
||||
|
||||
if 'latitude' in kwargs:
|
||||
self.latitude = kwargs['latitude']
|
||||
else:
|
||||
self.latitude = None
|
||||
|
||||
self.longitude = kwargs['longitude'] if 'longitude' in kwargs else None
|
||||
self.latitude = kwargs['latitude'] if 'latitude' in kwargs else None
|
||||
self.onosDeviceId = "device:bmv2:%d" % self.deviceId
|
||||
self.logfd = None
|
||||
self.bmv2popen = None
|
||||
self.stopped = False
|
||||
|
||||
@classmethod
|
||||
def pickUnusedPort(cls):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(('localhost', 0))
|
||||
addr, port = s.getsockname()
|
||||
s.close()
|
||||
return port
|
||||
# Remove files from previous executions
|
||||
self.cleanupTmpFiles()
|
||||
|
||||
writeToFile("/tmp/bmv2-%d-grpc-port" % self.deviceId, self.grpcPort)
|
||||
writeToFile("/tmp/bmv2-%d-thrift-port" % self.deviceId, self.thriftPort)
|
||||
|
||||
def getSourceIp(self, dstIP):
|
||||
"""
|
||||
Queries the Linux routing table to get the source IP that can talk with dstIP, and vice
|
||||
versa.
|
||||
Queries the Linux routing table to get the source IP that can talk with
|
||||
dstIP, and vice versa.
|
||||
"""
|
||||
ipRouteOut = self.cmd('ip route get %s' % dstIP)
|
||||
r = re.search(r"src (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", ipRouteOut)
|
||||
@ -158,7 +177,7 @@ class ONOSBmv2Switch(Switch):
|
||||
"""
|
||||
srcIP = self.getSourceIp(controllerIP)
|
||||
if not srcIP:
|
||||
warn("WARN: unable to get device IP address, won't do onos-netcfg")
|
||||
warn("*** WARN: unable to get switch IP address, won't do netcfg\n")
|
||||
return
|
||||
|
||||
cfgData = {
|
||||
@ -177,19 +196,52 @@ class ONOSBmv2Switch(Switch):
|
||||
url = 'http://%s:8181/onos/v1/network/configuration/' % controllerIP
|
||||
# Instantiate password manager for HTTP auth
|
||||
pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
pm.add_password(None, url, os.environ['ONOS_WEB_USER'], os.environ['ONOS_WEB_PASS'])
|
||||
urllib2.install_opener(urllib2.build_opener(urllib2.HTTPBasicAuthHandler(pm)))
|
||||
pm.add_password(None, url,
|
||||
os.environ['ONOS_WEB_USER'],
|
||||
os.environ['ONOS_WEB_PASS'])
|
||||
urllib2.install_opener(urllib2.build_opener(
|
||||
urllib2.HTTPBasicAuthHandler(pm)))
|
||||
# Push config data to controller
|
||||
req = urllib2.Request(url, json.dumps(cfgData), {'Content-Type': 'application/json'})
|
||||
req = urllib2.Request(url, json.dumps(cfgData),
|
||||
{'Content-Type': 'application/json'})
|
||||
try:
|
||||
f = urllib2.urlopen(req)
|
||||
print f.read()
|
||||
f.close()
|
||||
except urllib2.URLError as e:
|
||||
warn("WARN: unable to push config to ONOS (%s)" % e.reason)
|
||||
warn("*** WARN: unable to push config to ONOS (%s)\n" % e.reason)
|
||||
|
||||
def start(self, controllers):
|
||||
args = [BMV2_TARGET, '--device-id %s' % str(self.deviceId)]
|
||||
bmv2Args = [SIMPLE_SWITCH_GRPC] + self.grpcTargetArgs()
|
||||
if self.valgrind:
|
||||
bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
|
||||
|
||||
cmdString = " ".join(bmv2Args)
|
||||
|
||||
if self.dryrun:
|
||||
info("\n*** DRY RUN (not executing bmv2)")
|
||||
|
||||
info("\nStarting BMv2 target: %s\n" % cmdString)
|
||||
|
||||
try:
|
||||
if not self.dryrun:
|
||||
# Start the switch
|
||||
self.logfd = open(self.logfile, "w")
|
||||
self.bmv2popen = self.popen(cmdString,
|
||||
stdout=self.logfd,
|
||||
stderr=self.logfd)
|
||||
self.waitBmv2Start()
|
||||
# We want to be notified if BMv2 dies...
|
||||
threading.Thread(target=watchDog, args=[self]).start()
|
||||
|
||||
self.doOnosNetcfg(self.controllerIp(controllers))
|
||||
except Exception as ex:
|
||||
self.killBmv2()
|
||||
self.printBmv2Log()
|
||||
raise ex
|
||||
|
||||
def grpcTargetArgs(self):
|
||||
args = ['--device-id %s' % str(self.deviceId)]
|
||||
for port, intf in self.intfs.items():
|
||||
if not intf.IP():
|
||||
args.append('-i %d@%s' % (port, intf.name))
|
||||
@ -200,74 +252,73 @@ class ONOSBmv2Switch(Switch):
|
||||
args.append('--debugger')
|
||||
args.append('--log-console')
|
||||
if self.pktdump:
|
||||
args.append('--pcap --dump-packet-data %d' % PKT_BYTES_TO_DUMP)
|
||||
args.append('--pcap --dump-packet-data %s' % PKT_BYTES_TO_DUMP)
|
||||
args.append('-L%s' % self.loglevel)
|
||||
args.append('--thrift-port %d' % self.thriftPort)
|
||||
args.append('--thrift-port %s' % self.thriftPort)
|
||||
if not self.json:
|
||||
args.append('--no-p4')
|
||||
else:
|
||||
args.append(self.json)
|
||||
|
||||
# gRPC target-specific options.
|
||||
# gRPC target-specific options
|
||||
args.append('--')
|
||||
args.append('--cpu-port %d' % CPU_PORT)
|
||||
args.append('--grpc-server-addr 0.0.0.0:%d' % self.grpcPort)
|
||||
args.append('--cpu-port %s' % self.cpuPort)
|
||||
args.append('--grpc-server-addr 0.0.0.0:%s' % self.grpcPort)
|
||||
return args
|
||||
|
||||
bmv2cmd = " ".join(args)
|
||||
if self.valgrind:
|
||||
bmv2cmd = "%s %s" % (VALGRIND_PREFIX, bmv2cmd)
|
||||
if self.dryrun:
|
||||
info("\n*** DRY RUN (not executing bmv2)")
|
||||
info("\nStarting BMv2 target: %s\n" % bmv2cmd)
|
||||
def waitBmv2Start(self):
|
||||
# Wait for switch to open gRPC port, before sending ONOS the netcfg.
|
||||
# Include time-out just in case something hangs.
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
endtime = time.time() + SWITCH_START_TIMEOUT
|
||||
while True:
|
||||
result = sock.connect_ex(('127.0.0.1', self.grpcPort))
|
||||
if result == 0:
|
||||
# The port is open. Let's go! (Close socket first)
|
||||
sock.close()
|
||||
break
|
||||
# Port is not open yet. If there is time, we wait a bit.
|
||||
if endtime > time.time():
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
# Time's up.
|
||||
raise Exception("Switch did not start before timeout")
|
||||
|
||||
if self.persistent:
|
||||
# Bash loop to re-exec the switch if it crashes.
|
||||
bmv2cmd = "(while [ -e {} ]; do {} ; sleep 1; done;)".format(self.exectoken, bmv2cmd)
|
||||
def printBmv2Log(self):
|
||||
if os.path.isfile(self.logfile):
|
||||
print "-" * 80
|
||||
print "BMv2 %d log (from %s):" % (self.deviceId, self.logfile)
|
||||
with open(self.logfile, 'r') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) > BMV2_LOG_LINES:
|
||||
print "..."
|
||||
for line in lines[-BMV2_LOG_LINES:]:
|
||||
print line.rstrip()
|
||||
|
||||
cmdStr = "{} > {} 2>&1 &".format(bmv2cmd, self.logfile)
|
||||
|
||||
# Starts the switch.
|
||||
if not self.dryrun:
|
||||
out = self.cmd(cmdStr)
|
||||
if out:
|
||||
print out
|
||||
if self.netcfg:
|
||||
time.sleep(self.netcfgDelay)
|
||||
|
||||
try: # onos.py
|
||||
@staticmethod
|
||||
def controllerIp(controllers):
|
||||
try:
|
||||
# onos.py
|
||||
clist = controllers[0].nodes()
|
||||
except AttributeError:
|
||||
clist = controllers
|
||||
assert len(clist) > 0
|
||||
cip = clist[0].IP()
|
||||
return clist[0].IP()
|
||||
|
||||
# Wait for switch to open gRPC port, before sending ONOS the netcfg.json.
|
||||
# Include time-out just in case something hangs.
|
||||
if not self.dryrun:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
endtime = time.time() + SWITCH_START_TIMEOUT
|
||||
while True:
|
||||
result = sock.connect_ex(('127.0.0.1', self.grpcPort))
|
||||
if result == 0:
|
||||
# The port is open. Let's go! (Close socket first)
|
||||
sock.close()
|
||||
break
|
||||
# Port is not open yet. If there is time, we wait a bit.
|
||||
if endtime > time.time():
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
# Time's up.
|
||||
raise Exception("Switch did not start before {} second timeout. Exiting.\n"
|
||||
.format(SWITCH_START_TIMEOUT))
|
||||
exit()
|
||||
def killBmv2(self, log=False):
|
||||
if self.bmv2popen is not None:
|
||||
self.bmv2popen.kill()
|
||||
if self.logfd is not None:
|
||||
if log:
|
||||
self.logfd.write("*** PROCESS TERMINATED BY MININET ***\n")
|
||||
self.logfd.close()
|
||||
|
||||
self.doOnosNetcfg(cip)
|
||||
def cleanupTmpFiles(self):
|
||||
self.cmd("rm -f /tmp/bmv2-%d-*" % self.deviceId)
|
||||
|
||||
def stop(self, deleteIntfs=True):
|
||||
"""Terminate switch."""
|
||||
self.cmd("rm -f /tmp/bmv2-%d-*" % self.deviceId)
|
||||
self.cmd("rm -f /tmp/bmv2-%d.log*" % self.deviceId)
|
||||
self.cmd('kill %' + BMV2_TARGET)
|
||||
self.stopped = True
|
||||
self.killBmv2(log=True)
|
||||
Switch.stop(self, deleteIntfs)
|
||||
|
||||
|
||||
|
||||
52
tools/dev/p4vm/bm-commands.sh
Normal file
52
tools/dev/p4vm/bm-commands.sh
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BUILD_DIR=~/p4tools
|
||||
|
||||
export BMV2_PATH=${BUILD_DIR}/bmv2
|
||||
export P4RUNTIME_PATH=${BUILD_DIR}/p4runtime
|
||||
|
||||
bm-cli () {
|
||||
if [ -z "$1" ]; then
|
||||
echo "No argument supplied. Usage: bm-cli <BMV2 DEVICE ID>"
|
||||
return
|
||||
fi
|
||||
tport=$(head -n 1 /tmp/bmv2-$1-thrift-port)
|
||||
echo "Starting CLI for BMv2 instance $1 (Thrift port $tport)..."
|
||||
sudo ${BMV2_PATH}/tools/runtime_CLI.py --thrift-port ${tport} ${@:2}
|
||||
}
|
||||
|
||||
bm-dbg () {
|
||||
if [ -z "$1" ]; then
|
||||
echo "No argument supplied. Usage: bm-dbg <BMV2 DEVICE ID>"
|
||||
return
|
||||
fi
|
||||
tport=$(head -n 1 /tmp/bmv2-$1-thrift-port)
|
||||
echo "Starting debugger for BMv2 instance $1 (Thrift port $tport)..."
|
||||
sudo ${BMV2_PATH}/tools/p4dbg.py --thrift-port ${tport} ${@:2}
|
||||
}
|
||||
|
||||
bm-nmsg () {
|
||||
if [ -z "$1" ]; then
|
||||
echo "No argument supplied. Usage: bm-nmsg <BMV2 DEVICE ID>"
|
||||
return
|
||||
fi
|
||||
tport=$(head -n 1 /tmp/bmv2-$1-thrift-port)
|
||||
echo "Starting nanomsg event listener for BMv2 instance $1 (Thrift port $tport)..."
|
||||
sudo ${BMV2_PATH}/tools/nanomsg_client.py --thrift-port ${tport} ${@:2}
|
||||
}
|
||||
|
||||
bm-log () {
|
||||
if [ -z "$1" ]; then
|
||||
echo "No argument supplied. Usage: bm-log <BMV2 DEVICE ID>"
|
||||
return
|
||||
fi
|
||||
echo "Showing log for BMv2 instance $1..."
|
||||
echo "---"
|
||||
tail -f /tmp/bmv2-$1-log
|
||||
}
|
||||
|
||||
bm-sysrepo-reset () {
|
||||
echo "Resetting sysrepo data store..."
|
||||
sudo rm -rf /etc/sysrepo/data/*
|
||||
sudo ${P4RUNTIME_PATH}/proto/sysrepo/install_yangs.sh
|
||||
}
|
||||
@ -7,6 +7,7 @@ echo "sdn:rocks" | chpasswd
|
||||
echo "sdn ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/99_sdn
|
||||
chmod 440 /etc/sudoers.d/99_sdn
|
||||
usermod -aG vboxsf sdn
|
||||
update-locale LC_ALL="en_US.UTF-8"
|
||||
|
||||
# Java 8
|
||||
apt-get install software-properties-common -y
|
||||
@ -17,7 +18,11 @@ echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 select true" |
|
||||
apt-get -y install \
|
||||
oracle-java8-installer oracle-java8-set-default \
|
||||
zip unzip \
|
||||
bridge-utils
|
||||
bridge-utils \
|
||||
avahi-daemon \
|
||||
htop \
|
||||
valgrind \
|
||||
git-review
|
||||
|
||||
tee -a /etc/ssh/sshd_config <<EOF
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ tee -a ~/.profile <<EOF
|
||||
# ONOS
|
||||
export ONOS_ROOT=~/onos
|
||||
source ~/onos/tools/dev/bash_profile
|
||||
source ~/onos/tools/dev/p4vm/bm-commands.sh
|
||||
EOF
|
||||
source ~/.profile
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user