blob: eadb21b2bd3a7bf5dbfd5a3ca7da2381288ff3a7 [file] [log] [blame]
Carmelo Casconeb7524272017-06-05 16:53:13 -04001import os
Carmelo Cascone977ae3f2016-06-23 19:28:28 -07002import socket
Carmelo Casconeb7524272017-06-05 16:53:13 -04003import re
4import json
Carmelo Casconef11513d2018-01-16 00:31:14 -08005import threading
Brian O'Connor71167f92017-06-16 14:55:00 -07006import urllib2
Carmelo Cascone977ae3f2016-06-23 19:28:28 -07007
Carmelo Cascone46d360b2017-08-29 20:20:32 +02008import time
Carmelo Casconef11513d2018-01-16 00:31:14 -08009from contextlib import closing
Carmelo Cascone46d360b2017-08-29 20:20:32 +020010
Carmelo Casconeb7524272017-06-05 16:53:13 -040011from mininet.log import info, warn, error
Carmelo Cascone34433252017-08-25 20:27:18 +020012from mininet.node import Switch, Host
Carmelo Cascone785fada2016-06-16 18:34:16 -070013
Carmelo Casconeb7524272017-06-05 16:53:13 -040014if 'ONOS_ROOT' not in os.environ:
15 error("ERROR: environment var $ONOS_ROOT not set")
16 exit()
17
Carmelo Casconef11513d2018-01-16 00:31:14 -080018SIMPLE_SWITCH_GRPC = 'simple_switch_grpc'
Carmelo Casconeb7524272017-06-05 16:53:13 -040019ONOS_ROOT = os.environ["ONOS_ROOT"]
Carmelo Cascone34433252017-08-25 20:27:18 +020020PKT_BYTES_TO_DUMP = 80
Carmelo Cascone46d360b2017-08-29 20:20:32 +020021VALGRIND_PREFIX = 'valgrind --leak-check=yes'
Carmelo Casconef11513d2018-01-16 00:31:14 -080022SWITCH_START_TIMEOUT = 5 # seconds
23BMV2_LOG_LINES = 5
Carmelo Casconec2821332018-05-14 18:15:33 -070024BMV2_DEFAULT_DEVICE_ID = 0
Carmelo Casconef11513d2018-01-16 00:31:14 -080025
Carmelo Cascone46d360b2017-08-29 20:20:32 +020026
27def parseBoolean(value):
28 if value in ['1', 1, 'true', 'True']:
29 return True
30 else:
31 return False
Carmelo Cascone34433252017-08-25 20:27:18 +020032
33
Carmelo Casconef11513d2018-01-16 00:31:14 -080034def pickUnusedPort():
35 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36 s.bind(('localhost', 0))
37 addr, port = s.getsockname()
38 s.close()
39 return port
40
41
42def writeToFile(path, value):
43 with open(path, "w") as f:
44 f.write(str(value))
45
46
47def watchDog(sw):
48 while True:
49 if sw.stopped:
50 return
51 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
52 if s.connect_ex(('127.0.0.1', sw.grpcPort)) == 0:
53 time.sleep(1)
54 else:
55 warn("\n*** WARN: BMv2 instance %s (%s) died!\n"
56 % (sw.deviceId, sw.name))
57 sw.printBmv2Log()
58 print ("-" * 80) + "\n"
59 return
60
61
Carmelo Cascone34433252017-08-25 20:27:18 +020062class ONOSHost(Host):
63 def __init__(self, name, inNamespace=True, **params):
64 Host.__init__(self, name, inNamespace=inNamespace, **params)
65
66 def config(self, **params):
67 r = super(Host, self).config(**params)
68 for off in ["rx", "tx", "sg"]:
Carmelo Casconef11513d2018-01-16 00:31:14 -080069 cmd = "/sbin/ethtool --offload %s %s off" \
Carmelo Cascone6ec8f8f2017-11-22 14:27:06 -080070 % (self.defaultIntf(), off)
Carmelo Cascone34433252017-08-25 20:27:18 +020071 self.cmd(cmd)
72 # disable IPv6
73 self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
74 self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1")
75 self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1")
76 return r
Carmelo Casconeb7524272017-06-05 16:53:13 -040077
Carmelo Cascone785fada2016-06-16 18:34:16 -070078
79class ONOSBmv2Switch(Switch):
Carmelo Casconeb7524272017-06-05 16:53:13 -040080 """BMv2 software switch with gRPC server"""
Carmelo Cascone785fada2016-06-16 18:34:16 -070081
Carmelo Casconef11513d2018-01-16 00:31:14 -080082 def __init__(self, name, json=None, debugger=False, loglevel="warn",
Carmelo Casconeeaa8b1d2018-04-11 14:12:17 -070083 elogger=False, grpcport=None, cpuport=255,
84 thriftport=None, netcfg=True, dryrun=False, pipeconf="",
85 pktdump=False, valgrind=False, gnmi=False,
86 portcfg=True, **kwargs):
Carmelo Cascone785fada2016-06-16 18:34:16 -070087 Switch.__init__(self, name, **kwargs)
Carmelo Casconeeaa8b1d2018-04-11 14:12:17 -070088 self.grpcPort = pickUnusedPort() if not grpcport else grpcport
89 self.thriftPort = pickUnusedPort() if not thriftport else thriftport
Carmelo Casconeeaa8b1d2018-04-11 14:12:17 -070090 self.cpuPort = cpuport
Carmelo Casconefb76b042017-07-17 19:42:00 -040091 self.json = json
Carmelo Cascone46d360b2017-08-29 20:20:32 +020092 self.debugger = parseBoolean(debugger)
Carmelo Cascone785fada2016-06-16 18:34:16 -070093 self.loglevel = loglevel
Carmelo Casconef11513d2018-01-16 00:31:14 -080094 # Important: Mininet removes all /tmp/*.log files in case of exceptions.
95 # We want to be able to see the bmv2 log if anything goes wrong, hence
96 # avoid the .log extension.
Carmelo Casconec2821332018-05-14 18:15:33 -070097 self.logfile = '/tmp/bmv2-%s-log' % self.name
Carmelo Cascone46d360b2017-08-29 20:20:32 +020098 self.elogger = parseBoolean(elogger)
99 self.pktdump = parseBoolean(pktdump)
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200100 self.netcfg = parseBoolean(netcfg)
101 self.dryrun = parseBoolean(dryrun)
102 self.valgrind = parseBoolean(valgrind)
Carmelo Casconec2821332018-05-14 18:15:33 -0700103 self.netcfgfile = '/tmp/bmv2-%s-netcfg.json' % self.name
Carmelo Casconeeaa8b1d2018-04-11 14:12:17 -0700104 self.pipeconfId = pipeconf
105 self.injectPorts = parseBoolean(portcfg)
106 self.withGnmi = parseBoolean(gnmi)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800107 self.longitude = kwargs['longitude'] if 'longitude' in kwargs else None
108 self.latitude = kwargs['latitude'] if 'latitude' in kwargs else None
Carmelo Casconec2821332018-05-14 18:15:33 -0700109 self.onosDeviceId = "device:bmv2:%s" % self.name
Carmelo Casconef11513d2018-01-16 00:31:14 -0800110 self.logfd = None
111 self.bmv2popen = None
112 self.stopped = False
Yi Tseng7875cb72017-08-08 10:15:58 -0700113
Carmelo Casconef11513d2018-01-16 00:31:14 -0800114 # Remove files from previous executions
115 self.cleanupTmpFiles()
116
Carmelo Casconec2821332018-05-14 18:15:33 -0700117 writeToFile("/tmp/bmv2-%s-grpc-port" % self.name, self.grpcPort)
118 writeToFile("/tmp/bmv2-%s-thrift-port" % self.name, self.thriftPort)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700119
Carmelo Casconeb7524272017-06-05 16:53:13 -0400120 def getSourceIp(self, dstIP):
121 """
Carmelo Casconef11513d2018-01-16 00:31:14 -0800122 Queries the Linux routing table to get the source IP that can talk with
123 dstIP, and vice versa.
Carmelo Casconeb7524272017-06-05 16:53:13 -0400124 """
125 ipRouteOut = self.cmd('ip route get %s' % dstIP)
126 r = re.search(r"src (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", ipRouteOut)
127 return r.group(1) if r else None
128
Yi Tseng7875cb72017-08-08 10:15:58 -0700129 def getDeviceConfig(self, srcIP):
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200130
Yi Tseng7875cb72017-08-08 10:15:58 -0700131 basicCfg = {
132 "driver": "bmv2"
133 }
134
135 if self.longitude and self.latitude:
136 basicCfg["longitude"] = self.longitude
137 basicCfg["latitude"] = self.latitude
138
139 cfgData = {
Carmelo Cascone34433252017-08-25 20:27:18 +0200140 "generalprovider": {
141 "p4runtime": {
142 "ip": srcIP,
143 "port": self.grpcPort,
Carmelo Casconec2821332018-05-14 18:15:33 -0700144 "deviceId": BMV2_DEFAULT_DEVICE_ID,
Carmelo Cascone34433252017-08-25 20:27:18 +0200145 "deviceKeyId": "p4runtime:%s" % self.onosDeviceId
146 }
147 },
148 "piPipeconf": {
149 "piPipeconfId": self.pipeconfId
150 },
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100151 "basic": basicCfg
Yi Tseng7875cb72017-08-08 10:15:58 -0700152 }
153
Carmelo Cascone4f985cd2018-02-11 17:36:42 -0800154 if self.withGnmi:
155 cfgData["generalprovider"]["gnmi"] = {
156 "ip": srcIP,
157 "port": self.grpcPort
158 }
159
160 if self.injectPorts:
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100161 portData = {}
162 portId = 1
163 for intfName in self.intfNames():
164 if intfName == 'lo':
165 continue
166 portData[str(portId)] = {
167 "number": portId,
168 "name": intfName,
169 "enabled": True,
170 "removed": False,
171 "type": "copper",
172 "speed": 10000
173 }
174 portId += 1
175
176 cfgData['ports'] = portData
177
Yi Tseng7875cb72017-08-08 10:15:58 -0700178 return cfgData
179
180 def doOnosNetcfg(self, controllerIP):
181 """
182 Notifies ONOS about the new device via Netcfg.
183 """
184 srcIP = self.getSourceIp(controllerIP)
185 if not srcIP:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800186 warn("*** WARN: unable to get switch IP address, won't do netcfg\n")
Yi Tseng7875cb72017-08-08 10:15:58 -0700187 return
188
Carmelo Casconea11279b2017-06-22 04:30:08 -0400189 cfgData = {
190 "devices": {
Yi Tseng7875cb72017-08-08 10:15:58 -0700191 self.onosDeviceId: self.getDeviceConfig(srcIP)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400192 }
Carmelo Casconea11279b2017-06-22 04:30:08 -0400193 }
Carmelo Casconeb7524272017-06-05 16:53:13 -0400194 with open(self.netcfgfile, 'w') as fp:
195 json.dump(cfgData, fp, indent=4)
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200196
197 if not self.netcfg:
198 # Do not push config to ONOS.
199 return
200
Brian O'Connor71167f92017-06-16 14:55:00 -0700201 # Build netcfg URL
202 url = 'http://%s:8181/onos/v1/network/configuration/' % controllerIP
203 # Instantiate password manager for HTTP auth
204 pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
Carmelo Casconef11513d2018-01-16 00:31:14 -0800205 pm.add_password(None, url,
206 os.environ['ONOS_WEB_USER'],
207 os.environ['ONOS_WEB_PASS'])
208 urllib2.install_opener(urllib2.build_opener(
209 urllib2.HTTPBasicAuthHandler(pm)))
Brian O'Connor71167f92017-06-16 14:55:00 -0700210 # Push config data to controller
Carmelo Casconef11513d2018-01-16 00:31:14 -0800211 req = urllib2.Request(url, json.dumps(cfgData),
212 {'Content-Type': 'application/json'})
Carmelo Casconea11279b2017-06-22 04:30:08 -0400213 try:
214 f = urllib2.urlopen(req)
215 print f.read()
216 f.close()
217 except urllib2.URLError as e:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800218 warn("*** WARN: unable to push config to ONOS (%s)\n" % e.reason)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400219
Carmelo Cascone785fada2016-06-16 18:34:16 -0700220 def start(self, controllers):
Carmelo Casconef11513d2018-01-16 00:31:14 -0800221 bmv2Args = [SIMPLE_SWITCH_GRPC] + self.grpcTargetArgs()
222 if self.valgrind:
223 bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
224
225 cmdString = " ".join(bmv2Args)
226
227 if self.dryrun:
228 info("\n*** DRY RUN (not executing bmv2)")
229
230 info("\nStarting BMv2 target: %s\n" % cmdString)
231
232 try:
233 if not self.dryrun:
234 # Start the switch
235 self.logfd = open(self.logfile, "w")
236 self.bmv2popen = self.popen(cmdString,
237 stdout=self.logfd,
238 stderr=self.logfd)
239 self.waitBmv2Start()
240 # We want to be notified if BMv2 dies...
241 threading.Thread(target=watchDog, args=[self]).start()
242
243 self.doOnosNetcfg(self.controllerIp(controllers))
244 except Exception as ex:
245 self.killBmv2()
246 self.printBmv2Log()
247 raise ex
248
249 def grpcTargetArgs(self):
Carmelo Casconec2821332018-05-14 18:15:33 -0700250 args = ['--device-id %s' % str(BMV2_DEFAULT_DEVICE_ID)]
Carmelo Cascone785fada2016-06-16 18:34:16 -0700251 for port, intf in self.intfs.items():
252 if not intf.IP():
253 args.append('-i %d@%s' % (port, intf.name))
Carmelo Casconec2821332018-05-14 18:15:33 -0700254 args.append('--thrift-port %s' % self.thriftPort)
255 ntfaddr = 'ipc:///tmp/bmv2-%s-notifications.ipc' % self.name
256 args.append('--notifications-addr %s' % ntfaddr)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700257 if self.elogger:
Carmelo Casconec2821332018-05-14 18:15:33 -0700258 nanologaddr = 'ipc:///tmp/bmv2-%s-nanolog.ipc' % self.name
259 args.append('--nanolog %s' % nanologaddr)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700260 if self.debugger:
Carmelo Casconec2821332018-05-14 18:15:33 -0700261 dbgaddr = 'ipc:///tmp/bmv2-%s-debug.ipc' % self.name
262 args.append('--debugger-addr %s' % dbgaddr)
Carmelo Casconefb76b042017-07-17 19:42:00 -0400263 args.append('--log-console')
Carmelo Cascone34433252017-08-25 20:27:18 +0200264 if self.pktdump:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800265 args.append('--pcap --dump-packet-data %s' % PKT_BYTES_TO_DUMP)
Carmelo Cascone9e6621f2017-06-27 16:06:33 -0400266 args.append('-L%s' % self.loglevel)
Carmelo Casconefb76b042017-07-17 19:42:00 -0400267 if not self.json:
268 args.append('--no-p4')
269 else:
270 args.append(self.json)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800271 # gRPC target-specific options
Carmelo Cascone785fada2016-06-16 18:34:16 -0700272 args.append('--')
Carmelo Casconef11513d2018-01-16 00:31:14 -0800273 args.append('--cpu-port %s' % self.cpuPort)
274 args.append('--grpc-server-addr 0.0.0.0:%s' % self.grpcPort)
275 return args
Carmelo Cascone785fada2016-06-16 18:34:16 -0700276
Carmelo Casconef11513d2018-01-16 00:31:14 -0800277 def waitBmv2Start(self):
278 # Wait for switch to open gRPC port, before sending ONOS the netcfg.
279 # Include time-out just in case something hangs.
280 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
281 endtime = time.time() + SWITCH_START_TIMEOUT
282 while True:
283 result = sock.connect_ex(('127.0.0.1', self.grpcPort))
284 if result == 0:
285 # The port is open. Let's go! (Close socket first)
286 sock.close()
287 break
288 # Port is not open yet. If there is time, we wait a bit.
289 if endtime > time.time():
290 time.sleep(0.2)
291 else:
292 # Time's up.
293 raise Exception("Switch did not start before timeout")
Carmelo Casconefb76b042017-07-17 19:42:00 -0400294
Carmelo Casconef11513d2018-01-16 00:31:14 -0800295 def printBmv2Log(self):
296 if os.path.isfile(self.logfile):
297 print "-" * 80
Carmelo Casconec2821332018-05-14 18:15:33 -0700298 print "%s log (from %s):" % (self.name, self.logfile)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800299 with open(self.logfile, 'r') as f:
300 lines = f.readlines()
301 if len(lines) > BMV2_LOG_LINES:
302 print "..."
303 for line in lines[-BMV2_LOG_LINES:]:
304 print line.rstrip()
Carmelo Casconefb76b042017-07-17 19:42:00 -0400305
Carmelo Casconef11513d2018-01-16 00:31:14 -0800306 @staticmethod
307 def controllerIp(controllers):
308 try:
309 # onos.py
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200310 clist = controllers[0].nodes()
311 except AttributeError:
312 clist = controllers
313 assert len(clist) > 0
Carmelo Casconef11513d2018-01-16 00:31:14 -0800314 return clist[0].IP()
Keesjan Karsten8539f082018-01-04 17:03:31 +0100315
Carmelo Casconef11513d2018-01-16 00:31:14 -0800316 def killBmv2(self, log=False):
317 if self.bmv2popen is not None:
318 self.bmv2popen.kill()
319 if self.logfd is not None:
320 if log:
321 self.logfd.write("*** PROCESS TERMINATED BY MININET ***\n")
322 self.logfd.close()
Keesjan Karsten8539f082018-01-04 17:03:31 +0100323
Carmelo Casconef11513d2018-01-16 00:31:14 -0800324 def cleanupTmpFiles(self):
Carmelo Casconec2821332018-05-14 18:15:33 -0700325 self.cmd("rm -f /tmp/bmv2-%s-*" % self.name)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400326
327 def stop(self, deleteIntfs=True):
328 """Terminate switch."""
Carmelo Casconef11513d2018-01-16 00:31:14 -0800329 self.stopped = True
330 self.killBmv2(log=True)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400331 Switch.stop(self, deleteIntfs)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700332
333
Carmelo Casconeb7524272017-06-05 16:53:13 -0400334# Exports for bin/mn
Carmelo Cascone785fada2016-06-16 18:34:16 -0700335switches = {'onosbmv2': ONOSBmv2Switch}
Carmelo Cascone34433252017-08-25 20:27:18 +0200336hosts = {'onoshost': ONOSHost}