diff --git a/examples/scratchnet.py b/examples/scratchnet.py
index 9e9ccbd5e999be2c60c14c211a36598a4fdf6040..84c74bedb3dfd900621bafa0d199f1832eb1f068 100755
--- a/examples/scratchnet.py
+++ b/examples/scratchnet.py
@@ -6,35 +6,49 @@
 but it exposes the configuration details and allows customization.
 """
 
-from mininet.mininet import init, Node, createLink
+import logging
+
+from mininet.net import init
+from mininet.node import Node
+from mininet.util import createLink
+from mininet.log import info
+
+# print out info() messages, including cmdPrint
+logging.LOGLEVELDEFAULT = logging.INFO
 
 def scratchNet( cname='controller', cargs='ptcp:'):
-   # Create Network
-   controller = Node( 'c0', inNamespace=False )
-   switch = Node( 's0', inNamespace=False )
-   h0 = Node( 'h0' )
-   h1 = Node( 'h1' )
-   createLink( h0, switch )
-   createLink( h1, switch )
-   # Configure hosts
-   h0.setIP( h0.intfs[ 0 ], '192.168.123.1', '/24' )
-   h1.setIP( h1.intfs[ 0 ], '192.168.123.2', '/24' )
-   # Start network using kernel datapath
-   controller.cmdPrint( cname + ' ' + cargs + '&' )
-   switch.cmdPrint( 'dpctl deldp nl:0' )
-   switch.cmdPrint( 'dpctl adddp nl:0' )
-   for intf in switch.intfs:
+    # Create Network
+    print "*** creating Nodes"
+    controller = Node( 'c0', inNamespace=False )
+    switch = Node( 's0', inNamespace=False )
+    h0 = Node( 'h0' )
+    h1 = Node( 'h1' )
+    print "*** creating links"
+    createLink( node1=h0, port1=0, node2=switch, port2=0 )
+    createLink( node1=h1, port1=0, node2=switch, port2=1 )
+    # Configure hosts
+    print "*** configuring hosts"
+    h0.setIP( h0.intfs[ 0 ], '192.168.123.1', '/24' )
+    h1.setIP( h1.intfs[ 0 ], '192.168.123.2', '/24' )
+    print h0
+    print h1
+    # Start network using kernel datapath
+    controller.cmdPrint( cname + ' ' + cargs + '&' )
+    switch.cmdPrint( 'dpctl deldp nl:0' )
+    switch.cmdPrint( 'dpctl adddp nl:0' )
+    for intf in switch.intfs.values():
       switch.cmdPrint( 'dpctl addif nl:0 ' + intf )
-   switch.cmdPrint( 'ofprotocol nl:0 tcp:localhost &')
-   # Run test
-   h0.cmdPrint( 'ping -c1 ' + h1.IP() )
-   # Stop network
-   controller.cmdPrint( 'kill %' + cname)
-   switch.cmdPrint( 'dpctl deldp nl:0' )
-   switch.cmdPrint( 'kill %ofprotocol' )
-   switch.stop()
-   controller.stop()
+    switch.cmdPrint( 'ofprotocol nl:0 tcp:localhost &')
+    # Run test
+    print h0.cmd( 'ping -c1 ' + h1.IP() )
+    # Stop network
+    controller.cmdPrint( 'kill %' + cname)
+    switch.cmdPrint( 'dpctl deldp nl:0' )
+    switch.cmdPrint( 'kill %ofprotocol' )
+    switch.stop()
+    controller.stop()
    
 if __name__ == '__main__':
-   init()   
-   scratchNet()
+    info( '*** Scratch network demo\n' )
+    init()   
+    scratchNet()
diff --git a/mininet/net.py b/mininet/net.py
index 00abdda254c49190f65fd339a268398559e73df9..96da89971fb89d0fa61e36afcc6427e64522fca5 100755
--- a/mininet/net.py
+++ b/mininet/net.py
@@ -81,10 +81,10 @@
 from time import sleep
 
 from mininet.cli import CLI
-from mininet.log import info, error
+from mininet.log import info, error, debug
 from mininet.node import KernelSwitch, OVSKernelSwitch
 from mininet.util import quietRun, fixLimits
-from mininet.util import makeIntfPair, moveIntf, macColonHex
+from mininet.util import createLink, macColonHex
 from mininet.xterm import cleanUpScreens, makeXterms
 
 DATAPATHS = [ 'kernel' ] #[ 'user', 'kernel' ]
@@ -150,8 +150,6 @@ def addHost( self, name, defaultMac=None, defaultIp=None ):
            defaultIp: default IP address for intf 0
            returns: added host"""
         host = self.host( name )
