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