Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 80a8fa62

History | View | Annotate | Download (23 KB)

1
#!/usr/bin/python
2
"""Mininet: A simple networking testbed for OpenFlow!
3
author: Bob Lantz ( rlantz@cs.stanford.edu )
4
author: Brandon Heller ( brandonh@stanford.edu )
5

6
Mininet creates scalable OpenFlow test networks by using
7
process-based virtualization and network namespaces.
8

9
Simulated hosts are created as processes in separate network
10
namespaces. This allows a complete OpenFlow network to be simulated on
11
top of a single Linux kernel.
12

13
Each host has:
14
A virtual console ( pipes to a shell )
15
A virtual interfaces ( half of a veth pair )
16
A parent shell ( and possibly some child processes ) in a namespace
17

18
Hosts have a network interface which is configured via ifconfig/ip
19
link/etc.
20

21
This version supports both the kernel and user space datapaths
22
from the OpenFlow reference implementation.
23

24
In kernel datapath mode, the controller and switches are simply
25
processes in the root namespace.
26

27
Kernel OpenFlow datapaths are instantiated using dpctl( 8 ), and are
28
attached to the one side of a veth pair; the other side resides in the
29
host namespace. In this mode, switch processes can simply connect to the
30
controller via the loopback interface.
31

32
In user datapath mode, the controller and switches are full-service
33
nodes that live in their own network namespaces and have management
34
interfaces and IP addresses on a control network ( e.g. 10.0.123.1,
35
currently routed although it could be bridged. )
36

37
In addition to a management interface, user mode switches also have
38
several switch interfaces, halves of veth pairs whose other halves
39
reside in the host nodes that the switches are connected to.
40

41
Naming:
42
Host nodes are named h1-hN
43
Switch nodes are named s0-sN
44
Interfaces are named { nodename }-eth0 .. { nodename }-ethN,"""
45
import os
46
import re
47
import signal
48
from subprocess import call
49
import sys
50
from time import sleep
51

    
52
from mininet.log import lg
53
from mininet.node import KernelSwitch, OVSKernelSwitch
54
from mininet.util import quietRun, fixLimits
55
from mininet.util import makeIntfPair, moveIntf
56
from mininet.xterm import cleanUpScreens, makeXterms
57

    
58
DATAPATHS = [ 'kernel' ] #[ 'user', 'kernel' ]
59

    
60
def init():
61
    "Initialize Mininet."
62
    if os.getuid() != 0:
63
        # Note: this script must be run as root
64
        # Perhaps we should do so automatically!
65
        print "*** Mininet must run as root."
66
        exit( 1 )
67
    # If which produces no output, then netns is not in the path.
68
    # May want to loosen this to handle netns in the current dir.
69
    if not quietRun( [ 'which', 'netns' ] ):
70
        raise Exception( "Could not find netns; see INSTALL" )
71
    fixLimits()
72

    
73
class Mininet( object ):
74
    "Network emulation with hosts spawned in network namespaces."
75

    
76
    def __init__( self, topo, switch, host, controller, cparams,
77
                 build=True, xterms=False, cleanup=False,
78
                 inNamespace=False,
79
                 autoSetMacs=False, autoStaticArp=False ):
80
        """Create Mininet object.
81
           topo: Topo object
82
            switch: Switch class
83
           host: Host class
84
           controller: Controller class
85
           cparams: ControllerParams object
86
           now: build now?
87
           xterms: if build now, spawn xterms?
88
           cleanup: if build now, cleanup before creating?
89
           inNamespace: spawn switches and controller in net namespaces?
90
           autoSetMacs: set MAC addrs to DPIDs?
91
           autoStaticArp: set all-pairs static MAC addrs?"""
92
        self.topo = topo
93
        self.switch = switch
94
        self.host = host
95
        self.controller = controller
96
        self.cparams = cparams
97
        self.nodes = {} # dpid to Node{ Host, Switch } objects
98
        self.controllers = {} # controller name to Controller objects
99
        self.dps = 0 # number of created kernel datapaths
100
        self.inNamespace = inNamespace
101
        self.xterms = xterms
102
        self.cleanup = cleanup
103
        self.autoSetMacs = autoSetMacs
104
        self.autoStaticArp = autoStaticArp
105

    
106
        self.terms = [] # list of spawned xterm processes
107

    
108
        if build:
109
            self.build()
