openbsd-ports/net/mininet/patches/patch-mininet_basenode_py
2017-12-07 06:33:40 +00:00

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')