Create stc net-setup topo for SDN-IP

Updated to latest routinglib version.

Change-Id: I939f5562cc88c30671f8f2f4b074699bd5367a79
diff --git a/tools/test/topos/onosnet.py b/tools/test/topos/onosnet.py
index a1550fc..f8e4f6a 100644
--- a/tools/test/topos/onosnet.py
+++ b/tools/test/topos/onosnet.py
@@ -208,19 +208,22 @@
 CLI.do_bgIperf = do_bgIperf
 CLI.do_gratuitousArp = do_gratuitousArp
 
-def run( topo, controllers=None, link=TCLink, autoSetMacs=True):
-    if not topo:
-        print 'Need to provide a topology'
-        exit(1)
-
-    parser = ArgumentParser(description='ONOS Mininet ' + type(topo).__name__)
+def parse_args():
+    parser = ArgumentParser(description='ONOS Mininet')
     parser.add_argument('--cluster-size', help='Starts an ONOS cluster with the given number of instances',
                         type=int, action='store', dest='clusterSize', required=False, default=0)
     parser.add_argument('--netcfg', help='Relative path of the JSON file to be used with netcfg',
                         type=str, action='store', dest='netcfgJson', required=False, default='')
     parser.add_argument('ipAddrs', metavar='IP', type=str, nargs='*',
                         help='List of controller IP addresses', default=[])
-    args = parser.parse_args()
+    return parser.parse_args()
+
+def run( topo, controllers=None, link=TCLink, autoSetMacs=True):
+    if not topo:
+        print 'Need to provide a topology'
+        exit(1)
+
+    args = parse_args()
 
     if not controllers and len(args.ipAddrs) > 0:
         controllers = args.ipAddrs