110

    
111
    def _addHost( self, dpid ):
112
        """Add host.
113
           dpid: DPID of host to add"""
114
        host = self.host( 'h_' + self.topo.name( dpid ) )
115
        # for now, assume one interface per host.
116
        host.intfs.append( 'h_' + self.topo.name( dpid ) + '-eth0' )
117
        self.nodes[ dpid ] = host
118
        #lg.info( '%s ' % host.name )
119

    
120
    def _addSwitch( self, dpid ):
121
        """Add switch.
122
           dpid: DPID of switch to add"""
123
        sw = None
124
        swDpid = None
125
        if self.autoSetMacs:
126
            swDpid = dpid
127
        if self.switch is KernelSwitch or self.switch is OVSKernelSwitch:
128
            sw = self.switch( 's_' + self.topo.name( dpid ), dp = self.dps,
129
                             dpid = swDpid )
130
            self.dps += 1
131
        else:
132
            sw = self.switch( 's_' + self.topo.name( dpid ) )
133
        self.nodes[ dpid ] = sw
134

    
135
    def _addLink( self, src, dst ):
136
        """Add link.
137
           src: source DPID
138
           dst: destination DPID"""
139
        srcPort, dstPort = self.topo.port( src, dst )
140
        srcNode = self.nodes[ src ]
141
        dstNode = self.nodes[ dst ]
142
        srcIntf = srcNode.intfName( srcPort )
143
        dstIntf = dstNode.intfName( dstPort )
144
        makeIntfPair( srcIntf, dstIntf )
145
        srcNode.intfs.append( srcIntf )
146
        dstNode.intfs.append( dstIntf )
147
        srcNode.ports[ srcPort ] = srcIntf
148
        dstNode.ports[ dstPort ] = dstIntf
149
        #lg.info( '\n' )
150
        #lg.info( 'added intf %s to src node %x\n' % ( srcIntf, src ) )
151
        #lg.info( 'added intf %s to dst node %x\n' % ( dstIntf, dst ) )
152
        if srcNode.inNamespace:
153
            #lg.info( 'moving src w/inNamespace set\n' )
154
            moveIntf( srcIntf, srcNode )
155
        if dstNode.inNamespace:
156
            #lg.info( 'moving dst w/inNamespace set\n' )
157
            moveIntf( dstIntf, dstNode )
158
        srcNode.connection[ srcIntf ] = ( dstNode, dstIntf )
159
        dstNode.connection[ dstIntf ] = ( srcNode, srcIntf )
160

    
161
    def _addController( self, controller ):
162
        """Add controller.
163
           controller: Controller class"""
164
        controller = self.controller( 'c0', self.inNamespace )
165
        if controller: # allow controller-less setups
166
            self.controllers[ 'c0' ] = controller
167

    
168
    # Control network support:
169
    #
170
    # Create an explicit control network. Currently this is only
171
    # used by the user datapath configuration.
172
    #
173
    # Notes:
174
    #
175
    # 1. If the controller and switches are in the same ( e.g. root )
176
    #    namespace, they can just use the loopback connection.
177
    #    We may wish to do this for the user datapath as well as the
178
    #    kernel datapath.
179
    #
180
    # 2. If we can get unix domain sockets to work, we can use them
181
    #    instead of an explicit control network.
182
    #
183
    # 3. Instead of routing, we could bridge or use 'in-band' control.
184
    #
185
    # 4. Even if we dispense with this in general, it could still be
186
    #    useful for people who wish to simulate a separate control
187
    #    network ( since real networks may need one! )
188

    
189
    def _configureControlNetwork( self ):
190
        "Configure control network."
191
        self._configureRoutedControlNetwork()
192

    
193
    def _configureRoutedControlNetwork( self ):
194
        """Configure a routed control network on controller and switches.
195
           For use with the user datapath only right now.
196
           TODO( brandonh ) test this code!
197
           """
198

    
199
        # params were: controller, switches, ips
200

    
201
        controller = self.controllers[ 'c0' ]
202
        lg.info( '%s <-> ' % controller.name )
203
        for switchDpid in self.topo.switches():
204
            switch = self.nodes[ switchDpid ]
205
            lg.info( '%s ' % switch.name )
206
            sip = self.topo.ip( switchDpid )#ips.next()
207
            sintf = switch.intfs[ 0 ]
