547 lines
20 KiB
Plaintext
547 lines
20 KiB
Plaintext
$OpenBSD: patch-mininet_basenode_py,v 1.3 2017/12/07 06:33:40 akoshibe Exp $
|
|
OS-agnostic parts of node.py.
|
|
Turn on PID tracking by default since it's used to reliably stop
|
|
processes.
|
|
Index: mininet/basenode.py
|
|
--- mininet/basenode.py.orig
|
|
+++ mininet/basenode.py
|
|
@@ -0,0 +1,538 @@
|
|
+"""
|
|
+The base node object that other network nodes are based upon. A node implements
|
|
+the lightweight virtualization needed to implement hosts and switches.
|
|
+"""
|
|
+import os
|
|
+import pty
|
|
+import re
|
|
+import select
|
|
+from subprocess import Popen, PIPE
|
|
+
|
|
+plat = os.uname()[ 0 ]
|
|
+if plat == 'FreeBSD':
|
|
+ from mininet.freebsd.util import LO, moveIntf
|
|
+elif plat == 'Linux':
|
|
+ from mininet.linux.util import LO, moveIntf
|
|
+else:
|
|
+ from mininet.openbsd.util import LO, moveIntf
|
|
+
|
|
+from mininet.log import info, error, warn, debug
|
|
+from mininet.util import quietRun
|
|
+from mininet.moduledeps import pathCheck
|
|
+from mininet.link import Link
|
|
+from re import findall
|
|
+
|
|
+
|
|
+class BaseNode( 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?
|
|
+ privateDirs: list of private directory strings or tuples
|
|
+ params: Node parameters (see config() for details)"""
|
|
+
|
|
+ # Make sure class actually works
|
|
+ self.checkSetup()
|
|
+
|
|
+ self.name = params.get( 'name', name )
|
|
+ self.privateDirs = params.get( 'privateDirs', [] )
|
|
+ self.inNamespace = params.get( '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
|
|
+
|
|
+ # Make pylint happy
|
|
+ ( self.shell, self.execed, self.pid, self.stdin, self.stdout,
|
|
+ self.lastPid, self.lastCmd, self.pollOut ) = (
|
|
+ None, None, None, None, None, None, None, None )
|
|
+ self.waiting = False
|
|
+ self.readbuf = ''
|
|
+
|
|
+ # Start command interpreter shell
|
|
+ self.startShell()
|
|
+ self.mountPrivateDirs()
|
|
+
|
|
+ # File descriptor to node mapping support
|
|
+ # Class variables and methods
|
|
+
|
|
+ inToNode = {} # mapping of input fds to nodes
|
|
+ outToNode = {} # mapping of output fds to nodes
|
|
+
|
|
+ @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 getShell( self, master, slave, mnopts=None ):
|
|
+ # OS-specific virtualization method - overriden in system nodes
|
|
+ pass
|
|
+
|
|
+ def isShellBuiltin( self, cmd ):
|
|
+ # Shell-specific check - overridden in system nodes
|
|
+ pass
|
|
+
|
|
+ # Command support via shell process in namespace
|
|
+ def startShell( self, mnopts=None ):
|
|
+ "Start a shell process for running commands"
|
|
+ if self.shell:
|
|
+ error( "%s: shell is already running\n" % self.name )
|
|
+ return
|
|
+
|
|
+ # Spawn a shell subprocess in a pseudo-tty, to disable buffering
|
|
+ # in the subprocess and insulate it from signals (e.g. SIGINT)
|
|
+ # received by the parent
|
|
+ master, slave = pty.openpty()
|
|
+ self.shell = self.getShell( master, slave, mnopts )
|
|
+ self.stdin = os.fdopen( master, 'rw' )
|
|
+ self.stdout = self.stdin
|
|
+ self.pid = self.shell.pid
|
|
+ self.pollOut = select.poll()
|
|
+ self.pollOut.register( self.stdout )
|
|
+ # Maintain mapping between file descriptors and nodes
|
|
+ # This is useful for monitoring multiple nodes
|
|
+ # using select.poll()
|
|
+ self.outToNode[ self.stdout.fileno() ] = self
|
|
+ self.inToNode[ self.stdin.fileno() ] = self
|
|
+ self.execed = False
|
|
+ self.lastCmd = None
|
|
+ self.lastPid = None
|
|
+ self.readbuf = ''
|
|
+ # Wait for prompt
|
|
+ while True:
|
|
+ data = self.read( 1024 )
|
|
+ if data[ -1 ] == chr( 127 ):
|
|
+ break
|
|
+ self.pollOut.poll()
|
|
+ self.waiting = False
|
|
+ # +m: disable job control notification
|
|
+ self.cmd( 'unset HISTFILE; stty -echo; set +m' )
|
|
+
|
|
+ def mountPrivateDirs( self ):
|
|
+ "mount private directories - overridden"
|
|
+ pass
|
|
+
|
|
+ def unmountPrivateDirs( self ):
|
|
+ "mount private directories - overridden"
|
|
+ pass
|
|
+
|
|
+ def _popen( self, cmd, **params ):
|
|
+ """Internal method: spawn and return a process
|
|
+ cmd: command to run (list)
|
|
+ params: parameters to Popen()"""
|
|
+ # Leave this is as an instance method for now
|
|
+ assert self
|
|
+ return Popen( cmd, **params )
|
|
+
|
|
+ def cleanup( self ):
|
|
+ "Help python collect its garbage."
|
|
+ self.shell = None
|
|
+
|
|
+ # Subshell I/O, commands and control
|
|
+
|
|
+ def read( self, maxbytes=1024 ):
|
|
+ """Buffered read from node, potentially blocking.
|
|
+ maxbytes: maximum number of bytes to return"""
|
|
+ count = len( self.readbuf )
|
|
+ if count < maxbytes:
|
|
+ data = os.read( self.stdout.fileno(), maxbytes - count )
|
|
+ self.readbuf += data
|
|
+ if maxbytes >= len( self.readbuf ):
|
|
+ result = self.readbuf
|
|
+ self.readbuf = ''
|
|
+ else:
|
|
+ result = self.readbuf[ :maxbytes ]
|
|
+ self.readbuf = self.readbuf[ maxbytes: ]
|
|
+ return result
|
|
+
|
|
+ def readline( self ):
|
|
+ """Buffered readline from node, potentially blocking.
|
|
+ returns: line (minus newline) or None"""
|
|
+ self.readbuf += self.read( 1024 )
|
|
+ if '\n' not in self.readbuf:
|
|
+ return None
|
|
+ pos = self.readbuf.find( '\n' )
|
|
+ line = self.readbuf[ 0: pos ]
|
|
+ self.readbuf = self.readbuf[ pos + 1: ]
|
|
+ return line
|
|
+
|
|
+ # overridden in some platforms
|
|
+ def write( self, data ):
|
|
+ """Write data to node.
|
|
+ data: string"""
|
|
+ os.write( self.stdin.fileno(), data )
|
|
+
|
|
+ def terminate( self ):
|
|
+ "Send kill signal to Node and clean up after it."
|
|
+ pass
|
|
+
|
|
+ def stop( self, deleteIntfs=False ):
|
|
+ """Stop node.
|
|
+ deleteIntfs: delete interfaces? (False)"""
|
|
+ if deleteIntfs:
|
|
+ self.deleteIntfs()
|
|
+ self.terminate()
|
|
+
|
|
+ def waitReadable( self, timeoutms=None ):
|
|
+ """Wait until node's output is readable.
|
|
+ timeoutms: timeout in ms or None to wait indefinitely.
|
|
+ returns: result of poll()"""
|
|
+ if len( self.readbuf ) == 0:
|
|
+ return self.pollOut.poll( timeoutms )
|
|
+
|
|
+ def sendCmd( self, *args, **kwargs ):
|
|
+ """Send a command, followed by a command to echo a sentinel,
|
|
+ and return without waiting for the command to complete.
|
|
+ args: command and arguments, or string
|
|
+ printPid: print command's PID? (False)"""
|
|
+ assert self.shell and not self.waiting
|
|
+ printPid = kwargs.get( 'printPid', True )
|
|
+ # Allow sendCmd( [ list ] )
|
|
+ if len( args ) == 1 and isinstance( args[ 0 ], list ):
|
|
+ cmd = args[ 0 ]
|
|
+ # Allow sendCmd( cmd, arg1, arg2... )
|
|
+ elif len( args ) > 0:
|
|
+ cmd = args
|
|
+ # Convert to string
|
|
+ if not isinstance( cmd, str ):
|
|
+ cmd = ' '.join( [ str( c ) for c in cmd ] )
|
|
+ if not re.search( r'\w', cmd ):
|
|
+ # Replace empty commands with something harmless
|
|
+ cmd = 'echo -n'
|
|
+ self.lastCmd = cmd
|
|
+ # if a builtin command is backgrounded, it still yields a PID
|
|
+ if len( cmd ) > 0 and cmd[ -1 ] == '&':
|
|
+ # print ^A{pid}\n so monitor() can set lastPid
|
|
+ cmd += ' printf "\\001%d\\012" $! '
|
|
+ elif printPid and not self.isShellBuiltin( cmd ):
|
|
+ cmd = 'mnexec -p ' + cmd
|
|
+ self.write( cmd + '\n' )
|
|
+ self.lastPid = None
|
|
+ self.waiting = True
|
|
+
|
|
+ def monitor( self, timeoutms=None, findPid=True ):
|
|
+ """Monitor and return the output of a command.
|
|
+ Set self.waiting to False if command has completed.
|
|
+ timeoutms: timeout in ms or None to wait indefinitely
|
|
+ findPid: look for PID from mnexec -p"""
|
|
+ ready = self.waitReadable( timeoutms )
|
|
+ if not ready:
|
|
+ return ''
|
|
+ data = self.read( 1024 )
|
|
+ pidre = r'\[\d+\] \d+\r\n'
|
|
+ # Look for PID
|
|
+ marker = chr( 1 ) + r'\d+\r\n'
|
|
+ if findPid and chr( 1 ) in data:
|
|
+ # suppress the job and PID of a backgrounded command
|
|
+ if re.findall( pidre, data ):
|
|
+ data = re.sub( pidre, '', data )
|
|
+ # Marker can be read in chunks; continue until all of it is read
|
|
+ while not re.findall( marker, data ):
|
|
+ data += self.read( 1024 )
|
|
+ markers = re.findall( marker, data )
|
|
+ if markers:
|
|
+ self.lastPid = int( markers[ 0 ][ 1: ] )
|
|
+ data = re.sub( marker, '', data )
|
|
+ # Look for sentinel/EOF
|
|
+ if len( data ) > 0 and data[ -1 ] == chr( 127 ):
|
|
+ self.waiting = False
|
|
+ data = data[ :-1 ]
|
|
+ elif chr( 127 ) in data:
|
|
+ self.waiting = False
|
|
+ data = data.replace( chr( 127 ), '' )
|
|
+ return data
|
|
+
|
|
+ def waitOutput( self, verbose=False, findPid=True ):
|
|
+ """Wait for a command to complete.
|
|
+ Completion is signaled by a sentinel character, ASCII(127)
|
|
+ appearing in the output stream. Wait for the sentinel and return
|
|
+ the output, including trailing newline.
|
|
+ verbose: print output interactively"""
|
|
+ log = info if verbose else debug
|
|
+ output = ''
|
|
+ while self.waiting:
|
|
+ data = self.monitor( findPid=findPid )
|
|
+ output += data
|
|
+ log( data )
|
|
+ return output
|
|
+
|
|
+ def cmd( self, *args, **kwargs ):
|
|
+ """Send a command, wait for output, and return it.
|
|
+ cmd: string"""
|
|
+ verbose = kwargs.get( 'verbose', False )
|
|
+ log = info if verbose else debug
|
|
+ log( '*** %s : %s\n' % ( self.name, args ) )
|
|
+ if self.shell:
|
|
+ self.sendCmd( *args, **kwargs )
|
|
+ return self.waitOutput( verbose )
|
|
+ else:
|
|
+ warn( '(%s exited - ignoring cmd%s)\n' % ( self, args ) )
|
|
+
|
|
+ def cmdPrint( self, *args):
|
|
+ """Call cmd and printing its output
|
|
+ cmd: string"""
|
|
+ return self.cmd( *args, **{ 'verbose': True } )
|
|
+
|
|
+ def popen( self, *args, **kwargs ):
|
|
+ """Return a Popen() object in our namespace
|
|
+ args: Popen() args, single list, or string
|
|
+ kwargs: Popen() keyword args"""
|
|
+ pass
|
|
+
|
|
+ def pexec( self, *args, **kwargs ):
|
|
+ """Execute a command using popen
|
|
+ returns: out, err, exitcode"""
|
|
+ popen = self.popen( *args, stdin=PIPE, stdout=PIPE, stderr=PIPE,
|
|
+ **kwargs )
|
|
+ # Warning: this can fail with large numbers of fds!
|
|
+ out, err = popen.communicate()
|
|
+ exitcode = popen.wait()
|
|
+ return out, err, exitcode
|
|
+
|
|
+ # Interface management, configuration, and routing
|
|
+
|
|
+ # BL notes: This might be a bit redundant or over-complicated.
|
|
+ # However, it does allow a bit of specialization, including
|
|
+ # changing the canonical interface names. It's also tricky since
|
|
+ # the real interfaces are created as veth pairs, so we can't
|
|
+ # make a single interface at a time.
|
|
+
|
|
+ def newPort( self ):
|
|
+ "Return the next port number to allocate."
|
|
+ if len( self.ports ) > 0:
|
|
+ return max( self.ports.values() ) + 1
|
|
+ return self.portBase
|
|
+
|
|
+ def addIntf( self, intf, port=None, moveIntfFn=moveIntf ):
|
|
+ """Add an interface.
|
|
+ intf: interface
|
|
+ port: port number (optional, typically OpenFlow port number)
|
|
+ moveIntfFn: function to move interface (optional)"""
|
|
+ if port is None:
|
|
+ port = self.newPort()
|
|
+ self.intfs[ port ] = intf
|
|
+ self.ports[ intf ] = port
|
|
+ self.nameToIntf[ intf.name ] = intf
|
|
+ debug( '\n' )
|
|
+ debug( 'added intf %s (%d) to node %s\n' % (
|
|
+ intf, port, self.name ) )
|
|
+ if self.inNamespace:
|
|
+ debug( 'moving', intf, 'into namespace for', self.name, '\n' )
|
|
+ moveIntfFn( intf.name, self )
|
|
+
|
|
+ def delIntf( self, intf ):
|
|
+ """Remove interface from Node's known interfaces
|
|
+ Note: to fully delete interface, call intf.delete() instead"""
|
|
+ port = self.ports.get( intf )
|
|
+ if port is not None:
|
|
+ del self.intfs[ port ]
|
|
+ del self.ports[ intf ]
|
|
+ del self.nameToIntf[ intf.name ]
|
|
+
|
|
+ def defaultIntf( self ):
|
|
+ "Return interface for lowest port"
|
|
+ ports = self.intfs.keys()
|
|
+ if ports:
|
|
+ return self.intfs[ min( ports ) ]
|
|
+ else:
|
|
+ warn( '*** defaultIntf: warning:', self.name,
|
|
+ 'has no interfaces\n' )
|
|
+
|
|
+ def intf( self, intf=None ):
|
|
+ """Return our interface object with given string name,
|
|
+ default intf if name is falsy (None, empty string, etc).
|
|
+ or the input intf arg.
|
|
+
|
|
+ Having this fcn return its arg for Intf objects makes it
|
|
+ easier to construct functions with flexible input args for
|
|
+ interfaces (those that accept both string names and Intf objects).
|
|
+ """
|
|
+ if not intf:
|
|
+ return self.defaultIntf()
|
|
+ elif isinstance( intf, basestring ):
|
|
+ return self.nameToIntf[ intf ]
|
|
+ else:
|
|
+ return intf
|
|
+
|
|
+ def connectionsTo( self, node):
|
|
+ "Return [ intf1, intf2... ] for all intfs that connect self to node."
|
|
+ # We could optimize this if it is important
|
|
+ connections = []
|
|
+ for intf in self.intfList():
|
|
+ link = intf.link
|
|
+ if link:
|
|
+ node1, node2 = link.intf1.node, link.intf2.node
|
|
+ if node1 == self and node2 == node:
|
|
+ connections += [ ( intf, link.intf2 ) ]
|
|
+ elif node1 == node and node2 == self:
|
|
+ connections += [ ( intf, link.intf1 ) ]
|
|
+ return connections
|
|
+
|
|
+ def deleteIntfs( self, checkName=True ):
|
|
+ """Delete all of our interfaces.
|
|
+ checkName: only delete interfaces that contain our name"""
|
|
+ # In theory the interfaces should go away after we shut down.
|
|
+ # However, this takes time, so we're better off removing them
|
|
+ # explicitly so that we won't get errors if we run before they
|
|
+ # have been removed by the kernel. Unfortunately this is very slow,
|
|
+ # at least with Linux kernels before 2.6.33
|
|
+ for intf in self.intfs.values():
|
|
+ # Protect against deleting hardware interfaces
|
|
+ if ( self.name in intf.name ) or ( not checkName ):
|
|
+ intf.delete()
|
|
+ info( '.' )
|
|
+
|
|
+ # Routing support
|
|
+
|
|
+ def setARP( self, ip, mac ):
|
|
+ """Add an ARP entry.
|
|
+ ip: IP address as string
|
|
+ mac: MAC address as string"""
|
|
+ result = self.cmd( 'arp', '-s', ip, mac )
|
|
+ return result
|
|
+
|
|
+ def setHostRoute( self, ip, intf ):
|
|
+ """Add route to host.
|
|
+ ip: IP address as dotted decimal
|
|
+ intf: string, interface name"""
|
|
+ pass
|
|
+
|
|
+ def setDefaultRoute( self, intf=None ):
|
|
+ """Set the default route to go through intf.
|
|
+ intf: Intf or {dev <intfname> via <gw-ip> ...}"""
|
|
+ pass
|
|
+
|
|
+ # Convenience and configuration methods
|
|
+
|
|
+ def setMAC( self, mac, intf=None ):
|
|
+ """Set the MAC address for an interface.
|
|
+ intf: intf or intf name
|
|
+ mac: MAC address as string"""
|
|
+ return self.intf( intf ).setMAC( mac )
|
|
+
|
|
+ def setIP( self, ip, prefixLen=8, intf=None, **kwargs ):
|
|
+ """Set the IP address for an interface.
|
|
+ intf: intf or intf name
|
|
+ ip: IP address as a string
|
|
+ prefixLen: prefix length, e.g. 8 for /8 or 16M addrs
|
|
+ kwargs: any additional arguments for intf.setIP"""
|
|
+ return self.intf( intf ).setIP( ip, prefixLen, **kwargs )
|
|
+
|
|
+ def IP( self, intf=None ):
|
|
+ "Return IP address of a node or specific interface."
|
|
+ return self.intf( intf ).IP()
|
|
+
|
|
+ def MAC( self, intf=None ):
|
|
+ "Return MAC address of a node or specific interface."
|
|
+ return self.intf( intf ).MAC()
|
|
+
|
|
+ def intfIsUp( self, intf=None ):
|
|
+ "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 a *single* parameter
|
|
+ results: dict of results to update
|
|
+ method: config method name
|
|
+ param: arg=value (ignore if value=None)
|
|
+ value may also be list or dict"""
|
|
+ name, value = param.items()[ 0 ]
|
|
+ if value is None:
|
|
+ return
|
|
+ f = getattr( self, method, None )
|
|
+ if not f:
|
|
+ return
|
|
+ if isinstance( value, list ):
|
|
+ result = f( *value )
|
|
+ elif isinstance( value, dict ):
|
|
+ result = f( **value )
|
|
+ else:
|
|
+ result = f( value )
|
|
+ results[ name ] = result
|
|
+ return result
|
|
+
|
|
+ def config( self, mac=None, ip=None,
|
|
+ defaultRoute=None, lo='up', **_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, 'setDefaultRoute', defaultRoute=defaultRoute )
|
|
+ # This should be examined
|
|
+ self.cmd( 'ifconfig %s %s' % ( LO, lo ) )
|
|
+ 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
|
|
+ replace with Link( node1, node2)"""
|
|
+ return link( self, node )
|
|
+
|
|
+ # Other methods
|
|
+
|
|
+ def intfList( self ):
|
|
+ "List of our interfaces sorted by port number"
|
|
+ return [ self.intfs[ p ] for p in sorted( self.intfs.iterkeys() ) ]
|
|
+
|
|
+ def intfNames( self ):
|
|
+ "The names of our interfaces sorted by port number"
|
|
+ return [ str( i ) for i in self.intfList() ]
|
|
+
|
|
+ def __repr__( self ):
|
|
+ "More informative string representation"
|
|
+ intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() )
|
|
+ for i in self.intfList() ] ) )
|
|
+ return '<%s %s: %s pid=%s> ' % (
|
|
+ self.__class__.__name__, self.name, intfs, self.pid )
|
|
+
|
|
+ def __str__( self ):
|
|
+ "Abbreviated string representation"
|
|
+ return self.name
|
|
+
|
|
+ # 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')
|