blob: eafb482b155a0ea7a987820a45ec044782ed4010 [file] [log] [blame]
You Wang68568b12019-03-04 11:49:57 -08001import json
2import multiprocessing
3import os
4import random
5import re
6import socket
7import threading
8import urllib2
9from contextlib import closing
10
11import time
12from mininet.log import info, warn
13from mininet.node import Switch, Host
14
15SIMPLE_SWITCH_GRPC = 'simple_switch_grpc'
16PKT_BYTES_TO_DUMP = 80
17VALGRIND_PREFIX = 'valgrind --leak-check=yes'
18SWITCH_START_TIMEOUT = 5 # seconds
19BMV2_LOG_LINES = 5
20BMV2_DEFAULT_DEVICE_ID = 1
21
22
23def parseBoolean(value):
24 if value in ['1', 1, 'true', 'True']:
25 return True
26 else:
27 return False
28
29
30def pickUnusedPort():
31 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
32 s.bind(('localhost', 0))
33 addr, port = s.getsockname()
34 s.close()
35 return port
36
37
38def writeToFile(path, value):
39 with open(path, "w") as f:
40 f.write(str(value))
41
42
43def watchDog(sw):
44 while True:
45 if ONOSBmv2Switch.mininet_exception == 1:
46 sw.killBmv2(log=False)
47 return
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 died!\n" % sw.name)
55 sw.printBmv2Log()
56 print ("-" * 80) + "\n"
57 return
58
59
60class ONOSHost(Host):
61 def __init__(self, name, inNamespace=True, **params):
62 Host.__init__(self, name, inNamespace=inNamespace, **params)
63
64 def config(self, **params):
65 r = super(Host, self).config(**params)
66 for off in ["rx", "tx", "sg"]:
67 cmd = "/sbin/ethtool --offload %s %s off" \
68 % (self.defaultIntf(), off)
69 self.cmd(cmd)
70 # disable IPv6
71 self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
72 self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1")
73 self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1")
74 return r
75
76
77class ONOSBmv2Switch(Switch):
78 """BMv2 software switch with gRPC server"""
79 # Shared value used to notify to all instances of this class that a Mininet
80 # exception occurred. Mininet exception handling doesn't call the stop()
81 # method, so the mn process would hang after clean-up since Bmv2 would still
82 # be running.
83 mininet_exception = multiprocessing.Value('i', 0)
84
85 def __init__(self, name, json=None, debugger=False, loglevel="warn",
86 elogger=False, grpcport=None, cpuport=255, notifications=False,
87 thriftport=None, netcfg=True, dryrun=False, pipeconf="",
88 pktdump=False, valgrind=False, gnmi=False,
89 portcfg=True, onosdevid=None, **kwargs):
90 Switch.__init__(self, name, **kwargs)
91 self.grpcPort = grpcport
92 self.thriftPort = thriftport
93 self.cpuPort = cpuport
94 self.json = json
95 self.debugger = parseBoolean(debugger)
96 self.notifications = parseBoolean(notifications)
97 self.loglevel = loglevel
98 # Important: Mininet removes all /tmp/*.log files in case of exceptions.
99 # We want to be able to see the bmv2 log if anything goes wrong, hence
100 # avoid the .log extension.
101 self.logfile = '/tmp/bmv2-%s-log' % self.name
102 self.elogger = parseBoolean(elogger)
103 self.pktdump = parseBoolean(pktdump)
104 self.netcfg = parseBoolean(netcfg)
105 self.dryrun = parseBoolean(dryrun)
106 self.valgrind = parseBoolean(valgrind)
107 self.netcfgfile = '/tmp/bmv2-%s-netcfg.json' % self.name
108 self.pipeconfId = pipeconf
109 self.injectPorts = parseBoolean(portcfg)
110 self.withGnmi = parseBoolean(gnmi)
111 self.longitude = kwargs['longitude'] if 'longitude' in kwargs else None
112 self.latitude = kwargs['latitude'] if 'latitude' in kwargs else None
113 if onosdevid is not None and len(onosdevid) > 0:
114 self.onosDeviceId = onosdevid
115 else:
116 self.onosDeviceId = "device:bmv2:%s" % self.name
117 self.logfd = None
118 self.bmv2popen = None
119 self.stopped = False
120
121 # Remove files from previous executions
122 self.cleanupTmpFiles()
123
124 def getSourceIp(self, dstIP):
125 """
126 Queries the Linux routing table to get the source IP that can talk with
127 dstIP, and vice versa.
128 """
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
133 def getDeviceConfig(self, srcIP):
134
135 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 = {
144 "generalprovider": {
145 "p4runtime": {
146 "ip": srcIP,
147 "port": self.grpcPort,
148 "deviceId": BMV2_DEFAULT_DEVICE_ID,
149 "deviceKeyId": "p4runtime:%s" % self.onosDeviceId
150 },
151 "bmv2-thrift": {
152 "ip": srcIP,
153 "port": self.thriftPort
154 }
155 },
156 "piPipeconf": {
157 "piPipeconfId": self.pipeconfId
158 },
159 "basic": basicCfg
160 }
161
162 if self.withGnmi:
163 cfgData["generalprovider"]["gnmi"] = {
164 "ip": srcIP,
165 "port": self.grpcPort
166 }
167
168 if self.injectPorts:
169 portData = {}
170 portId = 1
171 for intfName in self.intfNames():
172 if intfName == 'lo':
173 continue
174 portData[str(portId)] = {
175 "number": portId,
176 "name": intfName,
177 "enabled": True,
178 "removed": False,
179 "type": "copper",
180 "speed": 10000
181 }
182 portId += 1
183
184 cfgData['ports'] = portData
185
186 return cfgData
187
188 def doOnosNetcfg(self, controllerIP):
189 """
190 Notifies ONOS about the new device via Netcfg.
191 """
192 srcIP = self.getSourceIp(controllerIP)
193 if not srcIP:
194 warn("*** WARN: unable to get switch IP address, won't do netcfg\n")
195 return
196
197 cfgData = {
198 "devices": {
199 self.onosDeviceId: self.getDeviceConfig(srcIP)
200 }
201 }
202 with open(self.netcfgfile, 'w') as fp:
203 json.dump(cfgData, fp, indent=4)
204
205 if not self.netcfg:
206 # Do not push config to ONOS.
207 return
208
209 # Build netcfg URL
210 url = 'http://%s:8181/onos/v1/network/configuration/' % controllerIP
211 # Instantiate password manager for HTTP auth
212 pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
213 pm.add_password(None, url,
214 os.environ['ONOS_WEB_USER'],
215 os.environ['ONOS_WEB_PASS'])
216 urllib2.install_opener(urllib2.build_opener(
217 urllib2.HTTPBasicAuthHandler(pm)))
218 # Push config data to controller
219 req = urllib2.Request(url, json.dumps(cfgData),
220 {'Content-Type': 'application/json'})
221 try:
222 f = urllib2.urlopen(req)
223 print f.read()
224 f.close()
225 except urllib2.URLError as e:
226 warn("*** WARN: unable to push config to ONOS (%s)\n" % e.reason)
227
228 def start(self, controllers):
229 bmv2Args = [SIMPLE_SWITCH_GRPC] + self.grpcTargetArgs()
230 if self.valgrind:
231 bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
232
233 cmdString = " ".join(bmv2Args)
234
235 if self.dryrun:
236 info("\n*** DRY RUN (not executing bmv2)")
237
238 info("\nStarting BMv2 target: %s\n" % cmdString)
239
240 writeToFile("/tmp/bmv2-%s-grpc-port" % self.name, self.grpcPort)
241 writeToFile("/tmp/bmv2-%s-thrift-port" % self.name, self.thriftPort)
242
243 try:
244 if not self.dryrun:
245 # Start the switch
246 self.logfd = open(self.logfile, "w")
247 self.bmv2popen = self.popen(cmdString,
248 stdout=self.logfd,
249 stderr=self.logfd)
250 self.waitBmv2Start()
251 # We want to be notified if BMv2 dies...
252 threading.Thread(target=watchDog, args=[self]).start()
253
254 self.doOnosNetcfg(self.controllerIp(controllers))
255 except Exception:
256 ONOSBmv2Switch.mininet_exception = 1
257 self.killBmv2()
258 self.printBmv2Log()
259 raise
260
261 def grpcTargetArgs(self):
262 if self.grpcPort is None:
263 self.grpcPort = pickUnusedPort()
264 if self.thriftPort is None:
265 self.thriftPort = pickUnusedPort()
266 args = ['--device-id %s' % str(BMV2_DEFAULT_DEVICE_ID)]
267 for port, intf in self.intfs.items():
268 if not intf.IP():
269 args.append('-i %d@%s' % (port, intf.name))
270 args.append('--thrift-port %s' % self.thriftPort)
271 if self.notifications:
272 ntfaddr = 'ipc:///tmp/bmv2-%s-notifications.ipc' % self.name
273 args.append('--notifications-addr %s' % ntfaddr)
274 if self.elogger:
275 nanologaddr = 'ipc:///tmp/bmv2-%s-nanolog.ipc' % self.name
276 args.append('--nanolog %s' % nanologaddr)
277 if self.debugger:
278 dbgaddr = 'ipc:///tmp/bmv2-%s-debug.ipc' % self.name
279 args.append('--debugger-addr %s' % dbgaddr)
280 args.append('--log-console')
281 if self.pktdump:
282 args.append('--pcap --dump-packet-data %s' % PKT_BYTES_TO_DUMP)
283 args.append('-L%s' % self.loglevel)
284 if not self.json:
285 args.append('--no-p4')
286 else:
287 args.append(self.json)
288 # gRPC target-specific options
289 args.append('--')
290 args.append('--cpu-port %s' % self.cpuPort)
291 args.append('--grpc-server-addr 0.0.0.0:%s' % self.grpcPort)
292 return args
293
294 def waitBmv2Start(self):
295 # Wait for switch to open gRPC port, before sending ONOS the netcfg.
296 # Include time-out just in case something hangs.
297 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
298 endtime = time.time() + SWITCH_START_TIMEOUT
299 while True:
300 result = sock.connect_ex(('127.0.0.1', self.grpcPort))
301 if result == 0:
302 # The port is open. Let's go! (Close socket first)
303 sock.close()
304 break
305 # Port is not open yet. If there is time, we wait a bit.
306 if endtime > time.time():
307 time.sleep(0.1)
308 else:
309 # Time's up.
310 raise Exception("Switch did not start before timeout")
311
312 def printBmv2Log(self):
313 if os.path.isfile(self.logfile):
314 print "-" * 80
315 print "%s log (from %s):" % (self.name, self.logfile)
316 with open(self.logfile, 'r') as f:
317 lines = f.readlines()
318 if len(lines) > BMV2_LOG_LINES:
319 print "..."
320 for line in lines[-BMV2_LOG_LINES:]:
321 print line.rstrip()
322
323 @staticmethod
324 def controllerIp(controllers):
325 try:
326 # onos.py
327 clist = controllers[0].nodes()
328 except AttributeError:
329 clist = controllers
330 assert len(clist) > 0
331 return random.choice(clist).IP()
332
333 def killBmv2(self, log=False):
334 if self.bmv2popen is not None:
335 self.bmv2popen.kill()
336 if self.logfd is not None:
337 if log:
338 self.logfd.write("*** PROCESS TERMINATED BY MININET ***\n")
339 self.logfd.close()
340
341 def cleanupTmpFiles(self):
342 self.cmd("rm -f /tmp/bmv2-%s-*" % self.name)
343
344 def stop(self, deleteIntfs=True):
345 """Terminate switch."""
346 self.stopped = True
347 self.killBmv2(log=True)
348 Switch.stop(self, deleteIntfs)
349
350
351# Exports for bin/mn
352switches = {'onosbmv2': ONOSBmv2Switch}
353hosts = {'onoshost': ONOSHost}