blob: ad3d248dfd3d06f395b2c43e7371a49a5b00b664 [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="",
Carmelo Cascone4f985cd2018-02-11 17:36:42 -080086 pktdump=False, valgrind=False, withGnmi=False,
87 injectPorts=True, **kwargs):
Carmelo Cascone785fada2016-06-16 18:34:16 -070088 Switch.__init__(self, name, **kwargs)
Carmelo Casconef11513d2018-01-16 00:31:14 -080089 self.grpcPort = pickUnusedPort() if not grpcPort else grpcPort
90 self.thriftPort = pickUnusedPort() if not thriftPort else thriftPort
Carmelo Cascone75e97992017-06-05 02:32:47 -040091 if self.dpid:
92 self.deviceId = int(self.dpid, 0 if 'x' in self.dpid else 16)
Carmelo Cascone977ae3f2016-06-23 19:28:28 -070093 else:
Carmelo Cascone75e97992017-06-05 02:32:47 -040094 self.deviceId = ONOSBmv2Switch.deviceId
95 ONOSBmv2Switch.deviceId += 1
Carmelo Casconef11513d2018-01-16 00:31:14 -080096 self.cpuPort = cpuPort
Carmelo Casconefb76b042017-07-17 19:42:00 -040097 self.json = json
Carmelo Cascone46d360b2017-08-29 20:20:32 +020098 self.debugger = parseBoolean(debugger)
Carmelo Cascone785fada2016-06-16 18:34:16 -070099 self.loglevel = loglevel
Carmelo Casconef11513d2018-01-16 00:31:14 -0800100 # Important: Mininet removes all /tmp/*.log files in case of exceptions.
101 # We want to be able to see the bmv2 log if anything goes wrong, hence
102 # avoid the .log extension.
103 self.logfile = '/tmp/bmv2-%d-log' % self.deviceId
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200104 self.elogger = parseBoolean(elogger)
105 self.pktdump = parseBoolean(pktdump)
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200106 self.netcfg = parseBoolean(netcfg)
107 self.dryrun = parseBoolean(dryrun)
108 self.valgrind = parseBoolean(valgrind)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400109 self.netcfgfile = '/tmp/bmv2-%d-netcfg.json' % self.deviceId
Yi Tseng7875cb72017-08-08 10:15:58 -0700110 self.pipeconfId = pipeconfId
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100111 self.injectPorts = parseBoolean(injectPorts)
Carmelo Cascone4f985cd2018-02-11 17:36:42 -0800112 self.withGnmi = parseBoolean(withGnmi)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800113 self.longitude = kwargs['longitude'] if 'longitude' in kwargs else None
114 self.latitude = kwargs['latitude'] if 'latitude' in kwargs else None
Yi Tseng7875cb72017-08-08 10:15:58 -0700115 self.onosDeviceId = "device:bmv2:%d" % self.deviceId
Carmelo Casconef11513d2018-01-16 00:31:14 -0800116 self.logfd = None
117 self.bmv2popen = None
118 self.stopped = False
Yi Tseng7875cb72017-08-08 10:15:58 -0700119
Carmelo Casconef11513d2018-01-16 00:31:14 -0800120 # Remove files from previous executions
121 self.cleanupTmpFiles()
122
123 writeToFile("/tmp/bmv2-%d-grpc-port" % self.deviceId, self.grpcPort)
124 writeToFile("/tmp/bmv2-%d-thrift-port" % self.deviceId, self.thriftPort)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700125
Carmelo Casconeb7524272017-06-05 16:53:13 -0400126 def getSourceIp(self, dstIP):
127 """
Carmelo Casconef11513d2018-01-16 00:31:14 -0800128 Queries the Linux routing table to get the source IP that can talk with
129 dstIP, and vice versa.
Carmelo Casconeb7524272017-06-05 16:53:13 -0400130 """
131 ipRouteOut = self.cmd('ip route get %s' % dstIP)
132 r = re.search(r"src (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", ipRouteOut)
133 return r.group(1) if r else None
134
Yi Tseng7875cb72017-08-08 10:15:58 -0700135 def getDeviceConfig(self, srcIP):
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200136
Yi Tseng7875cb72017-08-08 10:15:58 -0700137 basicCfg = {
138 "driver": "bmv2"
139 }
140
141 if self.longitude and self.latitude:
142 basicCfg["longitude"] = self.longitude
143 basicCfg["latitude"] = self.latitude
144
145 cfgData = {
Carmelo Cascone34433252017-08-25 20:27:18 +0200146 "generalprovider": {
147 "p4runtime": {
148 "ip": srcIP,
149 "port": self.grpcPort,
150 "deviceId": self.deviceId,
151 "deviceKeyId": "p4runtime:%s" % self.onosDeviceId
152 }
153 },
154 "piPipeconf": {
155 "piPipeconfId": self.pipeconfId
156 },
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100157 "basic": basicCfg
Yi Tseng7875cb72017-08-08 10:15:58 -0700158 }
159
Carmelo Cascone4f985cd2018-02-11 17:36:42 -0800160 if self.withGnmi:
161 cfgData["generalprovider"]["gnmi"] = {
162 "ip": srcIP,
163 "port": self.grpcPort
164 }
165
166 if self.injectPorts:
Andrea Campanellabf9e5ce2017-12-06 14:26:36 +0100167 portData = {}
168 portId = 1
169 for intfName in self.intfNames():
170 if intfName == 'lo':
171 continue
172 portData[str(portId)] = {
173 "number": portId,
174 "name": intfName,
175 "enabled": True,
176 "removed": False,
177 "type": "copper",
178 "speed": 10000
179 }
180 portId += 1
181
182 cfgData['ports'] = portData
183
Yi Tseng7875cb72017-08-08 10:15:58 -0700184 return cfgData
185
186 def doOnosNetcfg(self, controllerIP):
187 """
188 Notifies ONOS about the new device via Netcfg.
189 """
190 srcIP = self.getSourceIp(controllerIP)
191 if not srcIP:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800192 warn("*** WARN: unable to get switch IP address, won't do netcfg\n")
Yi Tseng7875cb72017-08-08 10:15:58 -0700193 return
194
Carmelo Casconea11279b2017-06-22 04:30:08 -0400195 cfgData = {
196 "devices": {
Yi Tseng7875cb72017-08-08 10:15:58 -0700197 self.onosDeviceId: self.getDeviceConfig(srcIP)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400198 }
Carmelo Casconea11279b2017-06-22 04:30:08 -0400199 }
Carmelo Casconeb7524272017-06-05 16:53:13 -0400200 with open(self.netcfgfile, 'w') as fp:
201 json.dump(cfgData, fp, indent=4)
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200202
203 if not self.netcfg:
204 # Do not push config to ONOS.
205 return
206
Brian O'Connor71167f92017-06-16 14:55:00 -0700207 # Build netcfg URL
208 url = 'http://%s:8181/onos/v1/network/configuration/' % controllerIP
209 # Instantiate password manager for HTTP auth
210 pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
Carmelo Casconef11513d2018-01-16 00:31:14 -0800211 pm.add_password(None, url,
212 os.environ['ONOS_WEB_USER'],
213 os.environ['ONOS_WEB_PASS'])
214 urllib2.install_opener(urllib2.build_opener(
215 urllib2.HTTPBasicAuthHandler(pm)))
Brian O'Connor71167f92017-06-16 14:55:00 -0700216 # Push config data to controller
Carmelo Casconef11513d2018-01-16 00:31:14 -0800217 req = urllib2.Request(url, json.dumps(cfgData),
218 {'Content-Type': 'application/json'})
Carmelo Casconea11279b2017-06-22 04:30:08 -0400219 try:
220 f = urllib2.urlopen(req)
221 print f.read()
222 f.close()
223 except urllib2.URLError as e:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800224 warn("*** WARN: unable to push config to ONOS (%s)\n" % e.reason)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400225
Carmelo Cascone785fada2016-06-16 18:34:16 -0700226 def start(self, controllers):
Carmelo Casconef11513d2018-01-16 00:31:14 -0800227 bmv2Args = [SIMPLE_SWITCH_GRPC] + self.grpcTargetArgs()
228 if self.valgrind:
229 bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
230
231 cmdString = " ".join(bmv2Args)
232
233 if self.dryrun:
234 info("\n*** DRY RUN (not executing bmv2)")
235
236 info("\nStarting BMv2 target: %s\n" % cmdString)
237
238 try:
239 if not self.dryrun:
240 # Start the switch
241 self.logfd = open(self.logfile, "w")
242 self.bmv2popen = self.popen(cmdString,
243 stdout=self.logfd,
244 stderr=self.logfd)
245 self.waitBmv2Start()
246 # We want to be notified if BMv2 dies...
247 threading.Thread(target=watchDog, args=[self]).start()
248
249 self.doOnosNetcfg(self.controllerIp(controllers))
250 except Exception as ex:
251 self.killBmv2()
252 self.printBmv2Log()
253 raise ex
254
255 def grpcTargetArgs(self):
256 args = ['--device-id %s' % str(self.deviceId)]
Carmelo Cascone785fada2016-06-16 18:34:16 -0700257 for port, intf in self.intfs.items():
258 if not intf.IP():
259 args.append('-i %d@%s' % (port, intf.name))
Carmelo Cascone785fada2016-06-16 18:34:16 -0700260 if self.elogger:
261 nanomsg = 'ipc:///tmp/bmv2-%d-log.ipc' % self.deviceId
262 args.append('--nanolog %s' % nanomsg)
263 if self.debugger:
264 args.append('--debugger')
Carmelo Casconefb76b042017-07-17 19:42:00 -0400265 args.append('--log-console')
Carmelo Cascone34433252017-08-25 20:27:18 +0200266 if self.pktdump:
Carmelo Casconef11513d2018-01-16 00:31:14 -0800267 args.append('--pcap --dump-packet-data %s' % PKT_BYTES_TO_DUMP)
Carmelo Cascone9e6621f2017-06-27 16:06:33 -0400268 args.append('-L%s' % self.loglevel)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800269 args.append('--thrift-port %s' % self.thriftPort)
Carmelo Casconefb76b042017-07-17 19:42:00 -0400270 if not self.json:
271 args.append('--no-p4')
272 else:
273 args.append(self.json)
Carmelo Casconef11513d2018-01-16 00:31:14 -0800274 # gRPC target-specific options
Carmelo Cascone785fada2016-06-16 18:34:16 -0700275 args.append('--')
Carmelo Casconef11513d2018-01-16 00:31:14 -0800276 args.append('--cpu-port %s' % self.cpuPort)
277 args.append('--grpc-server-addr 0.0.0.0:%s' % self.grpcPort)
278 return args
Carmelo Cascone785fada2016-06-16 18:34:16 -0700279
Carmelo Casconef11513d2018-01-16 00:31:14 -0800280 def waitBmv2Start(self):
281 # Wait for switch to open gRPC port, before sending ONOS the netcfg.
282 # Include time-out just in case something hangs.
283 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
284 endtime = time.time() + SWITCH_START_TIMEOUT
285 while True:
286 result = sock.connect_ex(('127.0.0.1', self.grpcPort))
287 if result == 0:
288 # The port is open. Let's go! (Close socket first)
289 sock.close()
290 break
291 # Port is not open yet. If there is time, we wait a bit.
292 if endtime > time.time():
293 time.sleep(0.2)
294 else:
295 # Time's up.
296 raise Exception("Switch did not start before timeout")
Carmelo Casconefb76b042017-07-17 19:42:00 -0400297
Carmelo Casconef11513d2018-01-16 00:31:14 -0800298 def printBmv2Log(self):
299 if os.path.isfile(self.logfile):
300 print "-" * 80
301 print "BMv2 %d log (from %s):" % (self.deviceId, self.logfile)
302 with open(self.logfile, 'r') as f:
303 lines = f.readlines()
304 if len(lines) > BMV2_LOG_LINES:
305 print "..."
306 for line in lines[-BMV2_LOG_LINES:]:
307 print line.rstrip()
Carmelo Casconefb76b042017-07-17 19:42:00 -0400308
Carmelo Casconef11513d2018-01-16 00:31:14 -0800309 @staticmethod
310 def controllerIp(controllers):
311 try:
312 # onos.py
Carmelo Cascone46d360b2017-08-29 20:20:32 +0200313 clist = controllers[0].nodes()
314 except AttributeError:
315 clist = controllers
316 assert len(clist) > 0
Carmelo Casconef11513d2018-01-16 00:31:14 -0800317 return clist[0].IP()
Keesjan Karsten8539f082018-01-04 17:03:31 +0100318
Carmelo Casconef11513d2018-01-16 00:31:14 -0800319 def killBmv2(self, log=False):
320 if self.bmv2popen is not None:
321 self.bmv2popen.kill()
322 if self.logfd is not None:
323 if log:
324 self.logfd.write("*** PROCESS TERMINATED BY MININET ***\n")
325 self.logfd.close()
Keesjan Karsten8539f082018-01-04 17:03:31 +0100326
Carmelo Casconef11513d2018-01-16 00:31:14 -0800327 def cleanupTmpFiles(self):
328 self.cmd("rm -f /tmp/bmv2-%d-*" % self.deviceId)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400329
330 def stop(self, deleteIntfs=True):
331 """Terminate switch."""
Carmelo Casconef11513d2018-01-16 00:31:14 -0800332 self.stopped = True
333 self.killBmv2(log=True)
Carmelo Casconeb7524272017-06-05 16:53:13 -0400334 Switch.stop(self, deleteIntfs)
Carmelo Cascone785fada2016-06-16 18:34:16 -0700335
336
Carmelo Casconeb7524272017-06-05 16:53:13 -0400337# Exports for bin/mn
Carmelo Cascone785fada2016-06-16 18:34:16 -0700338switches = {'onosbmv2': ONOSBmv2Switch}
Carmelo Cascone34433252017-08-25 20:27:18 +0200339hosts = {'onoshost': ONOSHost}