208
            node, cintf = switch.connection[ sintf ]
209
            if node != controller:
210
                lg.error( '*** Error: switch %s not connected to correct'
211
                         'controller' %
212
                         switch.name )
213
                exit( 1 )
214
            controller.setIP( cintf, self.cparams.ip, '/' +
215
                             self.cparams.subnetSize )
216
            switch.setIP( sintf, sip, '/' + self.cparams.subnetSize )
217
            controller.setHostRoute( sip, cintf )
218
            switch.setHostRoute( self.cparams.ip, sintf )
219
        lg.info( '\n' )
220
        lg.info( '*** Testing control network\n' )
221
        while not controller.intfIsUp( controller.intfs[ 0 ] ):
222
            lg.info( '*** Waiting for %s to come up\n', controller.intfs[ 0 ] )
223
            sleep( 1 )
224
        for switchDpid in self.topo.switches():
225
            switch = self.nodes[ switchDpid ]
226
            while not switch.intfIsUp( switch.intfs[ 0 ] ):
227
                lg.info( '*** Waiting for %s to come up\n' %
228
                    switch.intfs[ 0 ] )
229
                sleep( 1 )
230
            if self.ping( hosts=[ switch, controller ] ) != 0:
231
                lg.error( '*** Error: control network test failed\n' )
232
                exit( 1 )
233
        lg.info( '\n' )
234

    
235
    def _configHosts( self ):
236
        "Configure a set of hosts."
237
        # params were: hosts, ips
238
        for hostDpid in self.topo.hosts():
239
            host = self.nodes[ hostDpid ]
240
            hintf = host.intfs[ 0 ]
241
            host.setIP( hintf, self.topo.ip( hostDpid ),
242
                       '/' + str( self.cparams.subnetSize ) )
243
            host.setDefaultRoute( hintf )
244
            # You're low priority, dude!
245
            quietRun( 'renice +18 -p ' + repr( host.pid ) )
246
            lg.info( '%s ', host.name )
247
        lg.info( '\n' )
248

    
249
    def build( self ):
250
        """Build mininet.
251
           At the end of this function, everything should be connected
252
           and up."""
253
        if self.cleanup:
254
            pass # cleanup
255
        # validate topo?
256
        lg.info( '*** Adding controller\n' )
257
        self._addController( self.controller )
258
        lg.info( '*** Creating network\n' )
259
        lg.info( '*** Adding hosts:\n' )
260
        for host in sorted( self.topo.hosts() ):
261
            self._addHost( host )
262
            lg.info( '0x%x ' % host )
263
        lg.info( '\n*** Adding switches:\n' )
264
        for switch in sorted( self.topo.switches() ):
265
            self._addSwitch( switch )
266
            lg.info( '0x%x ' % switch )
267
        lg.info( '\n*** Adding edges:\n' )
268
        for src, dst in sorted( self.topo.edges() ):
269
            self._addLink( src, dst )
270
            lg.info( '(0x%x, 0x%x) ' % ( src, dst ) )
271
        lg.info( '\n' )
272

    
273
        if self.inNamespace:
274
            lg.info( '*** Configuring control network\n' )
275
            self._configureControlNetwork()
276

    
277
        lg.info( '*** Configuring hosts\n' )
278
        self._configHosts()
279

    
280
        if self.xterms:
281
            self.startXterms()
282
        if self.autoSetMacs:
283
            self.setMacs()
284
        if self.autoStaticArp:
285
            self.staticArp()
286

    
287
    def switchNodes( self ):
288
        "Return switch nodes."
289
        return [ self.nodes[ dpid ] for dpid in self.topo.switches() ]
290

    
291
    def hostNodes( self ):
292
        "Return host nodes."
293
        return [ self.nodes[ dpid ] for dpid in self.topo.hosts() ]
294

    
295
    def startXterms( self ):
296
        "Start an xterm for each node in the topo."
297
        lg.info( "*** Running xterms on %s\n" % os.environ[ 'DISPLAY' ] )
298
        cleanUpScreens()
299
        self.terms += makeXterms( self.controllers.values(), 'controller' )
300
        self.terms += makeXterms( self.switchNodes(), 'switch' )
301
        self.terms += makeXterms( self.hostNodes(), 'host' )
302

    
303
    def stopXterms( self ):
304
        "Kill each xterm."
305
        # Kill xterms
