blob: 8650016aa5298ecbc7d572664ca198471b113ce7 [file] [log] [blame]
Jonathan Hartce97e5b2016-04-19 01:41:31 -07001#!/usr/bin/python
2
3"""
4Libraries for creating L3 topologies with routing protocols.
5"""
6
7from mininet.node import Host, OVSBridge
8from mininet.nodelib import NAT
9from mininet.log import info, debug, error
10from mininet.cli import CLI
11from ipaddress import ip_network, ip_address, ip_interface
12
13class RoutedHost(Host):
14 """Host that can be configured with multiple IP addresses."""
15 def __init__(self, name, ips, gateway, *args, **kwargs):
16 super(RoutedHost, self).__init__(name, *args, **kwargs)
17
18 self.ips = ips
19 self.gateway = gateway
20
21 def config(self, **kwargs):
22 Host.config(self, **kwargs)
23
24 self.cmd('ip addr flush dev %s' % self.defaultIntf())
25 for ip in self.ips:
26 self.cmd('ip addr add %s dev %s' % (ip, self.defaultIntf()))
27
28 self.cmd('ip route add default via %s' % self.gateway)
29
30class Router(Host):
31
32 """An L3 router.
33 Configures the Linux kernel for L3 forwarding and supports rich interface
34 configuration of IP addresses, MAC addresses and VLANs."""
35
36 def __init__(self, name, interfaces, *args, **kwargs):
37 super(Router, self).__init__(name, **kwargs)
38
39 self.interfaces = interfaces
40
41 def config(self, **kwargs):
42 super(Host, self).config(**kwargs)
43
44 self.cmd('sysctl net.ipv4.ip_forward=1')
45 self.cmd('sysctl net.ipv4.conf.all.rp_filter=0')
46
47 for intf, configs in self.interfaces.items():
48 self.cmd('ip addr flush dev %s' % intf)
49 self.cmd( 'sysctl net.ipv4.conf.%s.rp_filter=0' % intf )
50
51 if not isinstance(configs, list):
52 configs = [configs]
53
54 for attrs in configs:
55 # Configure the vlan if there is one
56 if 'vlan' in attrs:
57 vlanName = '%s.%s' % (intf, attrs['vlan'])
58 self.cmd('ip link add link %s name %s type vlan id %s' %
59 (intf, vlanName, attrs['vlan']))
60 self.cmd('ip link set %s up' % vlanName)
61 addrIntf = vlanName
62 else:
63 addrIntf = intf
64
65 # Now configure the addresses on the vlan/native interface
66 if 'mac' in attrs:
67 self.cmd('ip link set %s down' % addrIntf)
68 self.cmd('ip link set %s address %s' % (addrIntf, attrs['mac']))
69 self.cmd('ip link set %s up' % addrIntf)
70 for addr in attrs['ipAddrs']:
71 self.cmd('ip addr add %s dev %s' % (addr, addrIntf))
72
73class QuaggaRouter(Router):
74
75 """Runs Quagga to create a router that can speak routing protocols."""
76
77 binDir = '/usr/lib/quagga'
78
79 def __init__(self, name, interfaces,
80 defaultRoute=None,
81 zebraConfFile=None,
82 protocols=[],
83 fpm=None,
84 runDir='/var/run/quagga', *args, **kwargs):
85 super(QuaggaRouter, self).__init__(name, interfaces, **kwargs)
86
87 self.protocols = protocols
88 self.fpm = fpm
89
90 for p in self.protocols:
91 p.setQuaggaRouter(self)
92
93 self.runDir = runDir
94 self.defaultRoute = defaultRoute
95
96 self.zebraConfFile = zebraConfFile
97 if (self.zebraConfFile is None):
98 self.zebraConfFile = '%s/zebrad%s.conf' % (self.runDir, self.name)
99 self.generateZebra()
100
101 self.socket = '%s/zebra%s.api' % (self.runDir, self.name)
102
103 self.zebraPidFile = '%s/zebra%s.pid' % (self.runDir, self.name)
104
105 def generateZebra(self):
106 configFile = open(self.zebraConfFile, 'w+')
107 configFile.write('hostname zebra-%s\n' % self.name)
108 configFile.write('password %s\n' % 'hello')
109 if (self.fpm is not None):
110 configFile.write('fpm connection ip %s port 2620' % self.fpm)
111 configFile.close()
112
113 def config(self, **kwargs):
114 super(QuaggaRouter, self).config(**kwargs)
115
116 self.cmd('%s/zebra -d -f %s -z %s -i %s'
117 % (QuaggaRouter.binDir, self.zebraConfFile, self.socket, self.zebraPidFile))
118
119 for p in self.protocols:
120 p.config(**kwargs)
121
122 if self.defaultRoute:
123 self.cmd('ip route add default via %s' % self.defaultRoute)
124
125 def terminate(self, **kwargs):
126 self.cmd("ps ax | grep '%s' | awk '{print $1}' | xargs kill"
127 % (self.socket))
128
129 for p in self.protocols:
130 p.terminate(**kwargs)
131
132 super(QuaggaRouter, self).terminate()
133
134class Protocol(object):
135
136 """Base abstraction of a protocol that the QuaggaRouter can run."""
137
138 def setQuaggaRouter(self, qr):
139 self.qr = qr
140
141 def config(self, **kwargs):
142 pass
143
144 def terminate(self, **kwargs):
145 pass
146
147class BgpProtocol(Protocol):
148
149 """Configures and runs the BGP protocol in Quagga."""
150
151 def __init__(self, configFile=None, asNum=None, neighbors=[], routes=[], *args, **kwargs):
152 self.configFile = configFile
153
154 self.asNum = asNum
155 self.neighbors = neighbors
156 self.routes = routes
157
158 def config(self, **kwargs):
159 if self.configFile is None:
160 self.configFile = '%s/bgpd%s.conf' % (self.qr.runDir, self.qr.name)
161 self.generateConfig()
162
163 bgpdPidFile = '%s/bgpd%s.pid' % (self.qr.runDir, self.qr.name)
164
165 self.qr.cmd('%s/bgpd -d -f %s -z %s -i %s'
166 % (QuaggaRouter.binDir, self.configFile, self.qr.socket, bgpdPidFile))
167
168 def generateConfig(self):
169 conf = ConfigurationWriter(self.configFile)
170
171 def getRouterId(interfaces):
172 intfAttributes = interfaces.itervalues().next()
173 print intfAttributes
174 if isinstance(intfAttributes, list):
175 # Try use the first set of attributes, but if using vlans they might not have addresses
176 intfAttributes = intfAttributes[1] if not intfAttributes[0]['ipAddrs'] else intfAttributes[0]
177 return intfAttributes['ipAddrs'][0].split('/')[0]
178
179 conf.writeLine('hostname bgp-%s' % self.qr.name);
180 conf.writeLine('password %s' % 'sdnip')
181 conf.writeLine('!')
182 conf.writeLine('router bgp %s' % self.asNum)
183
184 conf.indent()
185
186 conf.writeLine('bgp router-id %s' % getRouterId(self.qr.interfaces))
187 conf.writeLine('timers bgp %s' % '3 9')
188 conf.writeLine('!')
189
190 for neighbor in self.neighbors:
191 conf.writeLine('neighbor %s remote-as %s' % (neighbor['address'], neighbor['as']))
192 conf.writeLine('neighbor %s ebgp-multihop' % neighbor['address'])
193 conf.writeLine('neighbor %s timers connect %s' % (neighbor['address'], '5'))
194 conf.writeLine('neighbor %s advertisement-interval %s' % (neighbor['address'], '5'))
195 if 'port' in neighbor:
196 conf.writeLine('neighbor %s port %s' % (neighbor['address'], neighbor['port']))
197 conf.writeLine('!')
198
199 for route in self.routes:
200 conf.writeLine('network %s' % route)
201
202 conf.close()
203
204class OspfProtocol(Protocol):
205
206 """Configures and runs the OSPF protocol in Quagga."""
207
208 def __init__(self, configFile=None, *args, **kwargs):
209 self.configFile = configFile
210
211 def config(self, **kwargs):
212 if self.configFile is None:
213 self.configFile = '%s/ospfd%s.conf' % (self.qr.runDir, self.qr.name)
214 self.generateConfig()
215
216 ospfPidFile = '%s/ospf%s.pid' % (self.qr.runDir, self.qr.name)
217
218 self.qr.cmd('%s/ospfd -d -f %s -z %s -i %s'
219 % (QuaggaRouter.binDir, self.configFile, self.qr.socket, ospfPidFile))
220
221 def generateConfig(self):
222 conf = ConfigurationWriter(self.configFile)
223
224 def getRouterId(interfaces):
225 intfAttributes = interfaces.itervalues().next()
226 print intfAttributes
227 if isinstance(intfAttributes, list):
228 # Try use the first set of attributes, but if using vlans they might not have addresses
229 intfAttributes = intfAttributes[1] if not intfAttributes[0]['ipAddrs'] else intfAttributes[0]
230 return intfAttributes['ipAddrs'][0].split('/')[0]
231
232 conf.writeLine('hostname ospf-%s' % self.qr.name);
233 conf.writeLine('password %s' % 'hello')
234 conf.writeLine('!')
235 conf.writeLine('router ospf')
236
237 conf.indent()
238
239 conf.writeLine('ospf router-id %s' % getRouterId(self.qr.interfaces))
240 conf.writeLine('!')
241
242 for name, intf in self.qr.interfaces.items():
243 for ip in intf['ipAddrs']:
244 conf.writeLine('network %s area 0' % ip)
245 #if intf['ipAddrs'][0].startswith('192.168'):
246 # writeLine(1, 'passive-interface %s' % name)
247
248 conf.close()
249
250class PimProtocol(Protocol):
251
252 """Configures and runs the PIM protcol in Quagga."""
253
254 def __init__(self, configFile=None, *args, **kwargs):
255 self.configFile = configFile
256
257 def config(self, **kwargs):
258 pimPidFile = '%s/pim%s.pid' % (self.qr.runDir, self.qr.name)
Jonathan Hart09608592016-05-19 09:39:22 -0700259
260 self.qr.cmd('%s/pimd -Z -d -f %s -z %s -i %s'
Jonathan Hartce97e5b2016-04-19 01:41:31 -0700261 % (QuaggaRouter.binDir, self.configFile, self.qr.socket, pimPidFile))
262
263class ConfigurationWriter(object):
264
265 """Utility class for writing a configuration file."""
266
267 def __init__(self, filename):
268 self.filename = filename
269 self.indentValue = 0;
270
271 self.configFile = open(self.filename, 'w+')
272
273 def indent(self):
274 self.indentValue += 1
275
276 def unindent(self):
277 if (self.indentValue > 0):
278 self.indentValue -= 1
279
280 def write(self, string):
281 self.configFile.write(string)
282
283 def writeLine(self, string):
284 intentStr = ''
285 for _ in range(0, self.indentValue):
286 intentStr += ' '
287 self.write('%s%s\n' % (intentStr, string))
288
289 def close(self):
290 self.configFile.close()
291
292#Backward compatibility for BGP-only use case
293class BgpRouter(QuaggaRouter):
294
295 """Quagga router running the BGP protocol."""
296
297 def __init__(self, name, interfaces,
298 asNum, neighbors, routes=[],
299 defaultRoute=None,
300 quaggaConfFile=None,
301 zebraConfFile=None,
302 *args, **kwargs):
303 bgp = BgpProtocol(configFile=quaggaConfFile, asNum=asNum, neighbors=neighbors, routes=routes)
304
305 super(BgpRouter, self).__init__(name, interfaces,
306 zebraConfFile=zebraConfFile,
307 defaultRoute=defaultRoute,
308 protocols=[bgp],
309 *args, **kwargs)
310
311class RouterData(object):
312
313 """Internal data structure storing information about a router."""
314
315 def __init__(self, index):
316 self.index = index;
317 self.neighbors = []
318 self.interfaces = {}
319 self.switches = []
320
321 def addNeighbor(self, theirAddress, theirAsNum):
322 self.neighbors.append({'address':theirAddress.ip, 'as':theirAsNum})
323
324 def addInterface(self, intf, vlan, address):
325 if not intf in self.interfaces:
326 self.interfaces[intf] = InterfaceData(intf)
327
328 self.interfaces[intf].addAddress(vlan, address)
329
330 def setSwitch(self, switch):
331 self.switches.append(switch)
332
333class InterfaceData(object):
334
335 """Internal data structure storing information about an interface."""
336
337 def __init__(self, number):
338 self.number = number
339 self.addressesByVlan = {}
340
341 def addAddress(self, vlan, address):
342 if not vlan in self.addressesByVlan:
343 self.addressesByVlan[vlan] = []
344
345 self.addressesByVlan[vlan].append(address.with_prefixlen)
346
347class RoutedNetwork(object):
348
349 """Creates a host behind a router. This is common boilerplate topology
350 segment in routed networks."""
351
352 @staticmethod
353 def build(topology, router, hostName, networks):
354 # There's a convention that the router's addresses are already set up,
355 # and it has the last address in the network.
356
357 def getFirstAddress(network):
358 return '%s/%s' % (network[1], network.prefixlen)
359
360 defaultRoute = AutonomousSystem.getLastAddress(networks[0]).ip
361
362 host = topology.addHost(hostName, cls=RoutedHost,
363 ips=[getFirstAddress(network) for network in networks],
364 gateway=defaultRoute)
365
366 topology.addLink(router, host)
367
368class AutonomousSystem(object):
369
370 """Base abstraction of an autonomous system, which implies some internal
371 topology and connections to other topology elements (switches/other ASes)."""
372
373 psIdx = 1
374
375 def __init__(self, asNum, numRouters):
376 self.asNum = asNum
377 self.numRouters = numRouters
378 self.routers = {}
379 for i in range(1, numRouters + 1):
380 self.routers[i] = RouterData(i)
381
382 self.routerNodes={}
383
384 self.neighbors=[]
385 self.vlanAddresses={}
386
387 def peerWith(self, myRouter, myAddress, theirAddress, theirAsNum, intf=1, vlan=None):
388 router = self.routers[myRouter]
389
390 router.addInterface(intf, vlan, myAddress)
391 router.addNeighbor(theirAddress, theirAsNum)
392
393 def getRouter(self, i):
394 return self.routerNodes[i]
395
396 @staticmethod
397 def generatePeeringAddresses():
398 network = ip_network(u'10.0.%s.0/24' % AutonomousSystem.psIdx)
399 AutonomousSystem.psIdx += 1
400
401 return ip_interface('%s/%s' % (network[1], network.prefixlen)), \
402 ip_interface('%s/%s' % (network[2], network.prefixlen))
403
404 @staticmethod
405 def addPeering(as1, as2, router1=1, router2=1, intf1=1, intf2=1, address1=None, address2=None, useVlans=False):
406 vlan = AutonomousSystem.psIdx if useVlans else None
407
408 if address1 is None or address2 is None:
409 (address1, address2) = AutonomousSystem.generatePeeringAddresses()
410
411 as1.peerWith(router1, address1, address2, as2.asNum, intf=intf1, vlan=vlan)
412 as2.peerWith(router2, address2, address1, as1.asNum, intf=intf2, vlan=vlan)
413
414 @staticmethod
415 def getLastAddress(network):
416 return ip_interface(network.network_address + network.num_addresses - 2)
417
418 @staticmethod
419 def getIthAddress(network, i):
420 return ip_interface('%s/%s' % (network[i], network.prefixlen))
421
422class BasicAutonomousSystem(AutonomousSystem):
423
424 """Basic autonomous system containing one host and one or more routers
425 which peer with other ASes."""
426
427 def __init__(self, num, routes, numRouters=1):
428 super(BasicAutonomousSystem, self).__init__(65000+num, numRouters)
429 self.num = num
430 self.routes = routes
431
432 def addLink(self, switch, router=1):
433 self.routers[router].setSwitch(switch)
434
435 def build(self, topology):
436 self.addRouterAndHost(topology)
437
438 def addRouterAndHost(self, topology):
439
440 # TODO implementation is messy and needs to be cleaned up
441
442 intfs = {}
443
444 router = self.routers[1]
445 for i, router in self.routers.items():
446
447 #routerName = 'r%i%i' % (self.num, i)
448 routerName = 'r%i' % self.num
449 if not i==1:
450 routerName += ('%i' % i)
451
452 hostName = 'h%i' % self.num
453
454 for j, interface in router.interfaces.items():
455 nativeAddresses = interface.addressesByVlan.pop(None, [])
456 peeringIntf = [{'mac' : '00:00:%02x:00:%02x:%02x' % (self.num, i, j),
457 'ipAddrs' : nativeAddresses}]
458
459 for vlan, addresses in interface.addressesByVlan.items():
460 peeringIntf.append({'vlan':vlan,
461 'mac':'00:00:%02x:%02x:%02x:%02x' % (self.num, vlan, i, j),
462 'ipAddrs':addresses})
463
464 intfs.update({'%s-eth%s' % (routerName, j-1) : peeringIntf})
465
466 # Only add the host to the first router for now
467 if i==1:
468 internalAddresses=[]
469 for route in self.routes:
470 internalAddresses.append('%s/%s' % (AutonomousSystem.getLastAddress(route).ip, route.prefixlen))
471
472 internalIntf = {'ipAddrs' : internalAddresses}
473
474 # This is the configuration of the next interface after all the peering interfaces
475 intfs.update({'%s-eth%s' % (routerName, len(router.interfaces.keys())) : internalIntf})
476
477 routerNode = topology.addHost(routerName,
478 asNum=self.asNum, neighbors=router.neighbors,
479 routes=self.routes,
480 cls=BgpRouter, interfaces=intfs)
481
482 self.routerNodes[i] = routerNode
483
484 for switch in router.switches:
485 topology.addLink(switch, routerNode)
486
487 # Only add the host to the first router for now
488 if i==1:
489 defaultRoute = internalAddresses[0].split('/')[0]
490
491 host = topology.addHost(hostName, cls=RoutedHost,
492 ips=[self.getFirstAddress(route) for route in self.routes],
493 gateway=defaultRoute)
494
495 topology.addLink(routerNode, host)
496
497 #def getLastAddress(self, network):
498 # return ip_address(network.network_address + network.num_addresses - 2)
499
500 def getFirstAddress(self, network):
501 return '%s/%s' % (network[1], network.prefixlen)
502
503# TODO fix this AS - doesn't currently work
504class RouteServerAutonomousSystem(BasicAutonomousSystem):
505
506 def __init__(self, routerAddress, *args, **kwargs):
507 BasicAutonomousSystem.__init__(self, *args, **kwargs)
508
509 self.routerAddress = routerAddress
510
511 def build(self, topology, connectAtSwitch):
512
513 switch = topology.addSwitch('as%isw' % self.num, cls=OVSBridge)
514
515 self.addRouterAndHost(topology, self.routerAddress, switch)
516
517 rsName = 'rs%i' % self.num
518 routeServer = topology.addHost(rsName,
519 self.asnum, self.neighbors,
520 cls=BgpRouter,
521 interfaces={'%s-eth0' % rsName : {'ipAddrs':[self.peeringAddress]}})
522
523 topology.addLink(routeServer, switch)
524 topology.addLink(switch, connectAtSwitch)
525
526class SdnAutonomousSystem(AutonomousSystem):
527
528 """Runs the internal BGP speakers needed for ONOS routing apps like
529 SDN-IP."""
530
Jonathan Hartfc0af772017-01-16 13:15:08 -0800531 def __init__(self, onosIps, numBgpSpeakers=1, asNum=65000, externalOnos=True,
532 peerIntfConfig=None, withFpm=False):
Jonathan Hartce97e5b2016-04-19 01:41:31 -0700533 super(SdnAutonomousSystem, self).__init__(asNum, numBgpSpeakers)
534 self.onosIps = onosIps
535 self.numBgpSpeakers = numBgpSpeakers
536 self.peerIntfConfig = peerIntfConfig
Jonathan Hartfc0af772017-01-16 13:15:08 -0800537 self.withFpm = withFpm
Jonathan Hartce97e5b2016-04-19 01:41:31 -0700538 self.externalOnos= externalOnos
539 self.internalPeeringSubnet = ip_network(u'1.1.1.0/24')
540
541 for router in self.routers.values():
542 # Add iBGP sessions to ONOS nodes
543 for onosIp in onosIps:
544 router.neighbors.append({'address':onosIp, 'as':asNum, 'port':2000})
545
546 # Add iBGP sessions to other BGP speakers
547 for i, router2 in self.routers.items():
548 if router == router2:
549 continue
550 ip = AutonomousSystem.getIthAddress(self.internalPeeringSubnet, 10+i)
551 router.neighbors.append({'address':ip.ip, 'as':asNum})
552
553 def build(self, topology, connectAtSwitch, controlSwitch):
554
555 natIp = AutonomousSystem.getLastAddress(self.internalPeeringSubnet)
556
557 for i, router in self.routers.items():
558 name = 'bgp%s' % i
559
560 ip = AutonomousSystem.getIthAddress(self.internalPeeringSubnet, 10+i)
561 eth0 = { 'ipAddrs' : [ str(ip) ] }
562 if self.peerIntfConfig is not None:
563 eth1 = self.peerIntfConfig
564 else:
565 nativeAddresses = router.interfaces[1].addressesByVlan.pop(None, [])
566 eth1 = [{ 'mac':'00:00:00:00:00:%02x' % i,
567 'ipAddrs' : nativeAddresses }]
568
569 for vlan, addresses in router.interfaces[1].addressesByVlan.items():
570 eth1.append({'vlan':vlan,
571 'mac':'00:00:00:%02x:%02x:00' % (i, vlan),
572 'ipAddrs':addresses})
573
574
575 intfs = { '%s-eth0' % name : eth0,
576 '%s-eth1' % name : eth1 }
577
578 bgp = topology.addHost( name, cls=BgpRouter, asNum=self.asNum,
579 neighbors=router.neighbors,
580 interfaces=intfs,
581 defaultRoute=str(natIp.ip),
Jonathan Hartfc0af772017-01-16 13:15:08 -0800582 fpm=self.onosIps[0] if self.withFpm else None )
Jonathan Hartce97e5b2016-04-19 01:41:31 -0700583
584 topology.addLink( bgp, controlSwitch )
585 topology.addLink( bgp, connectAtSwitch )
586
587
588 if self.externalOnos:
589 nat = topology.addHost('nat', cls=NAT,
590 ip='%s/%s' % (natIp.ip, self.internalPeeringSubnet.prefixlen),
591 subnet=str(self.internalPeeringSubnet), inNamespace=False);
592 topology.addLink(controlSwitch, nat)
593
594
595def generateRoutes(baseRange, numRoutes, subnetSize=None):
596 baseNetwork = ip_network(baseRange)
597
598 # We need to get at least 2 addresses out of each subnet, so the biggest
599 # prefix length we can have is /30
600 maxPrefixLength = baseNetwork.max_prefixlen - 2
601
602 if subnetSize is not None:
603 return list(baseNetwork.subnets(new_prefix=subnetSize))
604
605 trySubnetSize = baseNetwork.prefixlen + 1
606 while trySubnetSize <= maxPrefixLength and \
607 len(list(baseNetwork.subnets(new_prefix=trySubnetSize))) < numRoutes:
608 trySubnetSize += 1
609
610 if trySubnetSize > maxPrefixLength:
611 raise Exception("Can't get enough routes from input parameters")
612
613 return list(baseNetwork.subnets(new_prefix=trySubnetSize))[:numRoutes]
614
615class RoutingCli( CLI ):
616
617 """CLI command that can bring a host up or down. Useful for simulating router failure."""
618
619 def do_host( self, line ):
620 args = line.split()
621 if len(args) != 2:
622 error( 'invalid number of args: host <host name> {up, down}\n' )
623 return
624
625 host = args[ 0 ]
626 command = args[ 1 ]
627 if host not in self.mn or self.mn.get( host ) not in self.mn.hosts:
628 error( 'invalid host: %s\n' % args[ 1 ] )
629 else:
630 if command == 'up':
631 op = 'up'
632 elif command == 'down':
633 op = 'down'
634 else:
635 error( 'invalid command: host <host name> {up, down}\n' )
636 return
637
638 for intf in self.mn.get( host ).intfList( ):
639 intf.link.intf1.ifconfig( op )
640 intf.link.intf2.ifconfig( op )