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