306
        for term in self.terms:
307
            os.kill( term.pid, signal.SIGKILL )
308
        cleanUpScreens()
309

    
310
    def setMacs( self ):
311
        """Set MAC addrs to correspond to datapath IDs on hosts.
312
           Assume that the host only has one interface."""
313
        for dpid in self.topo.hosts():
314
            hostNode = self.nodes[ dpid ]
315
            hostNode.setMAC( hostNode.intfs[ 0 ], dpid )
316

    
317
    def staticArp( self ):
318
        "Add all-pairs ARP entries to remove the need to handle broadcast."
319
        for src in self.topo.hosts():
320
            srcNode = self.nodes[ src ]
321
            for dst in self.topo.hosts():
322
                if src != dst:
323
                    srcNode.setARP( dst, dst )
324

    
325
    def start( self ):
326
        "Start controller and switches\n"
327
        lg.info( '*** Starting controller\n' )
328
        for cnode in self.controllers.values():
329
            cnode.start()
330
        lg.info( '*** Starting %s switches\n' % len( self.topo.switches() ) )
331
        for switchDpid in self.topo.switches():
332
            switch = self.nodes[ switchDpid ]
333
            #lg.info( 'switch = %s' % switch )
334
            lg.info( '0x%x ' % switchDpid )
335
            switch.start( self.controllers )
336
        lg.info( '\n' )
337

    
338
    def stop( self ):
339
        "Stop the controller(s), switches and hosts\n"
340
        if self.terms:
341
            lg.info( '*** Stopping %i terms\n' % len( self.terms ) )
342
            self.stopXterms()
343
        lg.info( '*** Stopping %i hosts\n' % len( self.topo.hosts() ) )
344
        for hostDpid in self.topo.hosts():
345
            host = self.nodes[ hostDpid ]
346
            lg.info( '%s ' % host.name )
347
            host.terminate()
348
        lg.info( '\n' )
349
        lg.info( '*** Stopping %i switches\n' % len( self.topo.switches() ) )
350
        for switchDpid in self.topo.switches():
351
            switch = self.nodes[ switchDpid ]
352
            lg.info( '%s' % switch.name )
353
            switch.stop()
354
        lg.info( '\n' )
355
        lg.info( '*** Stopping controller\n' )
356
        for cnode in self.controllers.values():
357
            cnode.stop()
358
        lg.info( '*** Test complete\n' )
359

    
360
    def run( self, test, **params ):
361
        "Perform a complete start/test/stop cycle."
362
        self.start()
363
        lg.info( '*** Running test\n' )
364
        result = getattr( self, test )( **params )
365
        self.stop()
366
        return result
367

    
368
    @staticmethod
369
    def _parsePing( pingOutput ):
370
        "Parse ping output and return packets sent, received."
371
        r = r'(\d+) packets transmitted, (\d+) received'
372
        m = re.search( r, pingOutput )
373
        if m == None:
374
            lg.error( '*** Error: could not parse ping output: %s\n' %
375
                     pingOutput )
376
            exit( 1 )
377
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
378
        return sent, received
379

    
380
    def ping( self, hosts=None ):
381
        """Ping between all specified hosts.
382
           hosts: list of host DPIDs
383
           returns: ploss packet loss percentage"""
384
        #self.start()
385
        # check if running - only then, start?
386
        packets = 0
387
        lost = 0
388
        ploss = None
389
        if not hosts:
390
            hosts = self.topo.hosts()
391
        lg.info( '*** Ping: testing ping reachability\n' )
392
        for nodeDpid in hosts:
393
            node = self.nodes[ nodeDpid ]
394
            lg.info( '%s -> ' % node.name )
395
            for destDpid in hosts:
396
                dest = self.nodes[ destDpid ]
397
                if node != dest:
398
                    result = node.cmd( 'ping -c1 ' + dest.IP() )
399
                    sent, received = self._parsePing( result )
400
                    packets += sent
401
                    if received > sent:
402
                        lg.error( '*** Error: received too many packets' )
403
                        lg.error( '%s' % result )
404
                        node.cmdPrint( 'route' )
405
                        exit( 1 )
406
                    lost += sent - received
407
                    lg.info( ( '%s ' % dest.name ) if received else 'X ' )
408
            lg.info( '\n' )
409
            ploss = 100 * lost / packets
