New configuration scheme and support for CPU limits (RT).
This commit is contained in:
+60
-34
@@ -39,10 +39,12 @@ class BasicIntf( object ):
|
||||
self.name = name
|
||||
self.link = link
|
||||
self.mac, self.ip = None, None
|
||||
# Add to node (and move ourselves if necessary )
|
||||
node.addIntf( self )
|
||||
self.config( **kwargs )
|
||||
|
||||
def cmd( self, *args, **kwargs ):
|
||||
self.node.cmd( *args, **kwargs )
|
||||
return self.node.cmd( *args, **kwargs )
|
||||
|
||||
def ifconfig( self, *args ):
|
||||
"Configure ourselves using ifconfig"
|
||||
@@ -93,22 +95,43 @@ class BasicIntf( object ):
|
||||
return "UP" in self.ifconfig()
|
||||
|
||||
|
||||
# Map of config params to config methods
|
||||
# Perhaps this could be more graceful, but it
|
||||
# is flexible
|
||||
configMap = { 'mac': 'setMAC',
|
||||
'ip': 'setIP',
|
||||
'ifconfig': 'ifconfig' }
|
||||
# The reason why we configure things in this way is so
|
||||
# That the parameters can be listed and documented in
|
||||
# the config method.
|
||||
# Dealing with subclasses and superclasses is slightly
|
||||
# annoying, but at least the information is there!
|
||||
|
||||
def config( self, **params ):
|
||||
"Configure interface based on parameters"
|
||||
self.__dict__.update(**params)
|
||||
for name, value in params.iteritems():
|
||||
method = self.configMap.get( name, None )
|
||||
if method:
|
||||
if type( value ) is str:
|
||||
value = value.split( ',' )
|
||||
method( value )
|
||||
def setParam( self, result, method, **param ):
|
||||
"""Internal method: configure single parameter
|
||||
result: dict of results to update
|
||||
method: config method
|
||||
param: foo=bar (ignore if bar=None)"""
|
||||
name, value = param.items()[ 0 ]
|
||||
if value is None:
|
||||
return
|
||||
if type( value ) is list:
|
||||
result[ name ] = getattr( self, method )( *value )
|
||||
elif type( value ) is dict:
|
||||
result[ name ] = getattr( self, method )( **value )
|
||||
else:
|
||||
result[ name ] = getattr( self, method )( value )
|
||||
|
||||
def config( self, mac=None, ip=None, ifconfig=None,
|
||||
defaultRoute=None, **params):
|
||||
"""Configure Node according to (optional) parameters:
|
||||
mac: MAC address
|
||||
ip: IP address
|
||||
ifconfig: arbitrary interface configuration
|
||||
Subclasses should override this method and call
|
||||
the parent class's config(**params)"""
|
||||
# If we were overriding this method, we would call
|
||||
# the superclass config method here as follows:
|
||||
# r = Parent.config( **params )
|
||||
r = {}
|
||||
self.setParam( r, 'setMAC', mac=mac )
|
||||
self.setParam( r, 'setIP', ip=ip )
|
||||
self.setParam( r, 'ifconfig', ifconfig=ifconfig )
|
||||
return r
|
||||
|
||||
def delete( self ):
|
||||
"Delete interface"
|
||||
@@ -125,10 +148,10 @@ class TCIntf( BasicIntf ):
|
||||
|
||||
def config( self, bw=None, delay=None, loss=0, disable_gro=True,
|
||||
speedup=0, use_hfsc=False, use_tbf=False, enable_ecn=False,
|
||||
enable_red=False, max_queue_size=1000, **kwargs ):
|
||||
enable_red=False, max_queue_size=1000, **params ):
|
||||
"Configure the port and set its properties."
|
||||
|
||||
BasicIntf.config( self, **kwargs)
|
||||
result = BasicIntf.config( self, **params)
|
||||
|
||||
# disable GRO
|
||||
if disable_gro:
|
||||
@@ -153,7 +176,7 @@ class TCIntf( BasicIntf ):
|
||||
delay = '0ms'
|
||||
|
||||
if bw is not None and delay is not None:
|
||||
info( self, '(bw %.2fMbit, delay %s, loss %d%%)\n' %
|
||||
info( self, '(bw %.2fMbit, delay %s, loss %d%%) ' %
|
||||
( bw, delay, loss ) )
|
||||
|
||||
# BL: hmm... what exactly is this???
|
||||
@@ -209,8 +232,11 @@ class TCIntf( BasicIntf ):
|
||||
debug(" *** executing command: %s\n" % c)
|
||||
return self.cmd(c)
|
||||
|
||||
outputs = [ doConfigPort(cmd) for cmd in cmds ]
|
||||
debug( "outputs: %s\n" % outputs )
|
||||
tcoutputs = [ doConfigPort(cmd) for cmd in cmds ]
|
||||
debug( "cmds:", cmds, '\n' )
|
||||
debug( "outputs:", tcoutputs, '\n' )
|
||||
result[ 'tcoutputs'] = tcoutputs
|
||||
return result
|
||||
|
||||
Intf = TCIntf
|
||||
|
||||
@@ -220,14 +246,18 @@ class Link( object ):
|
||||
Other types of links could be tunnels, link emulators, etc.."""
|
||||
|
||||
def __init__( self, node1, node2, port1=None, port2=None, intfName1=None, intfName2=None,
|
||||
intf=Intf, params1={}, params2={} ):
|
||||
intf=Intf, cls1=None, cls2=None, params1={}, params2={} ):
|
||||
"""Create veth link to another node, making two new interfaces.
|
||||
node1: first node
|
||||
node2: second node
|
||||
port1: node1 port number (optional)
|
||||
port2: node2 port number (optional)
|
||||
intf: default interface class/constructor
|
||||
cls1, cls2: optional interface-specific constructors
|
||||
intfName1: node1 interface name (optional)
|
||||
intfName2: node2 interface name (optional)"""
|
||||
intfName2: node2 interface name (optional)
|
||||
params1: parameters for interface 1
|
||||
params2: parameters for interface 2"""
|
||||
# This is a bit awkward; it seems that having everything in
|
||||
# params would be more orthogonal, but being able to specify
|
||||
# in-line arguments is more convenient!
|
||||
@@ -240,11 +270,13 @@ class Link( object ):
|
||||
if not intfName2:
|
||||
intfName2 = self.intfName( node2, port2 )
|
||||
self.makeIntfPair( intfName1, intfName2 )
|
||||
intf1 = intf( name=intfName1, node=node1, link=self, **params1 )
|
||||
intf2 = intf( name=intfName2, node=node2, link=self, **params2 )
|
||||
# Add to nodes
|
||||
node1.addIntf( intf1 )
|
||||
node2.addIntf( intf2 )
|
||||
if not cls1:
|
||||
cls1 = intf
|
||||
if not cls2:
|
||||
cls2 = intf
|
||||
intf1 = cls1( name=intfName1, node=node1, link=self, **params1 )
|
||||
intf2 = cls2( name=intfName2, node=node2, link=self, **params2 )
|
||||
# All we are is dust in the wind, and our two interfaces
|
||||
self.intf1, self.intf2 = intf1, intf2
|
||||
|
||||
@classmethod
|
||||
@@ -267,9 +299,3 @@ class Link( object ):
|
||||
|
||||
def __str__( self ):
|
||||
return '%s<->%s' % ( self.intf1, self.intf2 )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+129
-120
@@ -94,8 +94,7 @@ from time import sleep
|
||||
|
||||
from mininet.cli import CLI
|
||||
from mininet.log import info, error, debug, output
|
||||
from mininet.node import Host, UserSwitch, OVSKernelSwitch, Controller
|
||||
from mininet.node import ControllerParams
|
||||
from mininet.node import Host, OVSKernelSwitch, Controller
|
||||
from mininet.link import Link
|
||||
from mininet.util import quietRun, fixLimits
|
||||
from mininet.util import createLink, macColonHex, ipStr, ipParse
|
||||
@@ -106,7 +105,6 @@ class Mininet( object ):
|
||||
|
||||
def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host,
|
||||
controller=Controller, link=Link,
|
||||
cparams=ControllerParams( '10.0.0.0', 8 ),
|
||||
build=True, xterms=False, cleanup=False,
|
||||
inNamespace=False,
|
||||
autoSetMacs=False, autoStaticArp=False, listenPort=None ):
|
||||
@@ -116,12 +114,12 @@ class Mininet( object ):
|
||||
host: default Host class/constructor
|
||||
controller: default Controller class/constructor
|
||||
link: default Link class/constructor
|
||||
cparams: ControllerParams object
|
||||
ipBase: base IP address for hosts,
|
||||
build: build now from topo?
|
||||
xterms: if build now, spawn xterms?
|
||||
cleanup: if build now, cleanup before creating?
|
||||
inNamespace: spawn switches and controller in net namespaces?
|
||||
autoSetMacs: set MAC addrs from topo?
|
||||
autoSetMacs: set MAC addrs from topo dpid?
|
||||
autoStaticArp: set all-pairs static MAC addrs?
|
||||
listenPort: base listening port to open; will be incremented for
|
||||
each additional switch in the net if inNamespace=False"""
|
||||
@@ -129,7 +127,6 @@ class Mininet( object ):
|
||||
self.host = host
|
||||
self.controller = controller
|
||||
self.link = link
|
||||
self.cparams = cparams
|
||||
self.topo = topo
|
||||
self.inNamespace = inNamespace
|
||||
self.xterms = xterms
|
||||
@@ -141,13 +138,13 @@ class Mininet( object ):
|
||||
self.hosts = []
|
||||
self.switches = []
|
||||
self.controllers = []
|
||||
|
||||
self.nameToNode = {} # name to Node (Host/Switch) objects
|
||||
self.idToNode = {} # dpid to Node (Host/Switch) objects
|
||||
self.dps = 0 # number of created kernel datapaths
|
||||
|
||||
self.terms = [] # list of spawned xterm processes
|
||||
|
||||
init()
|
||||
switch.setup()
|
||||
init() # Initialize Mininet if necessary
|
||||
|
||||
self.built = False
|
||||
if topo and build:
|
||||
@@ -157,17 +154,15 @@ class Mininet( object ):
|
||||
# The specific items for host/switch/etc. should probably be
|
||||
# handled in the node classes rather than here!!
|
||||
|
||||
def addHost( self, name, mac=None, ip=None, host=None, **params ):
|
||||
def addHost( self, name, host=None, **params ):
|
||||
"""Add host.
|
||||
name: name of host to add
|
||||
mac: default MAC address for intf 0
|
||||
ip: default IP address for intf 0
|
||||
host: custom host constructor (optional)
|
||||
params: parameters for host
|
||||
returns: added host"""
|
||||
if not host:
|
||||
host = self.host
|
||||
defaults = { 'defaultMAC': mac, 'defaultIP': ip }
|
||||
defaults.update( params )
|
||||
h = host( name, **defaults)
|
||||
h = host( name, **params)
|
||||
self.hosts.append( h )
|
||||
self.nameToNode[ name ] = h
|
||||
return h
|
||||
@@ -175,19 +170,17 @@ class Mininet( object ):
|
||||
def addSwitch( self, name, switch=None, **params ):
|
||||
"""Add switch.
|
||||
name: name of switch to add
|
||||
switch: custom switch constructor (optional)
|
||||
returns: added switch
|
||||
side effect: increments listenPort and dps ivars."""
|
||||
side effect: increments listenPort ivar ."""
|
||||
defaults = { 'listenPort': self.listenPort,
|
||||
'inNamespace': self.inNamespace }
|
||||
defaults.update( params )
|
||||
if not switch:
|
||||
switch = self.switch
|
||||
if switch != UserSwitch:
|
||||
defaults[ 'dps' ] = self.dps
|
||||
defaults.update( params )
|
||||
sw = self.switch( name, **defaults )
|
||||
if not self.inNamespace and self.listenPort:
|
||||
self.listenPort += 1
|
||||
self.dps += 1
|
||||
self.switches.append( sw )
|
||||
self.nameToNode[ name ] = sw
|
||||
return sw
|
||||
@@ -203,122 +196,81 @@ class Mininet( object ):
|
||||
self.nameToNode[ name ] = controller_new
|
||||
return controller_new
|
||||
|
||||
# Control network support:
|
||||
#
|
||||
# Create an explicit control network. Currently this is only
|
||||
# used by the user datapath configuration.
|
||||
#
|
||||
# Notes:
|
||||
#
|
||||
# 1. If the controller and switches are in the same (e.g. root)
|
||||
# namespace, they can just use the loopback connection.
|
||||
#
|
||||
# 2. If we can get unix domain sockets to work, we can use them
|
||||
# instead of an explicit control network.
|
||||
#
|
||||
# 3. Instead of routing, we could bridge or use 'in-band' control.
|
||||
#
|
||||
# 4. Even if we dispense with this in general, it could still be
|
||||
# useful for people who wish to simulate a separate control
|
||||
# network (since real networks may need one!)
|
||||
#
|
||||
# 5. Basically nobody ever uses this method, so perhaps it should be moved
|
||||
# out of this core class.
|
||||
|
||||
def configureControlNetwork( self ):
|
||||
"Configure control network."
|
||||
self.configureRoutedControlNetwork()
|
||||
|
||||
# We still need to figure out the right way to pass
|
||||
# in the control network location.
|
||||
|
||||
def configureRoutedControlNetwork( self, ip='192.168.123.1',
|
||||
prefixLen=16 ):
|
||||
"""Configure a routed control network on controller and switches.
|
||||
For use with the user datapath only right now."""
|
||||
controller = self.controllers[ 0 ]
|
||||
info( controller.name + ' <->' )
|
||||
cip = ip
|
||||
snum = ipParse( ip )
|
||||
for switch in self.switches:
|
||||
info( ' ' + switch.name )
|
||||
sintf, cintf = createLink( switch, controller )
|
||||
snum += 1
|
||||
while snum & 0xff in [ 0, 255 ]:
|
||||
snum += 1
|
||||
sip = ipStr( snum )
|
||||
controller.setIP( cintf, cip, prefixLen )
|
||||
switch.setIP( sintf, sip, prefixLen )
|
||||
controller.setHostRoute( sip, cintf )
|
||||
switch.setHostRoute( cip, sintf )
|
||||
info( '\n' )
|
||||
info( '*** Testing control network\n' )
|
||||
while not controller.intfIsUp( cintf ):
|
||||
info( '*** Waiting for', cintf, 'to come up\n' )
|
||||
sleep( 1 )
|
||||
for switch in self.switches:
|
||||
while not switch.intfIsUp( sintf ):
|
||||
info( '*** Waiting for', sintf, 'to come up\n' )
|
||||
sleep( 1 )
|
||||
if self.ping( hosts=[ switch, controller ] ) != 0:
|
||||
error( '*** Error: control network test failed\n' )
|
||||
exit( 1 )
|
||||
info( '\n' )
|
||||
|
||||
def configHosts( self ):
|
||||
"Configure a set of hosts."
|
||||
# params were: hosts, ips
|
||||
for host in self.hosts:
|
||||
hintf = host.defaultIntf()
|
||||
host.setIP( host.defaultIP, self.cparams.prefixLen, hintf )
|
||||
host.setDefaultRoute( hintf )
|
||||
host.configDefault( defaultRoute=host.defaultIntf )
|
||||
# You're low priority, dude!
|
||||
quietRun( 'renice +18 -p ' + repr( host.pid ) )
|
||||
# BL: do we want to do this here or not?
|
||||
# May not make sense if we have CPU lmiting...
|
||||
# quietRun( 'renice +18 -p ' + repr( host.pid ) )
|
||||
info( host.name + ' ' )
|
||||
info( '\n' )
|
||||
|
||||
def buildFromTopo( self, topo ):
|
||||
def buildFromTopo( self, topo=None ):
|
||||
"""Build mininet from a topology object
|
||||
At the end of this function, everything should be connected
|
||||
and up."""
|
||||
|
||||
if not topo:
|
||||
topo = self.topo()
|
||||
|
||||
def addNode( prefix, addMethod, nodeId ):
|
||||
"Add a host or a switch."
|
||||
"Add a host or a switch from topo"
|
||||
name = prefix + topo.name( nodeId )
|
||||
# MAC and IP should probably be from nodeInfo...
|
||||
mac = macColonHex( nodeId ) if self.setMacs else None
|
||||
ip = topo.ip( nodeId )
|
||||
ni = topo.nodeInfo( nodeId )
|
||||
node = addMethod( name, cls=ni.cls, mac=mac, ip=ip, **ni.params )
|
||||
# Default IP and MAC addresses
|
||||
defaults = { 'ip': topo.ip( nodeId ) }
|
||||
if self.autoSetMacs:
|
||||
defaults[ 'mac'] = macColonHex( nodeId )
|
||||
defaults.update( ni.params )
|
||||
node = addMethod( name, cls=ni.cls, **defaults )
|
||||
self.idToNode[ nodeId ] = node
|
||||
info( name + ' ' )
|
||||
|
||||
def addLink( srcId, dstId, link=None ):
|
||||
"Add a link from topo"
|
||||
src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
|
||||
srcPort, dstPort = topo.port( srcId, dstId )
|
||||
ei = topo.edgeInfo( srcId, dstId )
|
||||
link = getattr( ei, 'cls', link )
|
||||
params = ei.params
|
||||
if not link:
|
||||
link = self.link
|
||||
info( '(%s, %s) ' % ( src.name, dst.name ) )
|
||||
link( src, dst, srcPort, dstPort, **params )
|
||||
|
||||
# Possibly we should clean up here and/or validate
|
||||
# the topo
|
||||
if self.cleanup:
|
||||
pass
|
||||
|
||||
info( '*** Adding controller\n' )
|
||||
self.addController( 'c0' )
|
||||
info( '*** Creating network\n' )
|
||||
|
||||
if not self.controllers:
|
||||
# Add a default controller
|
||||
info( '*** Adding controller\n' )
|
||||
self.addController( 'c0' )
|
||||
|
||||
info( '*** Adding hosts:\n' )
|
||||
for hostId in sorted( topo.hosts() ):
|
||||
addNode( 'h', self.addHost, hostId )
|
||||
|
||||
info( '\n*** Adding switches:\n' )
|
||||
for switchId in sorted( topo.switches() ):
|
||||
addNode( 's', self.addSwitch, switchId)
|
||||
addNode( 's', self.addSwitch, switchId )
|
||||
|
||||
info( '\n*** Adding links:\n' )
|
||||
for srcId, dstId in sorted( topo.edges() ):
|
||||
src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
|
||||
srcPort, dstPort = topo.port( srcId, dstId )
|
||||
ei = topo.edgeInfo( srcId, dstId )
|
||||
link, params = ei.cls, ei.params
|
||||
if not link:
|
||||
link = self.link
|
||||
link( src, dst, srcPort, dstPort, **params )
|
||||
info( '(%s, %s) ' % ( src.name, dst.name ) )
|
||||
addLink( srcId, dstId )
|
||||
|
||||
info( '\n' )
|
||||
|
||||
|
||||
def configureControlNetwork( self ):
|
||||
error( "configureControlNetwork: override in subclass, or use"
|
||||
"MininetWithControlNet class" )
|
||||
|
||||
def build( self ):
|
||||
"Build mininet."
|
||||
if self.topo:
|
||||
@@ -330,8 +282,6 @@ class Mininet( object ):
|
||||
self.configHosts()
|
||||
if self.xterms:
|
||||
self.startTerms()
|
||||
if self.autoSetMacs:
|
||||
self.setMacs()
|
||||
if self.autoStaticArp:
|
||||
self.staticArp()
|
||||
self.built = True
|
||||
@@ -346,17 +296,10 @@ class Mininet( object ):
|
||||
|
||||
def stopXterms( self ):
|
||||
"Kill each xterm."
|
||||
# Kill xterms
|
||||
for term in self.terms:
|
||||
os.kill( term.pid, signal.SIGKILL )
|
||||
cleanUpScreens()
|
||||
|
||||
def setMacs( self ):
|
||||
"""Set MAC addrs to correspond to default MACs on hosts.
|
||||
Assume that the host only has one interface."""
|
||||
for host in self.hosts:
|
||||
host.setMAC( host.intfs[ 0 ], host.defaultMAC )
|
||||
|
||||
def staticArp( self ):
|
||||
"Add all-pairs ARP entries to remove the need to handle broadcast."
|
||||
for src in self.hosts:
|
||||
@@ -384,18 +327,19 @@ class Mininet( object ):
|
||||
self.stopXterms()
|
||||
info( '*** Stopping %i hosts\n' % len( self.hosts ) )
|
||||
for host in self.hosts:
|
||||
info( '%s ' % host.name )
|
||||
info( host.name + ' ' )
|
||||
host.terminate()
|
||||
info( '\n' )
|
||||
info( '*** Stopping %i switches\n' % len( self.switches ) )
|
||||
for switch in self.switches:
|
||||
info( switch.name )
|
||||
info( switch.name + ' ' )
|
||||
switch.stop()
|
||||
info( '\n' )
|
||||
info( '*** Stopping %i controllers\n' % len( self.controllers ) )
|
||||
for controller in self.controllers:
|
||||
info( controller.name + ' ' )
|
||||
controller.stop()
|
||||
info( '*** Done\n' )
|
||||
info( '\n*** Done\n' )
|
||||
|
||||
def run( self, test, *args, **kwargs ):
|
||||
"Perform a complete start/test/stop cycle."
|
||||
@@ -429,6 +373,9 @@ class Mininet( object ):
|
||||
if not ready and timeoutms >= 0:
|
||||
yield None, None
|
||||
|
||||
# XXX These test methods should be moved out of this class.
|
||||
# Probably we should create a tests.py for them
|
||||
|
||||
@staticmethod
|
||||
def _parsePing( pingOutput ):
|
||||
"Parse ping output and return packets sent, received."
|
||||
@@ -543,6 +490,8 @@ class Mininet( object ):
|
||||
output( '*** Results: %s\n' % result )
|
||||
return result
|
||||
|
||||
# BL: I think this can be rewritten now that we have
|
||||
# a real link class.
|
||||
def configLinkStatus( self, src, dst, status ):
|
||||
"""Change status of src <-> dst links.
|
||||
src: node name
|
||||
@@ -573,6 +522,70 @@ class Mininet( object ):
|
||||
return result
|
||||
|
||||
|
||||
class MininetWithControlNet( Mininet ):
|
||||
|
||||
"""Control network support:
|
||||
|
||||
Create an explicit control network. Currently this is only
|
||||
used/usable with the user datapath.
|
||||
|
||||
Notes:
|
||||
|
||||
1. If the controller and switches are in the same (e.g. root)
|
||||
namespace, they can just use the loopback connection.
|
||||
|
||||
2. If we can get unix domain sockets to work, we can use them
|
||||
instead of an explicit control network.
|
||||
|
||||
3. Instead of routing, we could bridge or use 'in-band' control.
|
||||
|
||||
4. Even if we dispense with this in general, it could still be
|
||||
useful for people who wish to simulate a separate control
|
||||
network (since real networks may need one!)
|
||||
|
||||
5. Basically nobody ever used this code, so it has been moved
|
||||
into its own class."""
|
||||
|
||||
def configureControlNetwork( self ):
|
||||
"Configure control network."
|
||||
self.configureRoutedControlNetwork()
|
||||
|
||||
# We still need to figure out the right way to pass
|
||||
# in the control network location.
|
||||
|
||||
def configureRoutedControlNetwork( self, ip='192.168.123.1',
|
||||
prefixLen=16 ):
|
||||
"""Configure a routed control network on controller and switches.
|
||||
For use with the user datapath only right now."""
|
||||
controller = self.controllers[ 0 ]
|
||||
info( controller.name + ' <->' )
|
||||
cip = ip
|
||||
snum = ipParse( ip )
|
||||
for switch in self.switches:
|
||||
info( ' ' + switch.name )
|
||||
sintf, cintf = createLink( switch, controller )
|
||||
snum += 1
|
||||
while snum & 0xff in [ 0, 255 ]:
|
||||
snum += 1
|
||||
sip = ipStr( snum )
|
||||
controller.setIP( cintf, cip, prefixLen )
|
||||
switch.setIP( sintf, sip, prefixLen )
|
||||
controller.setHostRoute( sip, cintf )
|
||||
switch.setHostRoute( cip, sintf )
|
||||
info( '\n' )
|
||||
info( '*** Testing control network\n' )
|
||||
while not controller.intfIsUp( cintf ):
|
||||
info( '*** Waiting for', cintf, 'to come up\n' )
|
||||
sleep( 1 )
|
||||
for switch in self.switches:
|
||||
while not switch.intfIsUp( sintf ):
|
||||
info( '*** Waiting for', sintf, 'to come up\n' )
|
||||
sleep( 1 )
|
||||
if self.ping( hosts=[ switch, controller ] ) != 0:
|
||||
error( '*** Error: control network test failed\n' )
|
||||
exit( 1 )
|
||||
info( '\n' )
|
||||
|
||||
# pylint thinks inited is unused
|
||||
# pylint: disable-msg=W0612
|
||||
|
||||
@@ -585,10 +598,6 @@ def init():
|
||||
# Perhaps we should do so automatically!
|
||||
print "*** Mininet must run as root."
|
||||
exit( 1 )
|
||||
# If which produces no output, then mnexec is not in the path.
|
||||
# May want to loosen this to handle mnexec in the current dir.
|
||||
if not quietRun( 'which mnexec' ):
|
||||
raise Exception( "Could not find mnexec - check $PATH" )
|
||||
fixLimits()
|
||||
init.inited = True
|
||||
|
||||
|
||||
+245
-121
@@ -48,7 +48,8 @@ import select
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
from mininet.log import info, error, debug
|
||||
from mininet.util import quietRun, errRun, moveIntf, isShellBuiltin
|
||||
from mininet.util import quietRun, errRun, errFail, moveIntf, isShellBuiltin
|
||||
from mininet.util import numCores
|
||||
from mininet.moduledeps import moduleDeps, pathCheck, OVS_KMOD, OF_KMOD, TUN
|
||||
from mininet.link import Link
|
||||
|
||||
@@ -58,21 +59,74 @@ class Node( object ):
|
||||
"""A virtual network node is simply a shell in a network namespace.
|
||||
We communicate with it using pipes."""
|
||||
|
||||
portBase = 0 # Nodes always start with eth0/port0, even in OF 1.0
|
||||
|
||||
def __init__( self, name, inNamespace=True, **params ):
|
||||
"""name: name of node
|
||||
inNamespace: in network namespace?
|
||||
params: Node parameters (see config() for details)"""
|
||||
|
||||
# Make sure class actually works
|
||||
self.checkSetup()
|
||||
|
||||
self.name = name
|
||||
self.inNamespace = inNamespace
|
||||
|
||||
# Stash configuration parameters for future reference
|
||||
self.params = params
|
||||
|
||||
self.intfs = {} # dict of port numbers to interfaces
|
||||
self.ports = {} # dict of interfaces to port numbers
|
||||
# replace with Port objects, eventually ?
|
||||
self.nameToIntf = {} # dict of interface names to Intfs
|
||||
|
||||
# Start command interpreter shell
|
||||
self.shell = None
|
||||
self.startShell()
|
||||
|
||||
# File descriptor to node mapping support
|
||||
# Class variables and methods
|
||||
|
||||
inToNode = {} # mapping of input fds to nodes
|
||||
outToNode = {} # mapping of output fds to nodes
|
||||
|
||||
portBase = 0 # Nodes always start with eth0/port0, even in OF 1.0
|
||||
@classmethod
|
||||
def fdToNode( cls, fd ):
|
||||
"""Return node corresponding to given file descriptor.
|
||||
fd: file descriptor
|
||||
returns: node"""
|
||||
node = cls.outToNode.get( fd )
|
||||
return node or cls.inToNode.get( fd )
|
||||
|
||||
def __init__( self, name, inNamespace=True,
|
||||
defaultMAC=None, defaultIP=None, **kwargs ):
|
||||
"""name: name of node
|
||||
inNamespace: in network namespace?
|
||||
defaultMAC: default MAC address for intf 0
|
||||
defaultIP: default IP address for intf 0"""
|
||||
self.name = name
|
||||
self.inNamespace = inNamespace
|
||||
self.defaultIP = defaultIP
|
||||
self.defaultMAC = defaultMAC
|
||||
# Automatic class setup support
|
||||
|
||||
isSetup = False;
|
||||
|
||||
@classmethod
|
||||
def checkSetup( cls ):
|
||||
"Make sure our class and superclasses are set up"
|
||||
while cls and not getattr( cls, 'isSetup', True ):
|
||||
cls.setup()
|
||||
cls.isSetup = True
|
||||
# Make pylint happy
|
||||
cls = getattr( type( cls ), '__base__', None )
|
||||
|
||||
@classmethod
|
||||
def setup( cls ):
|
||||
"Make sure our class dependencies are available"
|
||||
pathCheck( 'mnexec', 'ifconfig', moduleName='Mininet')
|
||||
|
||||
def cleanup( self ):
|
||||
"Help python collect its garbage."
|
||||
self.shell = None
|
||||
|
||||
# Command support via shell process in namespace
|
||||
|
||||
def startShell( self ):
|
||||
"Start a shell process for running commands"
|
||||
if self.shell:
|
||||
error( "%s: shell is already running" )
|
||||
return
|
||||
opts = '-cdp'
|
||||
if self.inNamespace:
|
||||
opts += 'n'
|
||||
@@ -89,31 +143,12 @@ class Node( object ):
|
||||
# using select.poll()
|
||||
self.outToNode[ self.stdout.fileno() ] = self
|
||||
self.inToNode[ self.stdin.fileno() ] = self
|
||||
self.intfs = {} # dict of port numbers to interfaces
|
||||
self.ports = {} # dict of interfaces to port numbers
|
||||
# replace with Port objects, eventually ?
|
||||
self.nameToIntf = {} # dict of interface names to Intfs
|
||||
self.execed = False
|
||||
self.lastCmd = None
|
||||
self.lastPid = None
|
||||
self.readbuf = ''
|
||||
self.waiting = False
|
||||
# Stash additional information as desired
|
||||
self.args = kwargs
|
||||
|
||||
@classmethod
|
||||
def fdToNode( cls, fd ):
|
||||
"""Return node corresponding to given file descriptor.
|
||||
fd: file descriptor
|
||||
returns: node"""
|
||||
node = Node.outToNode.get( fd )
|
||||
return node or Node.inToNode.get( fd )
|
||||
|
||||
def cleanup( self ):
|
||||
"Help python collect its garbage."
|
||||
self.shell = None
|
||||
|
||||
# Subshell I/O, commands and control
|
||||
def read( self, bytes=1024 ):
|
||||
"""Buffered read from node, non-blocking.
|
||||
bytes: maximum number of bytes to return"""
|
||||
@@ -267,10 +302,10 @@ class Node( object ):
|
||||
self.intfs[ port ] = intf
|
||||
self.ports[ intf ] = port
|
||||
self.nameToIntf[ intf.name ] = intf
|
||||
info( '\n' )
|
||||
info( 'added intf %s:%d to node %s\n' % ( intf,port, self.name ) )
|
||||
debug( '\n' )
|
||||
debug( 'added intf %s:%d to node %s\n' % ( intf,port, self.name ) )
|
||||
if self.inNamespace:
|
||||
info( 'moving', intf, 'into namespace for', self.name, '\n' )
|
||||
debug( 'moving', intf, 'into namespace for', self.name, '\n' )
|
||||
moveIntf( intf.name, self )
|
||||
|
||||
def defaultIntf( self ):
|
||||
@@ -326,13 +361,15 @@ class Node( object ):
|
||||
intf: string, interface name"""
|
||||
return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
|
||||
|
||||
def setDefaultRoute( self, intf ):
|
||||
def setDefaultRoute( self, intf=None ):
|
||||
"""Set the default route to go through intf.
|
||||
intf: string, interface name"""
|
||||
if not intf:
|
||||
intf = self.defaultIntf()
|
||||
self.cmd( 'ip route flush root 0/0' )
|
||||
return self.cmd( 'route add default %s' % intf )
|
||||
|
||||
# Convenience methods
|
||||
# Convenience and configuration methods
|
||||
|
||||
def setMAC( self, mac, intf=''):
|
||||
"""Set the MAC address for an interface.
|
||||
@@ -361,6 +398,49 @@ class Node( object ):
|
||||
"Check if an interface is up."
|
||||
return self.intf( intf ).isUp()
|
||||
|
||||
# The reason why we configure things in this way is so
|
||||
# That the parameters can be listed and documented in
|
||||
# the config method.
|
||||
# Dealing with subclasses and superclasses is slightly
|
||||
# annoying, but at least the information is there!
|
||||
|
||||
def setParam( self, results, method, **param ):
|
||||
"""Internal method: configure single parameter"""
|
||||
name, value = param.items()[ 0 ]
|
||||
f = getattr( self, method, None )
|
||||
if not value or not f:
|
||||
return
|
||||
if type( value ) is list:
|
||||
result = f( *value )
|
||||
elif type( value ) is dict:
|
||||
result = f( **value )
|
||||
else:
|
||||
result = f( value )
|
||||
results[ name ] = result
|
||||
|
||||
def config( self, mac=None, ip=None, ifconfig=None,
|
||||
defaultRoute=None, **params):
|
||||
"""Configure Node according to (optional) parameters:
|
||||
mac: MAC address for default interface
|
||||
ip: IP address for default interface
|
||||
ifconfig: arbitrary interface configuration
|
||||
Subclasses should override this method and call
|
||||
the parent class's config(**params)"""
|
||||
# If we were overriding this method, we would call
|
||||
# the superclass config method here as follows:
|
||||
# r = Parent.config( **params )
|
||||
r = {}
|
||||
self.setParam( r, 'setMAC', mac=mac )
|
||||
self.setParam( r, 'setIP', ip=ip )
|
||||
self.setParam( r, 'ifconfig', ifconfig=ifconfig )
|
||||
self.setParam( r, 'defaultRoute', defaultRoute=defaultRoute )
|
||||
return r
|
||||
|
||||
def configDefault( self, **moreParams ):
|
||||
"Configure with default parameters"
|
||||
self.params.update( moreParams )
|
||||
self.config( **self.params )
|
||||
|
||||
# This is here for backward compatibility
|
||||
def linkTo( self, node, link=Link ):
|
||||
"""(Deprecated) Link to another node
|
||||
@@ -382,9 +462,94 @@ class Node( object ):
|
||||
self.name, self.IP(), ','.join( self.intfNames() ), self.pid )
|
||||
|
||||
|
||||
class Host( Node ):
|
||||
"A host is simply a Node."
|
||||
class CPULimitedHost( Node ):
|
||||
|
||||
"CPU limited host"
|
||||
|
||||
def __init__( self, *args, **kwargs ):
|
||||
Node.__init__( self, *args, **kwargs )
|
||||
# Create a cgroup and move shell into it
|
||||
cgroup = 'cpu,cpuacct:/' + self.name
|
||||
errFail( 'cgcreate -g ' + cgroup )
|
||||
errFail( 'cgclassify -g %s %s' % ( cgroup, self.pid ) )
|
||||
self.sched = 'rt'
|
||||
self.period_us = 10000
|
||||
self.rtset = False
|
||||
|
||||
def cgroupSet( self, param, value, resource='cpu' ):
|
||||
"Set a cgroup parameter and return its value"
|
||||
cmd = 'cgset -r %s.%s=%s /%s' % (
|
||||
resource, param, value, self.name )
|
||||
return quietRun( cmd )
|
||||
|
||||
def cgroupGet( self, param, resource='cpu' ):
|
||||
cmd = 'cgget -r %s.%s /%s' % (
|
||||
resource, param, self.name )
|
||||
return quietRun( cmd ).split()[ -1 ]
|
||||
|
||||
def chrt( self, prio=20 ):
|
||||
"Set RT scheduling priority"
|
||||
quietRun( 'chrt -p %s %s' % ( prio, self.pid ) )
|
||||
result = quietRun( 'chrt -p %s' % self.pid )
|
||||
firstline = result.split( '\n' )[ 0 ]
|
||||
lastword = firstline.split( ' ' )[ -1 ]
|
||||
return lastword
|
||||
|
||||
def setCPUFrac( self, f=-1 ):
|
||||
"Set overall CPU fraction for this host"
|
||||
if ( f < 0 or f is None):
|
||||
# Reset to unlimited
|
||||
f = -1
|
||||
# Set new period and quota
|
||||
pstr, qstr = 'rt_period_us', 'rt_runtime_us'
|
||||
quota = int( self.period_us * f * numCores() )
|
||||
self.cgroupSet( pstr, self.period_us )
|
||||
nquota = int ( self.cgroupGet( qstr ) )
|
||||
self.cgroupSet( qstr, quota )
|
||||
nperiod = int( self.cgroupGet( pstr ) )
|
||||
# Set RT priority
|
||||
nchrt = self.chrt( prio=20 )
|
||||
# Check to make sure it worked
|
||||
if 'SCHED_RR' not in nchrt:
|
||||
error( '*** error: could not assign SCHED_RR to %s\n' % self.name )
|
||||
if nperiod != self.period_us:
|
||||
error( '*** error: period is %s rather than %s\n' % (
|
||||
nperiod, self.period_us ) )
|
||||
if nquota != quota:
|
||||
error( '*** error: quota is %s rather than %s\n' % (
|
||||
nquota, quota ) )
|
||||
|
||||
def config( self, cpu=None, **params ):
|
||||
"""cpu: desired overall system CPU fraction
|
||||
params: parameters for Node.config()"""
|
||||
r = Node.config( self, **params )
|
||||
self.setParam( r, 'setCPUFrac', cpu=cpu )
|
||||
return r
|
||||
|
||||
Host = CPULimitedHost
|
||||
|
||||
|
||||
# Some important things to note:
|
||||
#
|
||||
# The "IP" address which we assign to the switch is not
|
||||
# an "IP address for the switch" in the sense of IP routing.
|
||||
# Rather, it is the IP address for a control interface if
|
||||
# (and only if) you happen to be running the switch in a
|
||||
# namespace, which is something we currently don't support
|
||||
# for OVS!
|
||||
#
|
||||
# In general, you NEVER want to attempt to use Linux's
|
||||
# network stack (i.e. ifconfig) to "assign" an IP address or
|
||||
# MAC address to a switch data port. Instead, you "assign"
|
||||
# the IP and MAC addresses in the controller by specifying
|
||||
# packets that you want to receive or send. The "MAC" address
|
||||
# reported by ifconfig for a switch data port is essentially
|
||||
# meaningless.
|
||||
#
|
||||
# So, I'm tyring changing the API to make it
|
||||
# impossible to try this, since it will not work, since nobody
|
||||
# ever makes separate control networks in Mininet, and indeed
|
||||
# we don't even support running OVS in a namespace.
|
||||
|
||||
class Switch( Node ):
|
||||
"""A Switch is a Node that is running (or has execed?)
|
||||
@@ -392,19 +557,31 @@ class Switch( Node ):
|
||||
|
||||
portBase = SWITCH_PORT_BASE # 0 for OF < 1.0, 1 for OF >= 1.0
|
||||
|
||||
def __init__( self, name, opts='', listenPort=None, **kwargs):
|
||||
Node.__init__( self, name, **kwargs )
|
||||
def __init__( self, name, dpid=None, opts='', listenPort=None, **params):
|
||||
"""dpid: dpid for switch (or None for default)
|
||||
opts: additional switch options
|
||||
listenPort: port to listen on for dpctl connections"""
|
||||
Node.__init__( self, name, **params )
|
||||
self.dpid = dpid if dpid else self.defaultDpid()
|
||||
self.opts = opts
|
||||
self.listenPort = listenPort
|
||||
if self.listenPort:
|
||||
self.opts += ' --listen=ptcp:%i ' % self.listenPort
|
||||
self.controlIntf = None
|
||||
|
||||
def defaultDpid( self ):
|
||||
"Derive dpid from switch name, s1 -> 1"
|
||||
dpid = int( re.findall( '\d+', self.name )[ 0 ] )
|
||||
dpid = hex( dpid )[ 2: ]
|
||||
dpid = '0' * ( 12 - len( dpid ) ) + dpid
|
||||
return dpid
|
||||
|
||||
def defaultIntf( self ):
|
||||
"Return interface for HIGHEST port"
|
||||
ports = self.intfs.keys()
|
||||
if ports:
|
||||
intf = self.intfs[ max( ports ) ]
|
||||
return intf
|
||||
"Return control interface, if any"
|
||||
if not self.inNamespace:
|
||||
error( "error: tried to access control interface of "
|
||||
" switch %s in root namespace" % self.name )
|
||||
return self.controlIntf
|
||||
|
||||
def sendCmd( self, *cmd, **kwargs ):
|
||||
"""Send command to Node.
|
||||
@@ -440,15 +617,13 @@ class UserSwitch( Switch ):
|
||||
ofdlog = '/tmp/' + self.name + '-ofd.log'
|
||||
ofplog = '/tmp/' + self.name + '-ofp.log'
|
||||
self.cmd( 'ifconfig lo up' )
|
||||
mac_str = ''
|
||||
if self.defaultMAC:
|
||||
# ofdatapath expects a string of hex digits with no colons.
|
||||
mac_str = ' -d ' + ''.join( self.defaultMAC.split( ':' ) )
|
||||
intfs = sorted( self.intfs.values() )
|
||||
ports = sorted( self.ports.values() )
|
||||
intfs = [ str( self.intfs[ p ] ) for p in ports ]
|
||||
if self.inNamespace:
|
||||
intfs = intfs[ :-1 ]
|
||||
self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
|
||||
' punix:/tmp/' + self.name + mac_str + ' --no-slicing ' +
|
||||
' punix:/tmp/' + self.name + ' -d ' + self.dpid +
|
||||
' --no-slicing ' +
|
||||
' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
|
||||
self.cmd( 'ofprotocol unix:/tmp/' + self.name +
|
||||
' tcp:%s:%d' % ( controller.IP(), controller.port ) +
|
||||
@@ -461,61 +636,6 @@ class UserSwitch( Switch ):
|
||||
self.cmd( 'kill %ofprotocol' )
|
||||
self.deleteIntfs()
|
||||
|
||||
class KernelSwitch( Switch ):
|
||||
"""Kernel-space switch.
|
||||
Currently only works in root namespace."""
|
||||
|
||||
def __init__( self, name, dp=None, **kwargs ):
|
||||
"""Init.
|
||||
name: name for switch
|
||||
dp: netlink id (0, 1, 2, ...)
|
||||
defaultMAC: default MAC as string; random value if None"""
|
||||
Switch.__init__( self, name, **kwargs )
|
||||
self.dp = 'nl:%i' % dp
|
||||
self.intf = 'of%i' % dp
|
||||
if self.inNamespace:
|
||||
error( "KernelSwitch currently only works"
|
||||
" in the root namespace." )
|
||||
exit( 1 )
|
||||
|
||||
@staticmethod
|
||||
def setup():
|
||||
"Ensure any dependencies are loaded; if not, try to load them."
|
||||
pathCheck( 'ofprotocol',
|
||||
moduleName='the OpenFlow reference kernel switch'
|
||||
' (openflow.org) (NOTE: not available in OpenFlow 1.0!)' )
|
||||
moduleDeps( subtract=OVS_KMOD, add=OF_KMOD )
|
||||
|
||||
def start( self, controllers ):
|
||||
"Start up reference kernel datapath."
|
||||
ofplog = '/tmp/' + self.name + '-ofp.log'
|
||||
quietRun( 'ifconfig lo up' )
|
||||
# Delete local datapath if it exists;
|
||||
# then create a new one monitoring the given interfaces
|
||||
quietRun( 'dpctl deldp ' + self.dp )
|
||||
self.cmd( 'dpctl adddp ' + self.dp )
|
||||
if self.defaultMAC:
|
||||
self.cmd( 'ifconfig', self.intf, 'hw', 'ether', self.defaultMAC )
|
||||
ports = sorted( self.ports.values() )
|
||||
if len( ports ) != ports[ -1 ] + 1 - self.portBase:
|
||||
raise Exception( 'only contiguous, zero-indexed port ranges'
|
||||
'supported: %s' % ports )
|
||||
intfs = [ self.intfs[ port ] for port in ports ]
|
||||
self.cmd( 'dpctl', 'addif', self.dp, ' '.join( intfs ) )
|
||||
# Run protocol daemon
|
||||
controller = controllers[ 0 ]
|
||||
self.cmd( 'ofprotocol ' + self.dp +
|
||||
' tcp:%s:%d' % ( controller.IP(), controller.port ) +
|
||||
' --fail=closed ' + self.opts +
|
||||
' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
|
||||
self.execed = False
|
||||
|
||||
def stop( self ):
|
||||
"Terminate kernel datapath."
|
||||
quietRun( 'dpctl deldp ' + self.dp )
|
||||
self.cmd( 'kill %ofprotocol' )
|
||||
self.deleteIntfs()
|
||||
|
||||
|
||||
class OVSLegacyKernelSwitch( Switch ):
|
||||
"""Open VSwitch legacy kernel-space switch using ovs-openflowd.
|
||||
@@ -549,12 +669,6 @@ class OVSLegacyKernelSwitch( Switch ):
|
||||
# then create a new one monitoring the given interfaces
|
||||
quietRun( 'ovs-dpctl del-dp ' + self.dp )
|
||||
self.cmd( 'ovs-dpctl add-dp ' + self.dp )
|
||||
mac_str = ''
|
||||
if self.defaultMAC:
|
||||
# ovs-openflowd expects a string of exactly 16 hex digits with no
|
||||
# colons.
|
||||
mac_str = ' --datapath-id=0000' + \
|
||||
''.join( self.defaultMAC.split( ':' ) ) + ' '
|
||||
ports = sorted( self.ports.values() )
|
||||
if len( ports ) != ports[ -1 ] + 1 - self.portBase:
|
||||
raise Exception( 'only contiguous, one-indexed port ranges '
|
||||
@@ -565,7 +679,8 @@ class OVSLegacyKernelSwitch( Switch ):
|
||||
controller = controllers[ 0 ]
|
||||
self.cmd( 'ovs-openflowd ' + self.dp +
|
||||
' tcp:%s:%d' % ( controller.IP(), controller.port ) +
|
||||
' --fail=secure ' + self.opts + mac_str +
|
||||
' --fail=secure ' + self.opts +
|
||||
' --datapath-id=' + self.dpid +
|
||||
' 1>' + ofplog + ' 2>' + ofplog + '&' )
|
||||
self.execed = False
|
||||
|
||||
@@ -579,13 +694,17 @@ class OVSLegacyKernelSwitch( Switch ):
|
||||
class OVSSwitch( Switch ):
|
||||
"Open vSwitch switch. Depends on ovs-vsctl."
|
||||
|
||||
def __init__( self, name, dp=None, **kwargs ):
|
||||
def __init__( self, name, **params ):
|
||||
"""Init.
|
||||
name: name for switch
|
||||
defaultMAC: default MAC as unsigned int; random value if None"""
|
||||
Switch.__init__( self, name, **kwargs )
|
||||
Switch.__init__( self, name, **params )
|
||||
# self.dp is the text name for the datapath that
|
||||
# we use for ovs-vsctl. This is different from the
|
||||
# dpid, which is a 64-bit numerical value used by
|
||||
# the openflow protocol.
|
||||
self.dp = name
|
||||
|
||||
|
||||
@staticmethod
|
||||
def setup():
|
||||
"Make sure Open vSwitch is installed and working"
|
||||
@@ -609,7 +728,6 @@ class OVSSwitch( Switch ):
|
||||
self.cmd( 'ovs-vsctl del-br ', self.dp )
|
||||
self.cmd( 'ovs-vsctl add-br', self.dp )
|
||||
self.cmd( 'ovs-vsctl set-fail-mode', self.dp, 'secure' )
|
||||
# Add ports
|
||||
ports = sorted( self.ports.values() )
|
||||
intfs = [ self.intfs[ port ] for port in ports ]
|
||||
# XXX: Ugly check - we should probably fix this!
|
||||
@@ -629,19 +747,21 @@ class OVSSwitch( Switch ):
|
||||
|
||||
OVSKernelSwitch = OVSSwitch
|
||||
|
||||
|
||||
class Controller( Node ):
|
||||
"""A Controller is a Node that is running (or has execed?) an
|
||||
OpenFlow controller."""
|
||||
|
||||
def __init__( self, name, inNamespace=False, command='controller',
|
||||
cargs='-v ptcp:%d', cdir=None, defaultIP="127.0.0.1",
|
||||
port=6633 ):
|
||||
cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1",
|
||||
port=6633, **params ):
|
||||
self.command = command
|
||||
self.cargs = cargs
|
||||
self.cdir = cdir
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
Node.__init__( self, name, inNamespace=inNamespace,
|
||||
defaultIP=defaultIP )
|
||||
ip=ip, **params )
|
||||
|
||||
def start( self ):
|
||||
"""Start <controller> <args> on controller.
|
||||
@@ -664,9 +784,13 @@ class Controller( Node ):
|
||||
if self.intfs:
|
||||
ip = Node.IP( self, intf )
|
||||
else:
|
||||
ip = self.defaultIP
|
||||
ip = self.ip
|
||||
return ip
|
||||
|
||||
|
||||
# BL: This really seems to be poorly specified,
|
||||
# so it's going to go away!
|
||||
|
||||
class ControllerParams( object ):
|
||||
"Container for controller IP parameters."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user