diff --git a/Makefile b/Makefile index 485291b..8a90b2c 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ codecheck: $(PYSRC) test: $(MININET) $(TEST) mininet/test/test_nets.py -install: +install: mnexec + cp mnexec bin/ python setup.py install diff --git a/mininet/cli.py b/mininet/cli.py index 45039b3..18e93b3 100644 --- a/mininet/cli.py +++ b/mininet/cli.py @@ -15,10 +15,9 @@ size. The CLI automatically substitutes IP addresses for node names, so commands like -mininet> h0 ping -c1 h31 +mininet> h2 ping h3 -should work correctly and allow host h0 to ping host h31. -Note the '-c1' argument as per the Bugs/limitations section below! +should work correctly and allow host h2 to ping host h3 Several useful commands are provided, including the ability to list all nodes ('nodes'), to print out the network topology @@ -27,10 +26,7 @@ and bandwidth ('iperf'.) Bugs/limitations: -- Interactive commands are not supported at the moment; - notably, if you type 'ping h1', you can't interrupt it. - For now, we recommend limiting CLI use to non-interactive - commands which terminate in a reasonable amount of time. +- Interactive commands are not supported at the moment """ @@ -52,7 +48,16 @@ class CLI( Cmd ): self.nodemap[ node.name ] = node Cmd.__init__( self ) info( '*** Starting CLI:\n' ) - self.cmdloop() + while True: + try: + self.cmdloop() + break + except KeyboardInterrupt: + info( 'Interrupt\n' ) + + def emptyline( self ): + "Don't repeat last command when you hit return." + pass # Disable pylint "Unused argument: 'arg's'" messages. # Each CLI function needs the same interface. @@ -70,13 +75,10 @@ class CLI( Cmd ): 'addresses\n' 'for node names when a node is the first arg, so commands' ' like\n' - ' mininet> h0 ping -c1 h1\n' + ' mininet> h2 ping h3\n' 'should work.\n' '\n' - 'Interactive commands are not supported yet,\n' - 'so please limit commands to ones that do not\n' - 'require user interaction and will terminate\n' - 'after a reasonable amount of time.\n' ) + 'Interactive commands are not supported yet.\n' ) if args is "": self.stdout.write( helpStr ) @@ -176,7 +178,7 @@ class CLI( Cmd ): for arg in rest ] rest = ' '.join( rest ) # Run cmd on node: - node.sendCmd( rest ) + node.sendCmd( rest, printPid=True ) while True: try: done, data = node.monitor() diff --git a/mininet/node.py b/mininet/node.py index 7cd5845..a8691d7 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -49,7 +49,7 @@ from subprocess import Popen, PIPE, STDOUT from time import sleep from mininet.log import info, error, debug -from mininet.util import quietRun, makeIntfPair, moveIntf +from mininet.util import quietRun, makeIntfPair, moveIntf, isShellBuiltin class Node( object ): """A virtual network node is simply a shell in a network namespace. @@ -65,21 +65,20 @@ class Node( object ): defaultMAC: default MAC address for intf 0 defaultIP: default IP address for intf 0""" self.name = name - closeFds = False # speed vs. memory use - # setsid is necessary to detach from tty - # xpg_echo is needed so we can echo our sentinel in sendCmd - cmd = [ '/usr/bin/setsid', '/bin/bash', '-O', 'xpg_echo' ] + opts = '-cdp' self.inNamespace = inNamespace if self.inNamespace: - cmd = [ 'netns' ] + cmd + opts += '-n' + # xpg_echo is needed so we can echo our sentinel in sendCmd + cmd = [ 'mnexec', opts, 'bash', '-O', 'xpg_echo', '-m' ] self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, - close_fds=closeFds ) + close_fds=False ) self.stdin = self.shell.stdin self.stdout = self.shell.stdout self.pollOut = select.poll() self.pollOut.register( self.stdout ) # Maintain mapping between file descriptors and nodes - # This could be useful for monitoring multiple nodes + # This is useful for monitoring multiple nodes # using select.poll() self.outToNode[ self.stdout.fileno() ] = self self.inToNode[ self.stdin.fileno() ] = self @@ -89,10 +88,17 @@ class Node( object ): # replace with Port objects, eventually ? self.ips = {} # dict of interfaces to ip addresses as strings self.connection = {} # remote node connected to each interface - self.waiting = False self.execed = False self.defaultIP = defaultIP self.defaultMAC = defaultMAC + self.lastCmd = None + self.lastPid = None + # Grab PID + self.waiting = True + while self.lastPid is None: + self.monitor() + self.pid = self.lastPid + self.waiting = False @classmethod def fdToNode( cls, fd ): @@ -130,7 +136,7 @@ class Node( object ): "Wait until node's output is readable." self.pollOut.poll() - def sendCmd( self, cmd ): + def sendCmd( self, cmd, printPid=False ): """Send a command, followed by a command to echo a sentinel, and return without waiting for the command to complete.""" assert not self.waiting @@ -141,23 +147,36 @@ class Node( object ): cmd = cmd[ :-1 ] else: separator = ';' + if printPid and not isShellBuiltin( cmd ): + cmd = 'mnexec -p ' + cmd self.write( cmd + separator + ' echo -n "\\0177" \n' ) + self.lastCmd = cmd + self.lastPid = None self.waiting = True def sendInt( self ): - """Placeholder for function to interrupt running subprocess. - This is a tricky problem to solve.""" - self.write( chr( 3 ) ) + "Interrupt running command." + if self.lastPid: + os.kill( self.lastPid, signal.SIGINT ) def monitor( self ): "Monitor the output of a command, returning (done?, data)." assert self.waiting self.waitReadable() data = self.read( 1024 ) + # Look for PID + marker = chr( 1 ) + r'\d+\n' + if chr( 1 ) in data: + 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 return True, data[ :-1 ] elif chr( 127 ) in data: + self.waiting = False return True, data.replace( chr( 127 ), '' ) return False, data @@ -336,11 +355,11 @@ class Switch( Node ): """A Switch is a Node that is running (or has execed?) an OpenFlow switch.""" - def sendCmd( self, cmd ): + def sendCmd( self, cmd, printCmd=False): """Send command to Node. cmd: string""" if not self.execed: - return Node.sendCmd( self, cmd ) + return Node.sendCmd( self, cmd, printCmd ) else: error( '*** Error: %s has execed and cannot accept commands' % self.name ) diff --git a/mininet/util.py b/mininet/util.py index 9437430..db2dd92 100644 --- a/mininet/util.py +++ b/mininet/util.py @@ -178,3 +178,18 @@ def makeNumeric( s ): return float( s ) else: return s + +# pylint: disable-msg=E1101,W0612 + +def isShellBuiltin( cmd ): + "Return True if cmd is a bash builtin." + if isShellBuiltin.builtIns is None: + isShellBuiltin.builtIns = quietRun( 'bash -c enable' ) + space = cmd.find( ' ' ) + if space > 0: + cmd = cmd[ :space] + return cmd in isShellBuiltin.builtIns + +isShellBuiltin.builtIns = None + +# pylint: enable-msg=E1101,W0612 diff --git a/mnexec.c b/mnexec.c index 2a7e5c7..4b09912 100644 --- a/mnexec.c +++ b/mnexec.c @@ -72,7 +72,7 @@ int main(int argc, char *argv[]) if (optind < argc) { execvp(argv[optind], &argv[optind]); - perror("execvp"); + perror(argv[optind]); return 1; }