-        # for now, assume one interface per host.
-        host.intfs.append( name + '-eth0' )
         self.hosts.append( host )
         self.nameToNode[ name ] = host
         # May wish to add this to actual object
@@ -175,31 +173,6 @@ def addSwitch( self, name, defaultMac=None ):
         self.nameToNode[ name ] = sw
         return sw
 
-    def addLink( self, src, srcPort, dst, dstPort ):
-        """Add link.
-           src: source Node
-           srcPort: source port
-           dst: destination Node
-           dstPort: destination port"""
-        srcIntf = src.intfName( srcPort )
-        dstIntf = dst.intfName( dstPort )
-        makeIntfPair( srcIntf, dstIntf )
-        src.intfs.append( srcIntf )
-        dst.intfs.append( dstIntf )
-        src.ports[ srcPort ] = srcIntf
-        dst.ports[ dstPort ] = dstIntf
-        #info( '\n' )
-        #info( 'added intf %s to src node %x\n' % ( srcIntf, src ) )
-        #info( 'added intf %s to dst node %x\n' % ( dstIntf, dst ) )
-        if src.inNamespace:
-            #info( 'moving src w/inNamespace set\n' )
-            moveIntf( srcIntf, src )
-        if dst.inNamespace:
-            #info( 'moving dst w/inNamespace set\n' )
-            moveIntf( dstIntf, dst )
-        src.connection[ srcIntf ] = ( dst, dstIntf )
-        dst.connection[ dstIntf ] = ( src, srcIntf )
-
     def addController( self, controller ):
         """Add controller.
            controller: Controller class"""
@@ -315,7 +288,7 @@ def buildFromTopo( self, topo ):
         for srcId, dstId in sorted( topo.edges() ):
             src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
             srcPort, dstPort = topo.port( srcId, dstId )
-            self.addLink( src, srcPort, dst, dstPort )
+            createLink( src, srcPort, dst, dstPort )
             info( '(%s, %s) ' % ( src.name, dst.name ) )
         info( '\n' )
 
@@ -368,7 +341,7 @@ def start( self ):
             controller.start()
         info( '*** Starting %s switches\n' % len( self.switches ) )
         for switch in self.switches:
-            info( switch.name )
+            info( switch.name + ' ')
             switch.start( self.controllers )
         info( '\n' )
 