diff --git a/tools/test/topos/routinglib.py b/tools/test/topos/routinglib.py
new file mode 100644
index 0000000..6ac0932
--- /dev/null
+++ b/tools/test/topos/routinglib.py
@@ -0,0 +1,654 @@
+#!/usr/bin/python
+
+"""
+Libraries for creating L3 topologies with routing protocols.
+"""
+
+from mininet.node import Host, OVSBridge
+from mininet.nodelib import NAT
+from mininet.log import info, debug, error
+from mininet.cli import CLI
+from ipaddress import ip_network, ip_address, ip_interface
+import os
+
+class RoutedHost(Host):
+    """Host that can be configured with multiple IP addresses."""
+    def __init__(self, name, ips, gateway, *args, **kwargs):
+        super(RoutedHost, self).__init__(name, *args, **kwargs)
+
+        self.ips = ips
+        self.gateway = gateway
+
+    def config(self, **kwargs):
+        Host.config(self, **kwargs)
+
+        self.cmd('ip addr flush dev %s' % self.defaultIntf())
+        for ip in self.ips:
+            self.cmd('ip addr add %s dev %s' % (ip, self.defaultIntf()))
+
+        self.cmd('ip route add default via %s' % self.gateway)
+
+class Router(Host):
+    
+    """An L3 router.
+    Configures the Linux kernel for L3 forwarding and supports rich interface
+    configuration of IP addresses, MAC addresses and VLANs."""
+    
+    def __init__(self, name, interfaces, *args, **kwargs):
+        super(Router, self).__init__(name, **kwargs)
+
+        self.interfaces = interfaces
+        
+    def config(self, **kwargs):
+        super(Host, self).config(**kwargs)
+        
+        self.cmd('sysctl net.ipv4.ip_forward=1')
+        self.cmd('sysctl net.ipv4.conf.all.rp_filter=0')
+
+        for intf, configs in self.interfaces.items():
+            self.cmd('ip addr flush dev %s' % intf)
+            self.cmd( 'sysctl net.ipv4.conf.%s.rp_filter=0' % intf )
+            
+            if not isinstance(configs, list):
+                configs = [configs]
+                
+            for attrs in configs:
+                # Configure the vlan if there is one    
+                if 'vlan' in attrs:
+                    vlanName = '%s.%s' % (intf, attrs['vlan'])
+                    self.cmd('ip link add link %s name %s type vlan id %s' % 
+                             (intf, vlanName, attrs['vlan']))
+                    self.cmd('ip link set %s up' % vlanName)
+                    addrIntf = vlanName
+                else:
+                    addrIntf = intf
+                    
+                # Now configure the addresses on the vlan/native interface
+                if 'mac' in attrs:
+                    self.cmd('ip link set %s down' % addrIntf)
+                    self.cmd('ip link set %s address %s' % (addrIntf, attrs['mac']))
+                    self.cmd('ip link set %s up' % addrIntf)
+                for addr in attrs['ipAddrs']:
+                    self.cmd('ip addr add %s dev %s' % (addr, addrIntf))
+
+class QuaggaRouter(Router):
+    
+    """Runs Quagga to create a router that can speak routing protocols."""
+    
+    binDir = '/usr/lib/quagga'
+    logDir = '/var/log/quagga'
+    
+    def __init__(self, name, interfaces,
+                 defaultRoute=None,
+                 zebraConfFile=None,
+                 protocols=[],
+                 fpm=None,
+                 runDir='/var/run/quagga', *args, **kwargs):
+        super(QuaggaRouter, self).__init__(name, interfaces, **kwargs)
+        
+        self.protocols = protocols
+        self.fpm = fpm
+        
+        for p in self.protocols:
+            p.setQuaggaRouter(self)
+        
+        self.runDir = runDir
+        self.defaultRoute = defaultRoute
+        
+        # Ensure required directories exist
+        try:
+            original_umask = os.umask(0)
+            if (not os.path.isdir(QuaggaRouter.logDir)):
+                os.makedirs(QuaggaRouter.logDir, 0777)
+            if (not os.path.isdir(self.runDir)):
+                os.makedirs(self.runDir, 0777)
+        finally:
+            os.umask(original_umask)
+
+        self.zebraConfFile = zebraConfFile
+        if (self.zebraConfFile is None):
+            self.zebraConfFile = '%s/zebrad%s.conf' % (self.runDir, self.name)
+            self.generateZebra()
+            
+        self.socket = '%s/zebra%s.api' % (self.runDir, self.name)
+        
+        self.zebraPidFile = '%s/zebra%s.pid' % (self.runDir, self.name)
+
+    def generateZebra(self):
+        configFile = open(self.zebraConfFile, 'w+')
+        configFile.write('log file %s/zebrad%s.log\n' % (QuaggaRouter.logDir, self.name))
+        configFile.write('hostname zebra-%s\n' % self.name)
+        configFile.write('password %s\n' % 'hello')
+        if (self.fpm is not None):
+            configFile.write('fpm connection ip %s port 2620' % self.fpm)
+        configFile.close()
+
+    def config(self, **kwargs):
+        super(QuaggaRouter, self).config(**kwargs)
+
+        self.cmd('%s/zebra -d -f %s -z %s -i %s'
+                 % (QuaggaRouter.binDir, self.zebraConfFile, self.socket, self.zebraPidFile))
+        
+        for p in self.protocols:
+            p.config(**kwargs)
+        
+        if self.defaultRoute:
+            self.cmd('ip route add default via %s' % self.defaultRoute)
+        
+    def terminate(self, **kwargs):
+        self.cmd("ps ax | grep '%s' | awk '{print $1}' | xargs kill" 
+                 % (self.socket))
+        
+        for p in self.protocols:
+            p.terminate(**kwargs)
+
+        super(QuaggaRouter, self).terminate()
+        
+class Protocol(object):
+    
+    """Base abstraction of a protocol that the QuaggaRouter can run."""
+        
+    def setQuaggaRouter(self, qr):
+        self.qr = qr
+        
+    def config(self, **kwargs):
+        pass
+    
+    def terminate(self, **kwargs):
+        pass
+        
+class BgpProtocol(Protocol):
+    
+    """Configures and runs the BGP protocol in Quagga."""
+    
+    def __init__(self, configFile=None, asNum=None, neighbors=[], routes=[], *args, **kwargs):
+        self.configFile = configFile
+        
+        self.asNum = asNum
+        self.neighbors = neighbors
+        self.routes = routes
+            
+    def config(self, **kwargs):
+        if self.configFile is None:
+            self.configFile = '%s/bgpd%s.conf' % (self.qr.runDir, self.qr.name)
+            self.generateConfig()
+        
+        bgpdPidFile = '%s/bgpd%s.pid' % (self.qr.runDir, self.qr.name)
+        
+        self.qr.cmd('%s/bgpd -d -f %s -z %s -i %s'
+                     % (QuaggaRouter.binDir, self.configFile, self.qr.socket, bgpdPidFile))
+        
+    def generateConfig(self):
+        conf = ConfigurationWriter(self.configFile)
+                    
+        def getRouterId(interfaces):
+            intfAttributes = interfaces.itervalues().next()
+            print intfAttributes
+            if isinstance(intfAttributes, list):
+                # Try use the first set of attributes, but if using vlans they might not have addresses
+                intfAttributes = intfAttributes[1] if not intfAttributes[0]['ipAddrs'] else intfAttributes[0]
+            return intfAttributes['ipAddrs'][0].split('/')[0]
+        
+        conf.writeLine('log file %s/bgpd%s.log' % (QuaggaRouter.logDir, self.qr.name))
+        conf.writeLine('hostname bgp-%s' % self.qr.name);
+        conf.writeLine('password %s' % 'sdnip')
+        conf.writeLine('!')
+        conf.writeLine('router bgp %s' % self.asNum)
+        
+        conf.indent()
+        
+        conf.writeLine('bgp router-id %s' % getRouterId(self.qr.interfaces))
+        conf.writeLine('timers bgp %s' % '3 9')
+        conf.writeLine('!')
+        
+        for neighbor in self.neighbors:
+            conf.writeLine('neighbor %s remote-as %s' % (neighbor['address'], neighbor['as']))
+            conf.writeLine('neighbor %s ebgp-multihop' % neighbor['address'])
+            conf.writeLine('neighbor %s timers connect %s' % (neighbor['address'], '5'))
+            conf.writeLine('neighbor %s advertisement-interval %s' % (neighbor['address'], '5'))
+            if 'port' in neighbor:
+                conf.writeLine('neighbor %s port %s' % (neighbor['address'], neighbor['port']))
+            conf.writeLine('!')
+            
+        for route in self.routes:
+            conf.writeLine('network %s' % route)
+        
+        conf.close()
+    
+class OspfProtocol(Protocol):
+    
+    """Configures and runs the OSPF protocol in Quagga."""
+    
+    def __init__(self, configFile=None, *args, **kwargs):
+        self.configFile = configFile
+            
+    def config(self, **kwargs):
+        if self.configFile is None:
+            self.configFile = '%s/ospfd%s.conf' % (self.qr.runDir, self.qr.name)
+            self.generateConfig()
+        
+        ospfPidFile = '%s/ospf%s.pid' % (self.qr.runDir, self.qr.name)
+        
+        self.qr.cmd('%s/ospfd -d -f %s -z %s -i %s'
+                     % (QuaggaRouter.binDir, self.configFile, self.qr.socket, ospfPidFile))
+        
+    def generateConfig(self):
+        conf = ConfigurationWriter(self.configFile)
+            
+        def getRouterId(interfaces):
+            intfAttributes = interfaces.itervalues().next()
+            print intfAttributes
+            if isinstance(intfAttributes, list):
+                # Try use the first set of attributes, but if using vlans they might not have addresses
+                intfAttributes = intfAttributes[1] if not intfAttributes[0]['ipAddrs'] else intfAttributes[0]
+            return intfAttributes['ipAddrs'][0].split('/')[0]
+        
+        conf.writeLine('hostname ospf-%s' % self.qr.name);
+        conf.writeLine('password %s' % 'hello')
+        conf.writeLine('!')
+        conf.writeLine('router ospf')
+        
+        conf.indent()
+        
+        conf.writeLine('ospf router-id %s' % getRouterId(self.qr.interfaces))
+        conf.writeLine('!')
+        
+        for name, intf in self.qr.interfaces.items():
+            for ip in intf['ipAddrs']:
+                conf.writeLine('network %s area 0' % ip)
+            #if intf['ipAddrs'][0].startswith('192.168'):
+            #    writeLine(1, 'passive-interface %s' % name)
+            
+        conf.close()
+        
+class PimProtocol(Protocol):
+    
+    """Configures and runs the PIM protcol in Quagga."""
+    
+    def __init__(self, configFile=None, *args, **kwargs):
+        self.configFile = configFile
+        
+    def config(self, **kwargs):
+        pimPidFile = '%s/pim%s.pid' % (self.qr.runDir, self.qr.name)
+                
+        self.qr.cmd('%s/pimd -Z -d -f %s -z %s -i %s'
+                     % (QuaggaRouter.binDir, self.configFile, self.qr.socket, pimPidFile))
+        
+class ConfigurationWriter(object):
+    
+    """Utility class for writing a configuration file."""
+    
+    def __init__(self, filename):
+        self.filename = filename
+        self.indentValue = 0;
+        
+        self.configFile = open(self.filename, 'w+')
+    
+    def indent(self):
+        self.indentValue += 1
+        
+    def unindent(self):
+        if (self.indentValue > 0):
+            self.indentValue -= 1
+            
+    def write(self, string):
+        self.configFile.write(string)
+    
+    def writeLine(self, string):
+        intentStr = ''
+        for _ in range(0, self.indentValue):
+            intentStr += '  '
+        self.write('%s%s\n' % (intentStr, string))
+        
+    def close(self):
+        self.configFile.close()
+
+#Backward compatibility for BGP-only use case
+class BgpRouter(QuaggaRouter):
+    
+    """Quagga router running the BGP protocol."""
+    
+    def __init__(self, name, interfaces,
+                 asNum, neighbors, routes=[],
+                 defaultRoute=None,
+                 quaggaConfFile=None,
+                 zebraConfFile=None,
+                 *args, **kwargs):
+        bgp = BgpProtocol(configFile=quaggaConfFile, asNum=asNum, neighbors=neighbors, routes=routes)
+        
+        super(BgpRouter, self).__init__(name, interfaces, 
+                                        zebraConfFile=zebraConfFile,
+                                        defaultRoute=defaultRoute,
+                                        protocols=[bgp],
+                                        *args, **kwargs)
+        
+class RouterData(object):
+    
+    """Internal data structure storing information about a router."""
+    
+    def __init__(self, index):
+        self.index = index;
+        self.neighbors = []
+        self.interfaces = {}
+        self.switches = []
+        
+    def addNeighbor(self, theirAddress, theirAsNum):
+        self.neighbors.append({'address':theirAddress.ip, 'as':theirAsNum})
+    
+    def addInterface(self, intf, vlan, address):
+        if not intf in self.interfaces:
+            self.interfaces[intf] = InterfaceData(intf)
+            
+        self.interfaces[intf].addAddress(vlan, address)
+        
+    def setSwitch(self, switch):
+        self.switches.append(switch)
+        
+class InterfaceData(object):
+    
+    """Internal data structure storing information about an interface."""
+    
+    def __init__(self, number):
+        self.number = number
+        self.addressesByVlan = {}
+        
+    def addAddress(self, vlan, address):
+        if not vlan in self.addressesByVlan:
+            self.addressesByVlan[vlan] = []
+            
+        self.addressesByVlan[vlan].append(address.with_prefixlen)
+        
+class RoutedNetwork(object):
+    
+    """Creates a host behind a router. This is common boilerplate topology
+    segment in routed networks."""
+    
+    @staticmethod
+    def build(topology, router, hostName, networks):
+        # There's a convention that the router's addresses are already set up,
+        # and it has the last address in the network.
+        
+        def getFirstAddress(network):
+            return '%s/%s' % (network[1], network.prefixlen)
+        
+        defaultRoute = AutonomousSystem.getLastAddress(networks[0]).ip
+        
+        host = topology.addHost(hostName, cls=RoutedHost,
+                                ips=[getFirstAddress(network) for network in networks],
+                                gateway=defaultRoute)
+
+        topology.addLink(router, host)
+
+class AutonomousSystem(object):
+    
+    """Base abstraction of an autonomous system, which implies some internal
+    topology and connections to other topology elements (switches/other ASes)."""
+    
+    psIdx = 1
+    
+    def __init__(self, asNum, numRouters):
+        self.asNum = asNum
+        self.numRouters = numRouters
+        self.routers = {}
+        for i in range(1, numRouters + 1):
+            self.routers[i] = RouterData(i)
+            
+        self.routerNodes={}
+            
+        self.neighbors=[]
+        self.vlanAddresses={}
+        
+    def peerWith(self, myRouter, myAddress, theirAddress, theirAsNum, intf=1, vlan=None):
+        router = self.routers[myRouter]
+        
+        router.addInterface(intf, vlan, myAddress)
+        router.addNeighbor(theirAddress, theirAsNum)
+
+    def getRouter(self, i):
+        return self.routerNodes[i]
+
+    @staticmethod
+    def generatePeeringAddresses():
+        network = ip_network(u'10.0.%s.0/24' % AutonomousSystem.psIdx)
+        AutonomousSystem.psIdx += 1
+        
+        return ip_interface('%s/%s' % (network[1], network.prefixlen)), \
+            ip_interface('%s/%s' % (network[2], network.prefixlen))
+        
+    @staticmethod
+    def addPeering(as1, as2, router1=1, router2=1, intf1=1, intf2=1, address1=None, address2=None, useVlans=False):
+        vlan = AutonomousSystem.psIdx if useVlans else None
+        
+        if address1 is None or address2 is None:
+            (address1, address2) = AutonomousSystem.generatePeeringAddresses()
+            
+        as1.peerWith(router1, address1, address2, as2.asNum, intf=intf1, vlan=vlan)
+        as2.peerWith(router2, address2, address1, as1.asNum, intf=intf2, vlan=vlan)
+    
+    @staticmethod
+    def getLastAddress(network):
+        return ip_interface(network.network_address + network.num_addresses - 2)
+    
+    @staticmethod
+    def getIthAddress(network, i):
+        return ip_interface('%s/%s' % (network[i], network.prefixlen))
+
+class BasicAutonomousSystem(AutonomousSystem):
+
+    """Basic autonomous system containing one host and one or more routers
+    which peer with other ASes."""
+
+    def __init__(self, num, routes, numRouters=1):
+        super(BasicAutonomousSystem, self).__init__(65000+num, numRouters)
+        self.num = num
+        self.routes = routes
+        
+    def addLink(self, switch, router=1):
+        self.routers[router].setSwitch(switch)
+
+    def build(self, topology):
+        self.addRouterAndHost(topology)
+
+    def addRouterAndHost(self, topology):
+        
+        # TODO implementation is messy and needs to be cleaned up
+        
+        intfs = {}
+        
+        router = self.routers[1]
+        for i, router in self.routers.items():
+        
+            #routerName = 'r%i%i' % (self.num, i)
+            routerName = 'r%i' % self.num
+            if not i==1:
+                routerName += ('%i' % i)
+                
+            hostName = 'h%i' % self.num
+        
+            for j, interface in router.interfaces.items():
+                nativeAddresses = interface.addressesByVlan.pop(None, [])
+                peeringIntf = [{'mac' : '00:00:%02x:00:%02x:%02x' % (self.num, i, j),
+                               'ipAddrs' : nativeAddresses}]
+                
+                for vlan, addresses in interface.addressesByVlan.items():
+                    peeringIntf.append({'vlan':vlan,
+                                        'mac':'00:00:%02x:%02x:%02x:%02x' % (self.num, vlan, i, j),
+                                        'ipAddrs':addresses})
+                    
+                intfs.update({'%s-eth%s' % (routerName, j-1) : peeringIntf})
+            
+            # Only add the host to the first router for now
+            if i==1:
+                internalAddresses=[]
+                for route in self.routes:
+                    internalAddresses.append('%s/%s' % (AutonomousSystem.getLastAddress(route).ip, route.prefixlen))
+        
+                internalIntf = {'ipAddrs' : internalAddresses}
+        
+                # This is the configuration of the next interface after all the peering interfaces
+                intfs.update({'%s-eth%s' % (routerName, len(router.interfaces.keys())) : internalIntf})
+    
+            routerNode = topology.addHost(routerName,  
+                                  asNum=self.asNum, neighbors=router.neighbors,
+                                  routes=self.routes,
+                                  cls=BgpRouter, interfaces=intfs)
+            
+            self.routerNodes[i] = routerNode
+
+            for switch in router.switches:
+                topology.addLink(switch, routerNode)
+
+            # Only add the host to the first router for now
+            if i==1:
+                defaultRoute = internalAddresses[0].split('/')[0]
+        
+                host = topology.addHost(hostName, cls=RoutedHost,
+                                        ips=[self.getFirstAddress(route) for route in self.routes],
+                                        gateway=defaultRoute)
+        
+                topology.addLink(routerNode, host)
+
+    #def getLastAddress(self, network):
+    #    return ip_address(network.network_address + network.num_addresses - 2)
+    
+    def getFirstAddress(self, network):
+        return '%s/%s' % (network[1], network.prefixlen)
+
+# TODO fix this AS - doesn't currently work
+class RouteServerAutonomousSystem(BasicAutonomousSystem):
+
+    def __init__(self, routerAddress, *args, **kwargs):
+        BasicAutonomousSystem.__init__(self, *args, **kwargs)
+
+        self.routerAddress = routerAddress
+
+    def build(self, topology, connectAtSwitch):
+
+        switch = topology.addSwitch('as%isw' % self.num, cls=OVSBridge)
+
+        self.addRouterAndHost(topology, self.routerAddress, switch)
+
+        rsName = 'rs%i' % self.num
+        routeServer = topology.addHost(rsName,
+                                       self.asnum, self.neighbors,
+                                       cls=BgpRouter,
+                                       interfaces={'%s-eth0' % rsName : {'ipAddrs':[self.peeringAddress]}})
+
+        topology.addLink(routeServer, switch)
+        topology.addLink(switch, connectAtSwitch)
+        
+class SdnAutonomousSystem(AutonomousSystem):
+    
+    """Runs the internal BGP speakers needed for ONOS routing apps like
+    SDN-IP."""
+    
+    def __init__(self, onosIps, numBgpSpeakers=1, asNum=65000, externalOnos=True,
+                 peerIntfConfig=None, withFpm=False):
+        super(SdnAutonomousSystem, self).__init__(asNum, numBgpSpeakers)
+        self.onosIps = onosIps
+        self.numBgpSpeakers = numBgpSpeakers
+        self.peerIntfConfig = peerIntfConfig
+        self.withFpm = withFpm
+        self.externalOnos= externalOnos
+        self.internalPeeringSubnet = ip_network(u'1.1.1.0/24')
+        
+        for router in self.routers.values():
+            # Add iBGP sessions to ONOS nodes
+            for onosIp in onosIps:
+                router.neighbors.append({'address':onosIp, 'as':asNum, 'port':2000})
+                
+            # Add iBGP sessions to other BGP speakers
+            for i, router2 in self.routers.items():
+                if router == router2:
+                    continue
+                ip = AutonomousSystem.getIthAddress(self.internalPeeringSubnet, 10+i)
+                router.neighbors.append({'address':ip.ip, 'as':asNum})
+        
+    def build(self, topology, connectAtSwitch, controlSwitch):
+        
+        natIp = AutonomousSystem.getLastAddress(self.internalPeeringSubnet)
+        
+        for i, router in self.routers.items():
+            name = 'bgp%s' % i
+            
+            ip = AutonomousSystem.getIthAddress(self.internalPeeringSubnet, 10+i)
+            eth0 = { 'ipAddrs' : [ str(ip) ] }
+            if self.peerIntfConfig is not None:
+                eth1 = self.peerIntfConfig
+            else:
+                nativeAddresses = router.interfaces[1].addressesByVlan.pop(None, [])
+                eth1 = [{ 'mac':'00:00:00:00:00:%02x' % i, 
+                         'ipAddrs' : nativeAddresses }]
+                
+                for vlan, addresses in router.interfaces[1].addressesByVlan.items():
+                    eth1.append({'vlan':vlan,
+                                'mac':'00:00:00:%02x:%02x:00' % (i, vlan),
+                                'ipAddrs':addresses})
+            
+            
+            intfs = { '%s-eth0' % name : eth0,
+                      '%s-eth1' % name : eth1 }
+            
+            bgp = topology.addHost( name, cls=BgpRouter, asNum=self.asNum, 
+                                    neighbors=router.neighbors,
+                                    interfaces=intfs, 
+                                    defaultRoute=str(natIp.ip),
+                                    fpm=self.onosIps[0] if self.withFpm else None )
+            
+            topology.addLink( bgp, controlSwitch )
+            topology.addLink( bgp, connectAtSwitch )
+            
+            
+        if self.externalOnos:
+            nat = topology.addHost('nat', cls=NAT, 
+                                   ip='%s/%s' % (natIp.ip, self.internalPeeringSubnet.prefixlen), 
+                                   subnet=str(self.internalPeeringSubnet), inNamespace=False);
+            topology.addLink(controlSwitch, nat)
+
+        
+def generateRoutes(baseRange, numRoutes, subnetSize=None):
+    baseNetwork = ip_network(baseRange)
+    
+    # We need to get at least 2 addresses out of each subnet, so the biggest
+    # prefix length we can have is /30
+    maxPrefixLength = baseNetwork.max_prefixlen - 2
+    
+    if subnetSize is not None:
+        return list(baseNetwork.subnets(new_prefix=subnetSize))
+    
+    trySubnetSize = baseNetwork.prefixlen + 1
+    while trySubnetSize <= maxPrefixLength and \
+            len(list(baseNetwork.subnets(new_prefix=trySubnetSize))) < numRoutes:
+        trySubnetSize += 1
+        
+    if trySubnetSize > maxPrefixLength:
+        raise Exception("Can't get enough routes from input parameters")
+    
+    return list(baseNetwork.subnets(new_prefix=trySubnetSize))[:numRoutes]
+    
+class RoutingCli( CLI ):
+    
+    """CLI command that can bring a host up or down. Useful for simulating router failure."""
+    
+    def do_host( self, line ):
+        args = line.split()
+        if len(args) != 2:
+            error( 'invalid number of args: host <host name> {up, down}\n' )
+            return
+        
+        host = args[ 0 ]
+        command = args[ 1 ]
+        if host not in self.mn or self.mn.get( host ) not in self.mn.hosts:
+            error( 'invalid host: %s\n' % args[ 1 ] )
+        else:
+            if command == 'up':
+                op = 'up'
+            elif command == 'down':
+                op = 'down'
+            else:
+                error( 'invalid command: host <host name> {up, down}\n' )
+                return
+
+            for intf in self.mn.get( host ).intfList( ):
+                intf.link.intf1.ifconfig( op )
+                intf.link.intf2.ifconfig( op )
diff --git a/tools/test/topos/sdnip.json b/tools/test/topos/sdnip.json
new file mode 100644
index 0000000..912f54b
--- /dev/null
+++ b/tools/test/topos/sdnip.json
@@ -0,0 +1,296 @@
+{
+  "devices":{
+    "of:0000000000000001":{
+      "basic":{
+        "name":"S1",
+        "latitude":39.739317,
+        "longitude":-104.983791
+      }
+    },
+    "of:0000000000000002":{
+      "basic":{
+        "name":"S2",
+        "latitude":39.769089,
+        "longitude":-86.158039
+      }
+    },
+    "of:0000000000000003":{
+      "basic":{
+        "name":"S3",
+        "latitude":35.116541,
+        "longitude":-106.604146
+      }
+    },
+    "of:0000000000000004":{
+      "basic":{
+        "name":"S4",
+        "latitude":32.779501,
+        "longitude":-96.801104
+      }
+    },
+    "of:0000000000000005":{
+      "basic":{
+        "name":"S5",
+        "latitude":45.522585,
+        "longitude":-122.677890
+      }
+    },
+    "of:0000000000000006":{
+      "basic":{
+        "name":"S6",
+        "latitude":37.785286,
+        "longitude":-122.406509
+      }
+    },
+    "of:0000000000000007":{
+      "basic":{
+        "name":"S7",
+        "latitude":34.055604,
+        "longitude":-118.248567
+      }
+    },
+    "of:0000000000000008":{
+      "basic":{
+        "name":"S8",
+        "latitude":40.769487,
+        "longitude":-73.972520
+      }
+    },
+    "of:0000000000000009":{
+      "basic":{
+        "name":"S9",
+        "latitude":38.897676,
+        "longitude":-77.036525
+      }
+    },
+    "of:000000000000000A":{
+      "basic":{
+        "name":"S10",
+        "latitude":33.756298,
+        "longitude":-84.388507
+      }
+    }
+  },
+  "hosts":{
+    "00:00:00:00:00:01/-1":{
+      "basic":{
+        "location":"of:0000000000000001/7",
+        "uiType":"bgpSpeaker",
+        "latitude":42.8498743,
+        "longitude":-106.3598992,
+        "ips":[
+          "10.0.1.2",
+          "10.0.3.2",
+          "10.1.7.2"
+        ]
+      }
+    },
+    "00:00:00:00:00:02/-1":{
+      "basic":{
+        "location":"of:0000000000000001/8",
+        "uiType":"bgpSpeaker",
+        "latitude":42.8498743,
+        "longitude":-103.2984284,
+        "ips":[
+          "10.0.4.2",
+          "10.0.5.2"
+        ]
+      }
+    },
+    "00:00:00:00:00:03/-1":{
+      "basic":{
+        "location":"of:0000000000000001/9",
+        "uiType":"bgpSpeaker",
+        "latitude":41.1330594,
+        "longitude":-102.986907,
+        "ips":[
+          "10.0.2.2",
+          "10.0.6.2",
+          "10.1.8.2"
+        ]
+      }
+    },
+    "00:00:01:00:01:01/-1":{
+      "basic":{
+        "location":"of:0000000000000005/4",
+        "uiType":"router",
+        "latitude":42.512684,
+        "longitude":-126.4902853,
+        "ips":[
+          "10.0.1.1"
+        ]
+      }
+    },
+    "00:00:01:00:01:02/-1":{
+      "basic":{
+        "location":"of:0000000000000006/4",
+        "uiType":"router",
+        "latitude":41.3851357,
+        "longitude":-126.4902853,
+        "ips":[
+          "10.0.2.1"
+        ]
+      }
+    },
+    "00:00:02:00:01:01/-1":{
+      "basic":{
+        "location":"of:0000000000000007/4",
+        "uiType":"router",
+        "latitude":30.8457906,
+        "longitude":-121.354880,
+        "ips":[
+          "10.0.3.1",
+          "10.0.4.1"
+        ]
+      }
+    },
+    "00:00:03:00:01:01/-1":{
+      "basic":{
+        "location":"of:0000000000000008/4",
+        "uiType":"router",
+        "latitude":40.6976637,
+        "longitude":-69.976271,
+        "ips":[
+          "10.0.5.1",
+          "10.0.6.1"
+        ]
+      }
+    },
+    "00:00:04:00:01:01/-1":{
+      "basic":{
+        "location":"of:0000000000000009/4",
+        "uiType":"router",
+        "latitude":39.368502,
+        "longitude":-69.976271,
+        "ips":[
+          "10.0.7.1"
+        ]
+      }
+    },
+    "00:00:04:00:02:01/-1":{
+      "basic":{
+        "location":"of:000000000000000A/4",
+        "uiType":"router",
+        "latitude":30.8457906,
+        "longitude":-83.3532616,
+        "ips":[
+          "10.0.8.1"
+        ]
+      }
+    }
+  },
+  "ports":{
+    "of:0000000000000005/4":{
+      "interfaces":[
+        {
+          "name":"sw5-4",
+          "ips":[
+            "10.0.1.2/24"
+          ],
+          "mac":"00:00:00:00:00:01"
+        }
+      ]
+    },
+    "of:0000000000000006/4":{
+      "interfaces":[
+        {
+          "name":"sw6-4",
+          "ips":[
+            "10.0.2.2/24"
+          ],
+          "mac":"00:00:00:00:00:03"
+        }
+      ]
+    },
+    "of:0000000000000007/4":{
+      "interfaces":[
+        {
+          "name":"sw7-4",
+          "ips":[
+            "10.0.3.2/24"
+          ],
+          "mac":"00:00:00:00:00:01"
+        },
+        {
+          "name":"sw7-42",
+          "ips":[
+            "10.0.4.2/24"
+          ],
+          "mac":"00:00:00:00:00:02"
+        }
+      ]
+    },
+    "of:0000000000000008/4":{
+      "interfaces":[
+        {
+          "name":"sw8-4",
+          "ips":[
+            "10.0.5.2/24"
+          ],
+          "mac":"00:00:00:00:00:02"
+        },
+        {
+          "name":"sw8-42",
+          "ips":[
+            "10.0.6.2/24"
+          ],
+          "mac":"00:00:00:00:00:03"
+        }
+      ]
+    },
+    "of:0000000000000009/4":{
+      "interfaces":[
+        {
+          "name":"sw9-4",
+          "ips":[
+            "10.0.7.2/24"
+          ],
+          "mac":"00:00:00:00:00:01"
+        }
+      ]
+    },
+    "of:000000000000000A/4":{
+      "interfaces":[
+        {
+          "name":"sw10-4",
+          "ips":[
+            "10.0.8.2/24"
+          ],
+          "mac":"00:00:00:00:00:03"
+        }
+      ]
+    }
+  },
+  "apps":{
+    "org.onosproject.router":{
+      "bgp":{
+        "bgpSpeakers":[
+          {
+            "name":"speaker1",
+            "connectPoint":"of:0000000000000001/7",
+            "peers":[
+              "10.0.1.1",
+              "10.0.3.1",
+              "10.0.7.1"
+            ]
+          },
+          {
+            "connectPoint":"of:0000000000000001/8",
+            "peers":[
+              "10.0.4.1",
+              "10.0.5.1"
+            ]
+          },
+          {
+            "connectPoint":"of:0000000000000001/9",
+            "peers":[
+              "10.0.2.1",
+              "10.0.6.1",
+              "10.0.8.1"
+            ]
+          }
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/tools/test/topos/sdnip.py b/tools/test/topos/sdnip.py
new file mode 100755
index 0000000..7739d58
--- /dev/null
+++ b/tools/test/topos/sdnip.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+
+from mininet.cli import CLI
+from mininet.log import setLogLevel
+from mininet.node import Link, Host
+from mininet.net import Mininet
+from mininet.node import RemoteController, OVSSwitch, OVSBridge
+from mininet.term import makeTerm
+from mininet.topo import Topo
+from functools import partial
+from routinglib import RoutingCli as CLI
+from routinglib import AutonomousSystem, BasicAutonomousSystem, SdnAutonomousSystem
+from routinglib import generateRoutes
+
+class SdnIpTopo( Topo ):
+
+    "SDN-IP topology"
+
+    def __init__( self, onoses, **kwargs ):
+        Topo.__init__( self, **kwargs )
+        coreMesh = []
+
+        # Create first 4 switches
+        for i in range( 1, 5 ):
+            coreMesh.append( self.addSwitch( 's%s' %i ) )
+
+        # create full mesh between middle 4 switches
+        remaining = list( coreMesh )
+        while True:
+            first = remaining[ 0 ]
+            for switch in tuple( remaining ):
+                if switch is not first:
+                    self.addLink( switch, first )
+            remaining.remove( first )
+            if not remaining:
+                break
+
+        # Add more switches
+        s5 = self.addSwitch( 's5', dpid='00:00:00:00:00:00:00:05' )
+        s6 = self.addSwitch( 's6', dpid='00:00:00:00:00:00:00:06' )
+        s7 = self.addSwitch( 's7', dpid='00:00:00:00:00:00:00:07' )
+        s8 = self.addSwitch( 's8', dpid='00:00:00:00:00:00:00:08' )
+        s9 = self.addSwitch( 's9', dpid='00:00:00:00:00:00:00:09' )
+        s10 = self.addSwitch( 's10', dpid='00:00:00:00:00:00:00:0A' )
+
+        # Add more links
+        self.addLink( s5, s6 )
+        self.addLink( s5, s8 )
+        self.addLink( s6, s7 )
+        self.addLink( s8, s9 )
+        self.addLink( s9, s10 )
+        self.addLink( coreMesh[ 0 ], s5 )
+        self.addLink( coreMesh[ 0 ], s6 )
+        self.addLink( coreMesh[ 0 ], s7 )
+        self.addLink( coreMesh[ 1 ], s8 )
+        self.addLink( coreMesh[ 1 ], s9 )
+        self.addLink( coreMesh[ 1 ], s10 )
+        self.addLink( coreMesh[ 2 ], s7 )
+        self.addLink( coreMesh[ 3 ], s10 )
+        
+        # SDN AS        
+        sdnAs = SdnAutonomousSystem(onoses, numBgpSpeakers=3, asNum=65000, externalOnos=True)
+        cs0 = self.addSwitch('cs0', cls=OVSBridge)
+
+        numRoutesPerAs = 32
+
+        # Add external ASes
+        as1 = BasicAutonomousSystem(1, generateRoutes(u'192.168.1.0/24', numRoutesPerAs))
+        AutonomousSystem.addPeering(as1, sdnAs)
+        AutonomousSystem.addPeering(as1, sdnAs, router2=3, intf1=2)
+        as1.addLink(s5)
+        as1.addLink(s6)
+        as1.build(self)
+        
+        as2 = BasicAutonomousSystem(2, generateRoutes(u'192.168.2.0/24', numRoutesPerAs))
+        AutonomousSystem.addPeering(as2, sdnAs)
+        AutonomousSystem.addPeering(as2, sdnAs, router2=2)
+        as2.addLink(s7)
+        as2.build(self)
+        
+        as3 = BasicAutonomousSystem(3, generateRoutes(u'192.168.3.0/24', numRoutesPerAs))
+        AutonomousSystem.addPeering(as3, sdnAs, router2=2)
+        AutonomousSystem.addPeering(as3, sdnAs, router2=3)
+        as3.addLink(s8)
+        as3.build(self)
+        
+        as4 = BasicAutonomousSystem(4, generateRoutes(u'192.168.4.0/24', numRoutesPerAs), numRouters=2)
+        AutonomousSystem.addPeering(as4, sdnAs)
+        AutonomousSystem.addPeering(as4, sdnAs, router1=2, router2=3)
+        as4.addLink(s9)
+        as4.addLink(s10, router=2)
+        as4.build(self)
+
+        # add links between nets
+        #self.addLink( BGP1, coreMesh[ 0 ], port2=10 )
+        #self.addLink( BGP2, coreMesh[ 1 ], port2=10 )
+        #self.addLink( BGP3, coreMesh[ 2 ], port2=10 )
+        
+        sdnAs.build(self, coreMesh[ 0 ], cs0)
+        # TODO multihome the BGP speakers to different switches
+
+topos = { 'sdnip': ( lambda: SdnIpTopo() ) }
+
+if __name__ == '__main__':
+    setLogLevel( 'debug' )
+    from onosnet import run, parse_args
+    run(SdnIpTopo(onoses=parse_args().ipAddrs))
\ No newline at end of file
diff --git a/tools/test/topos/sdnip.recipe b/tools/test/topos/sdnip.recipe
new file mode 100644
index 0000000..59c9dd2
--- /dev/null
+++ b/tools/test/topos/sdnip.recipe
@@ -0,0 +1,4 @@
+# SDN-IP topology recipe
+export OTD=10 # ONOS Topology Devices
+export OTL=38 # ONOS Topology Links
+export OTH=9 # ONOS Topology Hosts
\ No newline at end of file