mirror of
https://github.com/opennetworkinglab/onos.git
synced 2025-10-14 17:01:02 +02:00
354 lines
12 KiB
Python
354 lines
12 KiB
Python
import json
|
|
import multiprocessing
|
|
import os
|
|
import random
|
|
import re
|
|
import socket
|
|
import threading
|
|
import urllib2
|
|
from contextlib import closing
|
|
|
|
import time
|
|
from mininet.log import info, warn
|
|
from mininet.node import Switch, Host
|
|
|
|
SIMPLE_SWITCH_GRPC = 'simple_switch_grpc'
|
|
PKT_BYTES_TO_DUMP = 80
|
|
VALGRIND_PREFIX = 'valgrind --leak-check=yes'
|
|
SWITCH_START_TIMEOUT = 5 # seconds
|
|
BMV2_LOG_LINES = 5
|
|
BMV2_DEFAULT_DEVICE_ID = 1
|
|
|
|
|
|
def parseBoolean(value):
|
|
if value in ['1', 1, 'true', 'True']:
|
|
return True
|
|
else:
|
|
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 ONOSBmv2Switch.mininet_exception == 1:
|
|
sw.killBmv2(log=False)
|
|
return
|
|
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 died!\n" % 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)
|
|
|
|
def config(self, **params):
|
|
r = super(Host, self).config(**params)
|
|
for off in ["rx", "tx", "sg"]:
|
|
cmd = "/sbin/ethtool --offload %s %s off" \
|
|
% (self.defaultIntf(), 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
|
|
|
|
|
|
class ONOSBmv2Switch(Switch):
|
|
"""BMv2 software switch with gRPC server"""
|
|
# Shared value used to notify to all instances of this class that a Mininet
|
|
# exception occurred. Mininet exception handling doesn't call the stop()
|
|
# method, so the mn process would hang after clean-up since Bmv2 would still
|
|
# be running.
|
|
mininet_exception = multiprocessing.Value('i', 0)
|
|
|
|
def __init__(self, name, json=None, debugger=False, loglevel="warn",
|
|
elogger=False, grpcport=None, cpuport=255, notifications=False,
|
|
thriftport=None, netcfg=True, dryrun=False, pipeconf="",
|
|
pktdump=False, valgrind=False, gnmi=False,
|
|
portcfg=True, onosdevid=None, **kwargs):
|
|
Switch.__init__(self, name, **kwargs)
|
|
self.grpcPort = grpcport
|
|
self.thriftPort = thriftport
|
|
self.cpuPort = cpuport
|
|
self.json = json
|
|
self.debugger = parseBoolean(debugger)
|
|
self.notifications = parseBoolean(notifications)
|
|
self.loglevel = loglevel
|
|
# 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-%s-log' % self.name
|
|
self.elogger = parseBoolean(elogger)
|
|
self.pktdump = parseBoolean(pktdump)
|
|
self.netcfg = parseBoolean(netcfg)
|
|
self.dryrun = parseBoolean(dryrun)
|
|
self.valgrind = parseBoolean(valgrind)
|
|
self.netcfgfile = '/tmp/bmv2-%s-netcfg.json' % self.name
|
|
self.pipeconfId = pipeconf
|
|
self.injectPorts = parseBoolean(portcfg)
|
|
self.withGnmi = parseBoolean(gnmi)
|
|
self.longitude = kwargs['longitude'] if 'longitude' in kwargs else None
|
|
self.latitude = kwargs['latitude'] if 'latitude' in kwargs else None
|
|
if onosdevid is not None and len(onosdevid) > 0:
|
|
self.onosDeviceId = onosdevid
|
|
else:
|
|
self.onosDeviceId = "device:bmv2:%s" % self.name
|
|
self.logfd = None
|
|
self.bmv2popen = None
|
|
self.stopped = False
|
|
|
|
# Remove files from previous executions
|
|
self.cleanupTmpFiles()
|
|
|
|
def getSourceIp(self, dstIP):
|
|
"""
|
|
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)
|
|
return r.group(1) if r else None
|
|
|
|
def getDeviceConfig(self, srcIP):
|
|
|
|
basicCfg = {
|
|
"driver": "bmv2"
|
|
}
|
|
|
|
if self.longitude and self.latitude:
|
|
basicCfg["longitude"] = self.longitude
|
|
basicCfg["latitude"] = self.latitude
|
|
|
|
cfgData = {
|
|
"generalprovider": {
|
|
"p4runtime": {
|
|
"ip": srcIP,
|
|
"port": self.grpcPort,
|
|
"deviceId": BMV2_DEFAULT_DEVICE_ID,
|
|
"deviceKeyId": "p4runtime:%s" % self.onosDeviceId
|
|
},
|
|
"bmv2-thrift": {
|
|
"ip": srcIP,
|
|
"port": self.thriftPort
|
|
}
|
|
},
|
|
"piPipeconf": {
|
|
"piPipeconfId": self.pipeconfId
|
|
},
|
|
"basic": basicCfg
|
|
}
|
|
|
|
if self.withGnmi:
|
|
cfgData["generalprovider"]["gnmi"] = {
|
|
"ip": srcIP,
|
|
"port": self.grpcPort
|
|
}
|
|
|
|
if self.injectPorts:
|
|
portData = {}
|
|
portId = 1
|
|
for intfName in self.intfNames():
|
|
if intfName == 'lo':
|
|
continue
|
|
portData[str(portId)] = {
|
|
"number": portId,
|
|
"name": intfName,
|
|
"enabled": True,
|
|
"removed": False,
|
|
"type": "copper",
|
|
"speed": 10000
|
|
}
|
|
portId += 1
|
|
|
|
cfgData['ports'] = portData
|
|
|
|
return cfgData
|
|
|
|
def doOnosNetcfg(self, controllerIP):
|
|
"""
|
|
Notifies ONOS about the new device via Netcfg.
|
|
"""
|
|
srcIP = self.getSourceIp(controllerIP)
|
|
if not srcIP:
|
|
warn("*** WARN: unable to get switch IP address, won't do netcfg\n")
|
|
return
|
|
|
|
cfgData = {
|
|
"devices": {
|
|
self.onosDeviceId: self.getDeviceConfig(srcIP)
|
|
}
|
|
}
|
|
with open(self.netcfgfile, 'w') as fp:
|
|
json.dump(cfgData, fp, indent=4)
|
|
|
|
if not self.netcfg:
|
|
# Do not push config to ONOS.
|
|
return
|
|
|
|
# Build netcfg URL
|
|
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)))
|
|
# Push config data to controller
|
|
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)\n" % e.reason)
|
|
|
|
def start(self, controllers):
|
|
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)
|
|
|
|
writeToFile("/tmp/bmv2-%s-grpc-port" % self.name, self.grpcPort)
|
|
writeToFile("/tmp/bmv2-%s-thrift-port" % self.name, self.thriftPort)
|
|
|
|
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:
|
|
ONOSBmv2Switch.mininet_exception = 1
|
|
self.killBmv2()
|
|
self.printBmv2Log()
|
|
raise
|
|
|
|
def grpcTargetArgs(self):
|
|
if self.grpcPort is None:
|
|
self.grpcPort = pickUnusedPort()
|
|
if self.thriftPort is None:
|
|
self.thriftPort = pickUnusedPort()
|
|
args = ['--device-id %s' % str(BMV2_DEFAULT_DEVICE_ID)]
|
|
for port, intf in self.intfs.items():
|
|
if not intf.IP():
|
|
args.append('-i %d@%s' % (port, intf.name))
|
|
args.append('--thrift-port %s' % self.thriftPort)
|
|
if self.notifications:
|
|
ntfaddr = 'ipc:///tmp/bmv2-%s-notifications.ipc' % self.name
|
|
args.append('--notifications-addr %s' % ntfaddr)
|
|
if self.elogger:
|
|
nanologaddr = 'ipc:///tmp/bmv2-%s-nanolog.ipc' % self.name
|
|
args.append('--nanolog %s' % nanologaddr)
|
|
if self.debugger:
|
|
dbgaddr = 'ipc:///tmp/bmv2-%s-debug.ipc' % self.name
|
|
args.append('--debugger-addr %s' % dbgaddr)
|
|
args.append('--log-console')
|
|
if self.pktdump:
|
|
args.append('--pcap --dump-packet-data %s' % PKT_BYTES_TO_DUMP)
|
|
args.append('-L%s' % self.loglevel)
|
|
if not self.json:
|
|
args.append('--no-p4')
|
|
else:
|
|
args.append(self.json)
|
|
# gRPC target-specific options
|
|
args.append('--')
|
|
args.append('--cpu-port %s' % self.cpuPort)
|
|
args.append('--grpc-server-addr 0.0.0.0:%s' % self.grpcPort)
|
|
return args
|
|
|
|
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.1)
|
|
else:
|
|
# Time's up.
|
|
raise Exception("Switch did not start before timeout")
|
|
|
|
def printBmv2Log(self):
|
|
if os.path.isfile(self.logfile):
|
|
print "-" * 80
|
|
print "%s log (from %s):" % (self.name, 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()
|
|
|
|
@staticmethod
|
|
def controllerIp(controllers):
|
|
try:
|
|
# onos.py
|
|
clist = controllers[0].nodes()
|
|
except AttributeError:
|
|
clist = controllers
|
|
assert len(clist) > 0
|
|
return random.choice(clist).IP()
|
|
|
|
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()
|
|
|
|
def cleanupTmpFiles(self):
|
|
self.cmd("rm -f /tmp/bmv2-%s-*" % self.name)
|
|
|
|
def stop(self, deleteIntfs=True):
|
|
"""Terminate switch."""
|
|
self.stopped = True
|
|
self.killBmv2(log=True)
|
|
Switch.stop(self, deleteIntfs)
|
|
|
|
|
|
# Exports for bin/mn
|
|
switches = {'onosbmv2': ONOSBmv2Switch}
|
|
hosts = {'onoshost': ONOSHost}
|