410
        lg.info( "*** Results: %i%% dropped (%d/%d lost)\n" %
411
                ( ploss, lost, packets ) )
412
        return ploss
413

    
414
    def pingAll( self ):
415
        """Ping between all hosts.
416
           returns: ploss packet loss percentage"""
417
        return self.ping()
418

    
419
    def pingPair( self ):
420
        """Ping between first two hosts, useful for testing.
421
           returns: ploss packet loss percentage"""
422
        hostsSorted = sorted( self.topo.hosts() )
423
        hosts = [ hostsSorted[ 0 ], hostsSorted[ 1 ] ]
424
        return self.ping( hosts=hosts )
425

    
426
    @staticmethod
427
    def _parseIperf( iperfOutput ):
428
        """Parse iperf output and return bandwidth.
429
           iperfOutput: string
430
           returns: result string"""
431
        r = r'([\d\.]+ \w+/sec)'
432
        m = re.search( r, iperfOutput )
433
        if m:
434
            return m.group( 1 )
435
        else:
436
            raise Exception( 'could not parse iperf output' )
437

    
438
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M',
439
              verbose=False ):
440
        """Run iperf between two hosts.
441
           hosts: list of host DPIDs; if None, uses opposite hosts
442
           l4Type: string, one of [ TCP, UDP ]
443
           verbose: verbose printing
444
           returns: results two-element array of server and client speeds"""
445
        if not hosts:
446
            hostsSorted = sorted( self.topo.hosts() )
447
            hosts = [ hostsSorted[ 0 ], hostsSorted[ -1 ] ]
448
        else:
449
            assert len( hosts ) == 2
450
        host0 = self.nodes[ hosts[ 0 ] ]
451
        host1 = self.nodes[ hosts[ 1 ] ]
