blob: 87ed1ba6d3bf46bd475aa9ae1564199fdc7e6ac3 [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
24
Carmelo Cascone46d360b2017-08-29 20:20:32 +020025
26def parseBoolean(value):
27 if value in ['1', 1, 'true', 'True']:
28 return True
29 else:
30 return False
Carmelo Cascone34433252017-08-25 20:27:18 +020031
32
Carmelo Casconef11513d2018-01-16 00:31:14 -080033def pickUnusedPort():
34 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
35 s.bind(('localhost', 0))
36 addr, port = s.getsockname()
37 s.close()
38 return port
39
40
41def writeToFile(path, value):
42 with open(path, "w") as f:
43 f.write(str(value))
44
45
46def watchDog(sw):
47 while True:
48 if sw.stopped:
49 return
50 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
51 if s.connect_ex(('127.0.0.1', sw.grpcPort)) == 0:
52 time.sleep(1)
53 else:
54 warn("\n*** WARN: BMv2 instance %s (%s) died!\n"
55 % (sw.deviceId, sw.name))
56 sw.printBmv2Log()
57 print ("-" * 80) + "\n"
58 return
59
60
Carmelo Cascone34433252017-08-25 20:27:18 +020061class ONOSHost(Host):
62 def __init__(self, name, inNamespace=True, **params):
63 Host.__init__(self, name, inNamespace=inNamespace, **params)
64
65 def config(self, **params):
66 r = super(Host, self).config(**params)
67 for off in ["rx", "tx", "sg"]:
Carmelo Casconef11513d2018-01-16 00:31:14 -080068 cmd = "/sbin/ethtool --offload %s %s off" \
Carmelo Cascone6ec8f8f2017-11-22 14:27:06 -080069 % (self.defaultIntf(), off)
Carmelo Cascone34433252017-08-25 20:27:18 +020070 self.cmd(cmd)
71 # disable IPv6
72 self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
73 self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1")
74 self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1")
75 return r
Carmelo Casconeb7524272017-06-05 16:53:13 -040076
Carmelo Cascone785fada2016-06-16 18:34:16 -070077
78class ONOSBmv2Switch(Switch):
Carmelo Casconeb7524272017-06-05 16:53:13 -040079 """BMv2 software switch with gRPC server"""
Carmelo Cascone785fada2016-06-16 18:34:16 -070080
Carmelo Cascone785fada2016-06-16 18:34:16 -070081 deviceId = 0
82
Carmelo Casconef11513d2018-01-16 00:31:14 -080083 def __init__(self, name, json=None, debugger=False, loglevel="warn",
84 elogger=False, grpcPort=None, cpuPort=255,
85 thriftPort=None, netcfg=True, dryrun=False, pipeconfId="",
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +010086 pktdump=False, valgrind=False, injectPorts=False, **kwargs):
Carmelo Cascone785fada2016-06-16 18:34:16 -070087 Switch.__init__(self, name, **kwargs)
Carmelo Casconef11513d2018-01-16 00:31:14 -080088 self.grpcPort = pickUnusedPort() if not grpcPort else grpcPort
89 self.thriftPort = pickUnusedPort() if not thriftPort else thriftPort
Carmelo Cascone75e97992017-06-05 02:32:47 -040090 if self.dpid:
91 self.deviceId = int(self.dpid, 0 if 'x' in self.dpid else 16)
Carmelo Cascone977ae3f2016-06-23 19:28:28 -070092 else:
Carmelo Cascone75e97992017-06-05 02:32:47 -040093 self.deviceId = ONOSBmv2Switch.deviceId
94 ONOSBmv2Switch.deviceId += 1
Carmelo Casconef11513d2018-01-16 00:31:14 -080095 self.cpuPort = cpuPort
Carmelo Casconefb76b042017-07-17 19:42:00 -040096 self.json = json
Carmelo Cascone46d360b2017-08-29 20:20:32 +020097 self.debugger = parseBoolean(debugger)
Carmelo Cascone785fada2016-06-16 18:34:16 -070098 self.loglevel = loglevel
Carmelo Casconef11513d2018-01-16 00:31:14 -080099 # Important: Mininet removes all /tmp/*.log files in case of exceptions.
100 # We want to be able to see the bmv2 log if anything goes wrong, hence
101 # avoid the .log extension.
102 self.logfile = '/tmp/bmv2-%d-log' % self.deviceId
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200103 self.elogger = parseBoolean(elogger)
104 self.pktdump = parseBoolean(pktdump)
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200105 self.netcfg = parseBoolean(netcfg)
106 self.dryrun = parseBoolean(dryrun)
107 self.valgrind = parseBoolean(valgrind)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400108 self.netcfgfile = '/tmp/bmv2-%d-netcfg.json' % self.deviceId
Yi Tseng7875cb72017-08-08 10:15:58 -0700109 self.pipeconfId = pipeconfId
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100110 self.injectPorts = parseBoolean(injectPorts)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800111 self.longitude = kwargs['longitude'] if 'longitude' in kwargs else None
112 self.latitude = kwargs['latitude'] if 'latitude' in kwargs else None
Yi Tseng7875cb72017-08-08 10:15:58 -0700113 self.onosDeviceId = "device:bmv2:%d" % self.deviceId
Carmelo Casconef11513d2018-01-16 00:31:14 -0800114 self.logfd = None
115 self.bmv2popen = None
116 self.stopped = False
Yi Tseng7875cb72017-08-08 10:15:58 -0700117
Carmelo Casconef11513d2018-01-16 00:31:14 -0800118 # Remove files from previous executions
119 self.cleanupTmpFiles()
120
121 writeToFile("/tmp/bmv2-%d-grpc-port" % self.deviceId, self.grpcPort)
122 writeToFile("/tmp/bmv2-%d-thrift-port" % self.deviceId, self.thriftPort)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700123
Carmelo Casconeb7524272017-06-05 16:53:13 -0400124 def getSourceIp(self, dstIP):
125 """
Carmelo Casconef11513d2018-01-16 00:31:14 -0800126 Queries the Linux routing table to get the source IP that can talk with
127 dstIP, and vice versa.
Carmelo Casconeb7524272017-06-05 16:53:13 -0400128 """
129 ipRouteOut = self.cmd('ip route get %s' % dstIP)
130 r = re.search(r"src (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", ipRouteOut)
131 return r.group(1) if r else None
132
Yi Tseng7875cb72017-08-08 10:15:58 -0700133 def getDeviceConfig(self, srcIP):
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200134
Yi Tseng7875cb72017-08-08 10:15:58 -0700135 basicCfg = {
136 "driver": "bmv2"
137 }
138
139 if self.longitude and self.latitude:
140 basicCfg["longitude"] = self.longitude
141 basicCfg["latitude"] = self.latitude
142
143 cfgData = {
Carmelo Cascone34433252017-08-25 20:27:18 +0200144 "generalprovider": {
145 "p4runtime": {
146 "ip": srcIP,
147 "port": self.grpcPort,
148 "deviceId": self.deviceId,
149 "deviceKeyId": "p4runtime:%s" % self.onosDeviceId
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100150 },
151 "gnmi": {
152 "ip": srcIP,
153 "port": self.grpcPort
Carmelo Cascone34433252017-08-25 20:27:18 +0200154 }
155 },
156 "piPipeconf": {
157 "piPipeconfId": self.pipeconfId
158 },
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100159 "basic": basicCfg
Yi Tseng7875cb72017-08-08 10:15:58 -0700160 }
161
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100162 if(self.injectPorts):
163 portData = {}
164 portId = 1
165 for intfName in self.intfNames():
166 if intfName == 'lo':
167 continue
168 portData[str(portId)] = {
169 "number": portId,
170 "name": intfName,
171 "enabled": True,
172 "removed": False,
173 "type": "copper",
174 "speed": 10000
175 }
176 portId += 1
177
178 cfgData['ports'] = portData
179
Yi Tseng7875cb72017-08-08 10:15:58 -0700180 return cfgData
181
182 def doOnosNetcfg(self, controllerIP):
183 """
184 Notifies ONOS about the new device via Netcfg.
185 """
186 srcIP = self.getSourceIp(controllerIP)
187 if not srcIP:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800188 warn("*** WARN: unable to get switch IP address, won't do netcfg\n")
Yi Tseng7875cb72017-08-08 10:15:58 -0700189 return
190
Carmelo Casconea11279b2017-06-22 04:30:08 -0400191 cfgData = {
192 "devices": {
Yi Tseng7875cb72017-08-08 10:15:58 -0700193 self.onosDeviceId: self.getDeviceConfig(srcIP)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400194 }
Carmelo Casconea11279b2017-06-22 04:30:08 -0400195 }
Carmelo Casconeb7524272017-06-05 16:53:13 -0400196 with open(self.netcfgfile, 'w') as fp:
197 json.dump(cfgData, fp, indent=4)
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200198
199 if not self.netcfg:
200 # Do not push config to ONOS.
201 return
202
Brian O'Connor71167f92017-06-16 14:55:00 -0700203 # Build netcfg URL
204 url = 'http://%s:8181/onos/v1/network/configuration/' % controllerIP
205 # Instantiate password manager for HTTP auth
206 pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
Carmelo Casconef11513d2018-01-16 00:31:14 -0800207 pm.add_password(None, url,
208 os.environ['ONOS_WEB_USER'],
209 os.environ['ONOS_WEB_PASS'])
210 urllib2.install_opener(urllib2.build_opener(
211 urllib2.HTTPBasicAuthHandler(pm)))
Brian O'Connor71167f92017-06-16 14:55:00 -0700212 # Push config data to controller
Carmelo Casconef11513d2018-01-16 00:31:14 -0800213 req = urllib2.Request(url, json.dumps(cfgData),
214 {'Content-Type': 'application/json'})
Carmelo Casconea11279b2017-06-22 04:30:08 -0400215 try:
216 f = urllib2.urlopen(req)
217 print f.read()
218 f.close()
219 except urllib2.URLError as e:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800220 warn("*** WARN: unable to push config to ONOS (%s)\n" % e.reason)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400221
Carmelo Cascone785fada2016-06-16 18:34:16 -0700222 def start(self, controllers):
Carmelo Casconef11513d2018-01-16 00:31:14 -0800223 bmv2Args = [SIMPLE_SWITCH_GRPC] + self.grpcTargetArgs()
224 if self.valgrind:
225 bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
226
227 cmdString = " ".join(bmv2Args)
228
229 if self.dryrun:
230 info("\n*** DRY RUN (not executing bmv2)")
231
232 info("\nStarting BMv2 target: %s\n" % cmdString)
233
234 try:
235 if not self.dryrun:
236 # Start the switch
237 self.logfd = open(self.logfile, "w")
238 self.bmv2popen = self.popen(cmdString,
239 stdout=self.logfd,
240 stderr=self.logfd)
241 self.waitBmv2Start()
242 # We want to be notified if BMv2 dies...
243 threading.Thread(target=watchDog, args=[self]).start()
244
245 self.doOnosNetcfg(self.controllerIp(controllers))
246 except Exception as ex:
247 self.killBmv2()
248 self.printBmv2Log()
249 raise ex
250
251 def grpcTargetArgs(self):
252 args = ['--device-id %s' % str(self.deviceId)]
Carmelo Cascone785fada2016-06-16 18:34:16 -0700253 for port, intf in self.intfs.items():
254 if not intf.IP():
255 args.append('-i %d@%s' % (port, intf.name))
Carmelo Cascone785fada2016-06-16 18:34:16 -0700256 if self.elogger:
257 nanomsg = 'ipc:///tmp/bmv2-%d-log.ipc' % self.deviceId
258 args.append('--nanolog %s' % nanomsg)
259 if self.debugger:
260 args.append('--debugger')
Carmelo Casconefb76b042017-07-17 19:42:00 -0400261 args.append('--log-console')
Carmelo Cascone34433252017-08-25 20:27:18 +0200262 if self.pktdump:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800263 args.append('--pcap --dump-packet-data %s' % PKT_BYTES_TO_DUMP)
Carmelo Cascone9e6621f2017-06-27 16:06:33 -0400264 args.append('-L%s' % self.loglevel)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800265 args.append('--thrift-port %s' % self.thriftPort)
Carmelo Casconefb76b042017-07-17 19:42:00 -0400266 if not self.json:
267 args.append('--no-p4')
268 else:
269 args.append(self.json)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800270 # gRPC target-specific options
Carmelo Cascone785fada2016-06-16 18:34:16 -0700271 args.append('--')
Carmelo Casconef11513d2018-01-16 00:31:14 -0800272 args.append('--cpu-port %s' % self.cpuPort)
273 args.append('--grpc-server-addr 0.0.0.0:%s' % self.grpcPort)
274 return args
Carmelo Cascone785fada2016-06-16 18:34:16 -0700275
Carmelo Casconef11513d2018-01-16 00:31:14 -0800276 def waitBmv2Start(self):
277 # Wait for switch to open gRPC port, before sending ONOS the netcfg.
278 # Include time-out just in case something hangs.
279 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
280 endtime = time.time() + SWITCH_START_TIMEOUT
281 while True:
282 result = sock.connect_ex(('127.0.0.1', self.grpcPort))
283 if result == 0:
284 # The port is open. Let's go! (Close socket first)
285 sock.close()
286 break
287 # Port is not open yet. If there is time, we wait a bit.
288 if endtime > time.time():
289 time.sleep(0.2)
290 else:
291 # Time's up.
292 raise Exception("Switch did not start before timeout")
Carmelo Casconefb76b042017-07-17 19:42:00 -0400293
Carmelo Casconef11513d2018-01-16 00:31:14 -0800294 def printBmv2Log(self):
295 if os.path.isfile(self.logfile):
296 print "-" * 80
297 print "BMv2 %d log (from %s):" % (self.deviceId, self.logfile)
298 with open(self.logfile, 'r') as f:
299 lines = f.readlines()
300 if len(lines) > BMV2_LOG_LINES:
301 print "..."
302 for line in lines[-BMV2_LOG_LINES:]:
303 print line.rstrip()
Carmelo Casconefb76b042017-07-17 19:42:00 -0400304
Carmelo Casconef11513d2018-01-16 00:31:14 -0800305 @staticmethod
306 def controllerIp(controllers):
307 try:
308 # onos.py
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200309 clist = controllers[0].nodes()
310 except AttributeError:
311 clist = controllers
312 assert len(clist) > 0
Carmelo Casconef11513d2018-01-16 00:31:14 -0800313 return clist[0].IP()
Keesjan Karsten8539f082018-01-04 17:03:31 +0100314
Carmelo Casconef11513d2018-01-16 00:31:14 -0800315 def killBmv2(self, log=False):
316 if self.bmv2popen is not None:
317 self.bmv2popen.kill()
318 if self.logfd is not None:
319 if log:
320 self.logfd.write("*** PROCESS TERMINATED BY MININET ***\n")
321 self.logfd.close()
Keesjan Karsten8539f082018-01-04 17:03:31 +0100322
Carmelo Casconef11513d2018-01-16 00:31:14 -0800323 def cleanupTmpFiles(self):
324 self.cmd("rm -f /tmp/bmv2-%d-*" % self.deviceId)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400325
326 def stop(self, deleteIntfs=True):
327 """Terminate switch."""
Carmelo Casconef11513d2018-01-16 00:31:14 -0800328 self.stopped = True
329 self.killBmv2(log=True)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400330 Switch.stop(self, deleteIntfs)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700331
332
Carmelo Casconeb7524272017-06-05 16:53:13 -0400333# Exports for bin/mn
Carmelo Cascone785fada2016-06-16 18:34:16 -0700334switches = {'onosbmv2': ONOSBmv2Switch}
Carmelo Cascone34433252017-08-25 20:27:18 +0200335hosts = {'onoshost': ONOSHost}