@@ -474,13 +447,14 @@ def iperf( self, hosts=None, l4Type='TCP', udpBw='10M',
            l4Type: string, one of [ TCP, UDP ]
            verbose: verbose printing
            returns: results two-element array of server and client speeds"""
+        log = info if verbose else debug
         if not hosts:
             hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ]
         else:
             assert len( hosts ) == 2
         host0, host1 = hosts
-        info( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
-        info( "%s and %s\n" % ( host0.name, host1.name ) )
+        log( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
+        log( "%s and %s\n" % ( host0.name, host1.name ) )
         host0.cmd( 'killall -9 iperf' )
         iperfArgs = 'iperf '
         bwArgs = ''
@@ -490,19 +464,16 @@ def iperf( self, hosts=None, l4Type='TCP', udpBw='10M',
         elif l4Type != 'TCP':
             raise Exception( 'Unexpected l4 type: %s' % l4Type )
         server = host0.cmd( iperfArgs + '-s &' )
-        if verbose:
-            info( '%s\n' % server )
+        log( '%s\n' % server )
         client = host1.cmd( iperfArgs + '-t 5 -c ' + host0.IP() + ' ' +
                            bwArgs )
-        if verbose:
-            info( '%s\n' % client )
+        log( '%s\n' % client )
         server = host0.cmd( 'killall -9 iperf' )
-        if verbose:
-            info( '%s\n' % server )
+        log( '%s\n' % server )
         result = [ self._parseIperf( server ), self._parseIperf( client ) ]
         if l4Type == 'UDP':
             result.insert( 0, udpBw )
-        info( '*** Results: %s\n' % result )
+        log( '*** Results: %s\n' % result )
         return result
 
     def iperfUdp( self, udpBw='10M' ):
diff --git a/mininet/node.py b/mininet/node.py
index 8153e84fc1d239061c3e5c2bc9323e3e35a4b18c..eb60b85a0086fd1dae12dc795774b27859de5a09 100644
--- a/mininet/node.py
+++ b/mininet/node.py
@@ -40,8 +40,8 @@
 import signal
 import select
 
-from mininet.log import info, error
-from mininet.util import quietRun
+from mininet.log import info, error, debug
+from mininet.util import quietRun, moveIntf
 
 
 class Node( object ):
@@ -72,13 +72,13 @@ def __init__( self, name, inNamespace=True ):
         self.inToNode[ self.stdin.fileno() ] = self
         self.pid = self.shell.pid
         self.intfCount = 0
-        self.intfs = [] # list of interface names, as strings
+        self.intfs = {} # dict of port numbers to interface names
+        self.ports = {} # dict of interface names to port numbers
+                        # 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.ports = {} # dict of ints to interface strings
-                        # replace with Port object, eventually
 
     @classmethod
     def fdToNode( cls, fd ):
@@ -145,11 +145,13 @@ def sendInt( self ):
         "Send ^C, hopefully interrupting an interactive subprocess."
         self.write( chr( 3 ) )
 
-    def waitOutput( self ):
+    def waitOutput( self, verbose=False ):
         """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."""
+           the output, including trailing newline.
+           verbose: print output interactively"""
+        log = info if verbose else debug
         assert self.waiting
         output = ''
         while True:
@@ -157,38 +159,60 @@ def waitOutput( self ):
             data = self.read( 1024 )
             if len( data ) > 0  and data[ -1 ] == chr( 0177 ):
                 output += data[ :-1 ]
+                log( output )
                 break
             else:
                 output += data
         self.waiting = False
         return output
 
-    def cmd( self, cmd ):
+    def cmd( self, cmd, verbose=False ):
         """Send a command, wait for output, and return it.
            cmd: string"""
+        log = info if verbose else debug
+        log( '*** %s : %s', self.name, cmd )
         self.sendCmd( cmd )
-        return self.waitOutput()
+        return self.waitOutput( verbose )
 
     def cmdPrint( self, cmd ):
         """Call cmd and printing its output
            cmd: string"""
-        #info( '*** %s : %s', self.name, cmd )
-        result = self.cmd( cmd )
-        #info( '%s\n', result )
-        return result
+        return self.cmd( cmd, verbose=True )
 
     # 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 intfName( self, n ):
-        "Construct a canonical interface name node-intf for interface N."
+        "Construct a canonical interface name node-ethN for interface n."
         return self.name + '-eth' + repr( n )
 
     def newIntf( self ):
         "Reserve and return a new interface name."
         intfName = self.intfName( self.intfCount )
         self.intfCount += 1
-        self.intfs += [ intfName ]
         return intfName
 
+    def addIntf( self, intf, port ):
+        """Add an interface.
+           intf: interface name (nodeN-ethM)
+           port: port number (typically OpenFlow port number)"""
+        self.intfs[ port ] = intf
+        self.ports[ intf ] = port
+        #info( '\n' )
+        #info( 'added intf %s to node %x\n' % ( srcIntf, src ) )
+        if self.inNamespace:
+            #info( 'moving w/inNamespace set\n' )
+            moveIntf( intf, self )
+
+    def connect( self, intf, dstNode, dstIntf ):
+        "Register connection of intf to dstIntf on dstNode."
+        self.connection[ intf ] = ( dstNode, dstIntf )
+
     def setMAC( self, intf, mac ):
         """Set the MAC address for an interface.
            mac: MAC address as string"""
@@ -206,9 +230,9 @@ def setARP( self, ip, mac ):
 
     def setIP( self, intf, ip, bits ):
         """Set the IP address for an interface.
-           intf: string, interface name
+           intf: interface name
            ip: IP address as a string
-           bits:"""
+           bits: prefix length of form /24"""
         result = self.cmd( [ 'ifconfig', intf, ip + bits, 'up' ] )
         self.ips[ intf ] = ip
         return result
@@ -226,20 +250,19 @@ def setDefaultRoute( self, intf ):
         return self.cmd( 'route add default ' + intf )
 
     def IP( self ):
-        "Return IP address of first interface"
-        if len( self.intfs ) > 0:
-            return self.ips.get( self.intfs[ 0 ], None )
+        "Return IP address of interface 0"
+        return self.ips.get( self.intfs.get( 0 , None ), None )
 
-    def intfIsUp( self ):
-        "Check if one of our interfaces is up."
-        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
+    def intfIsUp( self, port ):
+        """Check if interface for a given port number is up.
+           port: port number"""
+        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ port ] )
 
     # Other methods
     def __str__( self ):
         result = self.name + ':'
-        if self.IP():
-            result += ' IP=' + self.IP()
-        result += ' intfs=' + ','.join( self.intfs )
+        result += ' IP=' + repr( self.IP() )
+        result += ' intfs=' + ','.join( sorted( self.intfs.values() ) )
         result += ' waiting=' + repr( self.waiting )
         return result
 
@@ -282,17 +305,17 @@ def __init__( self, name ):
     def start( self, controllers ):
         """Start OpenFlow reference user datapath.
            Log to /tmp/sN-{ofd,ofp}.log.
-           controllers: dict of controller names to objects"""
+           controllers: list of controller objects"""
         controller = controllers[ 0 ]
         ofdlog = '/tmp/' + self.name + '-ofd.log'
         ofplog = '/tmp/' + self.name + '-ofp.log'
         self.cmd( 'ifconfig lo up' )
-        intfs = self.intfs
+        intfs = sorted( self.intfs.values() )
 
-        self.cmdPrint( 'ofdatapath -i ' + ','.join( intfs ) +
+        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
             ' punix:/tmp/' + self.name +
             ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
-        self.cmdPrint( 'ofprotocol unix:/tmp/' + self.name +
+        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
             ' tcp:' + controller.IP() + ' --fail=closed' +
             ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
 
@@ -322,20 +345,20 @@ def start( self, controllers ):
         # Delete local datapath if it exists;
         # then create a new one monitoring the given interfaces
         quietRun( 'dpctl deldp nl:%i' % self.dp )
-        self.cmdPrint( 'dpctl adddp nl:%i' % self.dp )
+        self.cmd( 'dpctl adddp nl:%i' % self.dp )
         if self.defaultMac:
             intf = 'of%i' % self.dp
             self.cmd( [ 'ifconfig', intf, 'hw', 'ether', self.defaultMac ] )
 
-        if len( self.ports ) != max( self.ports.keys() ) + 1:
+        if len( self.intfs ) != max( self.intfs ) + 1:
             raise Exception( 'only contiguous, zero-indexed port ranges'
-                            'supported: %s' % self.ports )
-        intfs = [ self.ports[ port ] for port in self.ports.keys() ]
-        self.cmdPrint( 'dpctl addif nl:' + str( self.dp ) + ' ' +
+                            'supported: %s' % self.intfs )
+        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
+        self.cmd( 'dpctl addif nl:' + str( self.dp ) + ' ' +
             ' '.join( intfs ) )
         # Run protocol daemon
         controller = controllers[ 0 ]
-        self.cmdPrint( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
+        self.cmd( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
                       controller.IP() + ':' +
                       str( controller.port ) +
                       ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
@@ -345,11 +368,11 @@ def stop( self ):
         "Terminate kernel datapath."
         quietRun( 'dpctl deldp nl:%i' % self.dp )
         # In theory the interfaces should go away after we shut down.
-        # However, this takes time, so we're better off to remove them
+        # 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.
         self.cmd( 'kill %ofprotocol' )
-        for intf in self.intfs:
+        for intf in self.intfs.values():
             quietRun( 'ip link del ' + intf )
             info( '.' )
 
@@ -374,21 +397,21 @@ def start( self, controllers ):
         # Delete local datapath if it exists;
         # then create a new one monitoring the given interfaces
         quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
-        self.cmdPrint( 'ovs-dpctl add-dp dp%i' % self.dp )
+        self.cmd( 'ovs-dpctl add-dp dp%i' % self.dp )
         if self.defaultMac:
             intf = 'dp' % self.dp
             mac = self.defaultMac
             self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
 
-        if len( self.ports ) != max( self.ports.keys() ) + 1:
+        if len( self.intfs ) != max( self.intfs ) + 1:
             raise Exception( 'only contiguous, zero-indexed port ranges'
-                            'supported: %s' % self.ports )
-        intfs = [ self.ports[ port ] for port in self.ports.keys() ]
-        self.cmdPrint( 'ovs-dpctl add-if dp' + str( self.dp ) + ' ' +
+                            'supported: %s' % self.intfs )
+        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
+        self.cmd( 'ovs-dpctl add-if dp' + str( self.dp ) + ' ' +
                       ' '.join( intfs ) )
         # Run protocol daemon
         controller = controllers[ 0 ]
-        self.cmdPrint( 'ovs-openflowd dp' + str( self.dp ) + ' tcp:' +
+        self.cmd( 'ovs-openflowd dp' + str( self.dp ) + ' tcp:' +
                       controller.IP() + ':' +
                       ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
         self.execed = False
@@ -397,11 +420,11 @@ def stop( self ):
         "Terminate kernel datapath."
         quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
         # In theory the interfaces should go away after we shut down.
-        # However, this takes time, so we're better off to remove them
+        # 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.
         self.cmd( 'kill %ovs-openflowd' )
-        for intf in self.intfs:
+        for intf in self.intfs.values():
             quietRun( 'ip link del ' + intf )
             info( '.' )
 
@@ -425,8 +448,8 @@ def start( self ):
            Log to /tmp/cN.log"""
         cout = '/tmp/' + self.name + '.log'
         if self.cdir is not None:
-            self.cmdPrint( 'cd ' + self.cdir )
-        self.cmdPrint( self.controller + ' ' + self.cargs +
+            self.cmd( 'cd ' + self.cdir )
+        self.cmd( self.controller + ' ' + self.cargs +
             ' 1> ' + cout + ' 2> ' + cout + ' &' )
         self.execed = False
 
diff --git a/mininet/util.py b/mininet/util.py
index 221c85e4db5cf47be186cc7326ee0edd5b9cb387..3bec7f59616c11bd4caecef0be4810935ea0c34d 100644
--- a/mininet/util.py
+++ b/mininet/util.py
@@ -15,7 +15,7 @@ def run( cmd ):
 def checkRun( cmd ):
     """Simple interface to subprocess.check_call()
        cmd: list of command params"""
-    check_call( cmd.split( ' ' ) )
+    return check_call( cmd.split( ' ' ) )
 
 def quietRun( cmd ):
     """Run a command, routing stderr to stdout, and return the output.
@@ -63,7 +63,7 @@ def makeIntfPair( intf1, intf2 ):
     quietRun( 'ip link del ' + intf2 )
     # Create new pair
     cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2
-    return checkRun( cmd )
+    return quietRun( cmd )
 
 def retry( retries, delaySecs, fn, *args, **keywords ):
     """Try something several times before giving up.
@@ -101,19 +101,20 @@ def moveIntf( intf, node, printError=False, retries=3, delaySecs=0.001 ):
        printError: if true, print error"""
     retry( retries, delaySecs, moveIntfNoRetry, intf, node, printError )
 
-def createLink( node1, node2, retries=10, delaySecs=0.001 ):
+def createLink( node1, port1, node2, port2 ):
     """Create a link between nodes, making an interface for each.
        node1: Node object
-       node2: Node object"""
-    intf1 = node1.newIntf()
-    intf2 = node2.newIntf()
+       port1: node1 port number
+       node2: Node object
+       port2: node2 port number
+       returns: intf1 name, intf2 name"""
+    intf1 = node1.intfName( port1 )
+    intf2 = node2.intfName( port2 )
     makeIntfPair( intf1, intf2 )
-    if node1.inNamespace:
-        retry( retries, delaySecs, moveIntf, intf1, node1 )
-    if node2.inNamespace:
-        retry( retries, delaySecs, moveIntf, intf2, node2 )
-    node1.connection[ intf1 ] = ( node2, intf2 )
-    node2.connection[ intf2 ] = ( node1, intf1 )
+    node1.addIntf( intf1, port1 )
+    node2.addIntf( intf2, port2 )
+    node1.connect( intf1, node2, intf2 )
+    node2.connect( intf2, node1, intf1 )
     return intf1, intf2
 
 def fixLimits():
@@ -141,7 +142,8 @@ def macColonHex( mac ):
 
 def ipStr( ip ):
     """Generate IP address string
-       returns: ip addr string"""
+       ip: unsigned int of form x << 16 | y << 8 | z
+       returns: ip address string 10.x.y.z """
     hi = ( ip & 0xff0000 ) >> 16
     mid = ( ip & 0xff00 ) >> 8
     lo = ip & 0xff