452
        lg.info( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
453
        lg.info( "%s and %s\n" % ( host0.name, host1.name ) )
454
        host0.cmd( 'killall -9 iperf' )
455
        iperfArgs = 'iperf '
456
        bwArgs = ''
457
        if l4Type == 'UDP':
458
            iperfArgs += '-u '
459
            bwArgs = '-b ' + udpBw + ' '
460
        elif l4Type != 'TCP':
461
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
462
        server = host0.cmd( iperfArgs + '-s &' )
463
        if verbose:
464
            lg.info( '%s\n' % server )
465
        client = host1.cmd( iperfArgs + '-t 5 -c ' + host0.IP() + ' ' +
466
                           bwArgs )
467
        if verbose:
468
            lg.info( '%s\n' % client )
469
        server = host0.cmd( 'killall -9 iperf' )
470
        if verbose:
471
            lg.info( '%s\n' % server )
472
        result = [ self._parseIperf( server ), self._parseIperf( client ) ]
473
        if l4Type == 'UDP':
474
            result.insert( 0, udpBw )
475
        lg.info( '*** Results: %s\n' % result )
476
        return result
477

    
478
    def iperfUdp( self, udpBw='10M' ):
479
        "Run iperf UDP test."
480
        return self.iperf( l4Type='UDP', udpBw=udpBw )
481

    
482
    def interact( self ):
483
        "Start network and run our simple CLI."
484
        self.start()
485
        result = MininetCLI( self )
486
        self.stop()
487
        return result
488

    
489

    
490
class MininetCLI( object ):
491
    "Simple command-line interface to talk to nodes."
492
    cmds = [ '?', 'help', 'nodes', 'net', 'sh', 'ping_all', 'exit', \
493
            'ping_pair', 'iperf', 'iperf_udp', 'intfs', 'dump' ]
494

    
495
    def __init__( self, mininet ):
496
        self.mn = mininet
497
        self.nodemap = {} # map names to Node objects
498
        for node in self.mn.nodes.values():
499
            self.nodemap[ node.name ] = node
500
        for cname, cnode in self.mn.controllers.iteritems():
501
            self.nodemap[ cname ] = cnode
502
        self.nodelist = self.nodemap.values()
503
        self.run()
504

    
505
    # Disable pylint "Unused argument: 'arg's'" messages.
506
    # Each CLI function needs the same interface.
507
    # pylint: disable-msg=W0613
508

    
509
    # Commands
510
    def help( self, args ):
511
        "Semi-useful help for CLI."
512
        helpStr = ( 'Available commands are:' + str( self.cmds ) + '\n' +
513
                   'You may also send a command to a node using:\n' +
514
                   '  <node> command {args}\n' +
515
                   'For example:\n' +
516
                   '  mininet> h0 ifconfig\n' +
517
                   '\n' +
518
                   'The interpreter automatically substitutes IP ' +
519
                   'addresses\n' +
520
                   'for node names, so commands like\n' +
521
                   '  mininet> h0 ping -c1 h1\n' +
522
                   'should work.\n' +
523
                   '\n\n' +
524
                   'Interactive commands are not really supported yet,\n' +
525
                   'so please limit commands to ones that do not\n' +
526
                   'require user interaction and will terminate\n' +
527
                   'after a reasonable amount of time.\n' )
528
        print( helpStr )
529

    
530
    def nodes( self, args ):
531
        "List all nodes."
532
        nodes = ' '.join( [ node.name for node in sorted( self.nodelist ) ] )
533
        lg.info( 'available nodes are: \n%s\n' % nodes )
534

    
535
    def net( self, args ):
536
        "List network connections."
537
        for switchDpid in self.mn.topo.switches():
538
            switch = self.mn.nodes[ switchDpid ]
539
            lg.info( '%s <->', switch.name )
540
            for intf in switch.intfs:
541
                node = switch.connection[ intf ]
542
                lg.info( ' %s' % node.name )
543
            lg.info( '\n' )
544

    
545
    def sh( self, args ):
546
        "Run an external shell command"
547
        call( [ 'sh', '-c' ] + args )
548

    
549
    def pingAll( self, args ):
550
        "Ping between all hosts."
551
        self.mn.pingAll()
552

    
553
    def pingPair( self, args ):
554
        "Ping between first two hosts, useful for testing."
555
        self.mn.pingPair()
556

    
557
    def iperf( self, args ):
558
        "Simple iperf TCP test between two hosts."
559
        self.mn.iperf()
560

    
561
    def iperfUdp( self, args ):
562
        "Simple iperf UDP test between two hosts."
563
        udpBw = args[ 0 ] if len( args ) else '10M'
564
        self.mn.iperfUdp( udpBw )
565

    
566
    def intfs( self, args ):
567
        "List interfaces."
568
        for node in self.mn.nodes.values():
569
            lg.info( '%s: %s\n' % ( node.name, ' '.join( node.intfs ) ) )
570

    
571
    def dump( self, args ):
572
        "Dump node info."
573
        for node in self.mn.nodes.values():
574
            lg.info( '%s\n' % node )
575

    
576
    # Re-enable pylint "Unused argument: 'arg's'" messages.
577
    # pylint: enable-msg=W0613
578

    
579
    def run( self ):
580
        "Read and execute commands."
581
        lg.warn( '*** Starting CLI:\n' )
582
        while True:
583
            lg.warn( 'mininet> ' )
584
            inputLine = sys.stdin.readline()
585
            if inputLine == '':
586
                break
587
            if inputLine[ -1 ] == '\n':
588
                inputLine = inputLine[ :-1 ]
589
            cmd = inputLine.split( ' ' )
590
            first = cmd[ 0 ]
591
            rest = cmd[ 1: ]
592
            if first in self.cmds and hasattr( self, first ):
593
                getattr( self, first )( rest )
594
            elif first in self.nodemap and rest != []:
595
                node = self.nodemap[ first ]
596
                # Substitute IP addresses for node names in command
597
                rest = [ self.nodemap[ arg ].IP()
598
                    if arg in self.nodemap else arg
599
                    for arg in rest ]
600
                rest = ' '.join( rest )
601
                # Interactive commands don't work yet, and
602
                # there are still issues with control-c
603
                lg.warn( '*** %s: running %s\n' % ( node.name, rest ) )
604
                node.sendCmd( rest )
605
                while True:
606
                    try:
607
                        done, data = node.monitor()
608
                        lg.info( '%s\n' % data )
609
                        if done:
610
                            break
611
                    except KeyboardInterrupt:
612
                        node.sendInt()
613
            elif first == '':
614
                pass
615
            elif first in [ 'exit', 'quit' ]:
616
                break
617
            elif first == '?':
618
                self.help( rest )
619
            else:
620
                lg.error( 'CLI: unknown node or command: < %s >\n' % first )
621
            #lg.info( '*** CLI: command complete\n' )
622
        return 'exited by user command'