diff --git a/examples/ripcordtest.py b/examples/ripcordtest.py index 3f18dba5dacc790cbaddeb2d480c5a3958322559..b27b08672161150eb59cb2c5cabefbd7eab2f652 100755 --- a/examples/ripcordtest.py +++ b/examples/ripcordtest.py @@ -1,61 +1,513 @@ #!/usr/bin/python -"A FatTree network, using Brandon Heller's ripcord system." +'''Run a FatTree network from the Ripcord project. + +For verbose printout, set LOG_LEVEL_DEFAULT in mininet.py to logging.INFO. +''' + +import os +import re +from subprocess import call +import sys +from time import sleep + +from mininet.mininet import Switch, Controller, Host, lg +from mininet.mininet import init, quietRun, checkRun, retry, MOVEINTF_DELAY -import ripcord from ripcord.topo import FatTreeTopo -from mininet.mininet import init, Controller, Network, Host, nameGen, Cli -from mininet.mininet import createLink, flush - -class NoxController( Controller ): - def __init__( self, name, kernel=False, **kwargs ): - Controller.__init__( self, name, kernel=kernel, - controller='nox_core', - cargs='-v --libdir=/usr/local/lib -i ptcp:', - cdir='/usr/local/bin', **kwargs) - -class FatTree( Network ): - "A customized Network that uses ripcord's FatTree." - def __init__( self, depth, **kwargs ): - self.depth = depth - Network.__init__( self, **kwargs ) - def makeNet( self, controller ): - ft = FatTreeTopo( self.depth ) - graph = ft.g - switches = [] - hosts = [] - hostnames = nameGen( 'h' ) - switchnames = nameGen( 's' ) - dpnames = nameGen( 'nl:') - graphToMini = {} - miniToGraph = {} - # Create nodes - for graphNode in graph.nodes(): - isLeaf = len( graph.neighbors( graphNode ) ) == 1 - if isLeaf: - mininetNode = Host( hostnames.next() ) - hosts += [ mininetNode ] - else: - mininetNode = self.Switch( switchnames.next(), dpnames.next() ) - switches += [ mininetNode ] - print mininetNode.name, ; flush() - miniToGraph[ mininetNode ] = graphNode - graphToMini[ graphNode ] = mininetNode - print - print "*** Creating links" - for switch in switches: - currentNeighbors = [ switch.connection[ intf ][ 0 ] - for intf in switch.intfs ] - for neighbor in graph.neighbors( miniToGraph[ switch ] ): - miniNeighbor = graphToMini[ neighbor ] - if miniNeighbor not in currentNeighbors: - print '.', ; flush() - createLink( switch, graphToMini[ neighbor ] ) - print - return switches, hosts - +def make_veth_pair(intf1, intf2): + '''Create a veth pair connecting intf1 and intf2. + + @param intf1 string, interface name + @param intf2 string, interface name + ''' + # Delete any old interfaces with the same names + quietRun('ip link del ' + intf1) + quietRun('ip link del ' + intf2) + # Create new pair + cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2 + #lg.info('running command: %s\n' % cmd) + return checkRun(cmd) + + +def move_intf(intf, node): + '''Move interface to node. + + @param intf string interface name + @param node Node object + + @return success boolean, did operation complete? + ''' + cmd = 'ip link set ' + intf + ' netns ' + repr(node.pid) + #lg.info('running command: %s\n' % cmd) + quietRun(cmd) + #lg.info(' output: %s\n' % output) + links = node.cmd('ip link show') + if not intf in links: + lg.error('*** Error: move_intf: % not successfully moved to %s:\n' % + (intf, node.name)) + return False + return True + + +class Mininet(object): + '''Network emulation with hosts spawned in network namespaces.''' + + def __init__(self, topo, switch, host, controller, cparams, + build = True, xterms = False, cleanup = False, + in_namespace = False, switch_is_kernel = True): + '''Create Mininet object. + + @param topo Topo object + @param switch Switch class + @param host Host class + @param controller Controller class + @param cparams ControllerParams object + @param now build now? + @param xterms if build now, spawn xterms? + @param cleanup if build now, cleanup before creating? + @param in_namespace spawn switches and hosts in their own namespace? + ''' + self.topo = topo + self.switch = switch + self.host = host + self.controller = controller + self.cparams = cparams + self.nodes = {} # dpid to Node{Host, Switch} objects + self.controllers = {} # controller name to Controller objects + self.dps = 0 # number of created kernel datapaths + self.in_namespace = in_namespace + self.switch_is_kernel = switch_is_kernel + + self.kernel = True #temporary! + + if build: + self.build(xterms, cleanup) + + def _add_host(self, dpid): + '''Add host. + + @param dpid DPID of host to add + ''' + host = self.host('h_' + self.topo.name(dpid)) + # for now, assume one interface per host. + host.intfs.append('h_' + self.topo.name(dpid) + '-eth0') + self.nodes[dpid] = host + #lg.info('%s ' % host.name) + + def _add_switch(self, dpid): + ''' + @param dpid DPID of switch to add + ''' + sw = None + if self.switch_is_kernel: + sw = self.switch('s_' + self.topo.name(dpid), 'nl:' + str(self.dps)) + self.dps += 1 + else: + sw = self.switch('s_' + self.topo.name(dpid)) + self.nodes[dpid] = sw + + def _add_link(self, src, dst): + '''Add link. + + @param src source DPID + @param dst destination DPID + ''' + src_port, dst_port = self.topo.port(src, dst) + src_node = self.nodes[src] + dst_node = self.nodes[dst] + src_intf = src_node.intfName(src_port) + dst_intf = dst_node.intfName(dst_port) + make_veth_pair(src_intf, dst_intf) + src_node.intfs.append(src_intf) + dst_node.intfs.append(dst_intf) + #lg.info('\n') + #lg.info('added intf %s to src node %x\n' % (src_intf, src)) + #lg.info('added intf %s to dst node %x\n' % (dst_intf, dst)) + if src_node.inNamespace: + #lg.info('moving src w/inNamespace set\n') + retry(3, MOVEINTF_DELAY, move_intf, src_intf, src_node) + if dst_node.inNamespace: + #lg.info('moving dst w/inNamespace set\n') + retry(3, MOVEINTF_DELAY, move_intf, dst_intf, dst_node) + src_node.connection[src_intf] = (dst_node, dst_intf) + dst_node.connection[dst_intf] = (src_node, src_intf) + + def _add_controller(self, controller): + '''Add controller. + + @param controller Controller class + ''' + controller = self.controller('c0', kernel = self.kernel) + self.controllers['c0'] = controller + + # Control network support: + # + # Create an explicit control network. Currently this is only + # used by the user datapath configuration. + # + # Notes: + # + # 1. If the controller and switches are in the same (e.g. root) + # namespace, they can just use the loopback connection. + # We may wish to do this for the user datapath as well as the + # kernel datapath. + # + # 2. If we can get unix domain sockets to work, we can use them + # instead of an explicit control network. + # + # 3. Instead of routing, we could bridge or use 'in-band' control. + # + # 4. Even if we dispense with this in general, it could still be + # useful for people who wish to simulate a separate control + # network (since real networks may need one!) + + def _configureControlNetwork(self): + '''Configure control network.''' + self._configureRoutedControlNetwork() + + def _configureRoutedControlNetwork(self): + '''Configure a routed control network on controller and switches. + + For use with the user datapath only right now. + + @todo(brandonh) Test this code and verify that user-space works! + ''' + # params were: controller, switches, ips + + controller = self.controllers['c0'] + lg.info('%s <-> ' % controller.name) + for switch_dpid in self.topo.switches(): + switch = self.nodes[switch_dpid] + lg.info('%s ' % switch.name) + sip = self.topo.ip(switch_dpid)#ips.next() + sintf = switch.intfs[0] + node, cintf = switch.connection[sintf] + if node != controller: + lg.error('*** Error: switch %s not connected to correct' + 'controller' % + switch.name) + exit(1) + controller.setIP(cintf, self.cparams.ip, '/' + + self.cparams.subnet_size) + switch.setIP(sintf, sip, '/' + self.cparams.subnet_size) + controller.setHostRoute(sip, cintf) + switch.setHostRoute(self.cparams.ip, sintf) + lg.info('\n') + lg.info('*** Testing control network\n') + while not controller.intfIsUp(controller.intfs[0]): + lg.info('*** Waiting for %s to come up\n', controller.intfs[0]) + sleep(1) + for switch_dpid in self.topo.switches(): + switch = self.nodes[switch_dpid] + while not switch.intfIsUp(switch.intfs[0]): + lg.info('*** Waiting for %s to come up\n' % switch.intfs[0]) + sleep(1) + if self.ping_test(hosts=[switch, controller]) != 0: + lg.error('*** Error: control network test failed\n') + exit(1) + lg.info('\n') + + def _config_hosts( self ): + '''Configure a set of hosts.''' + # params were: hosts, ips + for host_dpid in self.topo.hosts(): + host = self.nodes[host_dpid] + hintf = host.intfs[0] + host.setIP(hintf, self.topo.ip(host_dpid), + '/' + str(self.cparams.subnet_size)) + host.setDefaultRoute(hintf) + # You're low priority, dude! + quietRun('renice +18 -p ' + repr(host.pid)) + lg.info('%s ', host.name) + lg.info('\n') + + def build(self, xterms, cleanup): + '''Build mininet. + + At the end of this function, everything should be connected and up. + + @param xterms spawn xterms on build? + @param cleanup cleanup before creating? + ''' + if cleanup: + pass # cleanup + # validate topo? + kernel = self.kernel + if kernel: + lg.info('*** Using kernel datapath\n') + else: + lg.info('*** Using user datapath\n') + lg.info('*** Adding controller\n') + self._add_controller(self.controller) + lg.info('*** Creating network\n') + lg.info('*** Adding hosts:\n') + for host in sorted(self.topo.hosts()): + self._add_host(host) + lg.info('0x%x ' % host) + lg.info('\n*** Adding switches:\n') + for switch in sorted(self.topo.switches()): + self._add_switch(switch) + lg.info('0x%x ' % switch) + lg.info('\n*** Adding edges: ') + for src, dst in sorted(self.topo.edges()): + self._add_link(src, dst) + lg.info('(0x%x, 0x%x) ' % (src, dst)) + lg.info('\n') + + if not kernel: + lg.info('*** Configuring control network\n') + self._configureControlNetwork() + + lg.info('*** Configuring hosts\n') + self._config_hosts() + + if xterms: + pass # build xterms + + def start(self): + '''Start controller and switches\n''' + lg.info('*** Starting controller\n') + self.controllers['c0'].start() + #for controller in self.controllers: + # controller.start() + lg.info('*** Starting %s switches\n' % len(self.topo.switches())) + for switch_dpid in self.topo.switches(): + switch = self.nodes[switch_dpid] + #lg.info('switch = %s' % switch) + lg.info('0x%x ' % switch_dpid) + switch.start(self.controllers['c0']) + lg.info('\n') + + def stop(self): + '''Stop the controller(s), switches and hosts\n''' + lg.info('*** Stopping %i hosts\n' % len(self.topo.hosts())) + for host_dpid in self.topo.hosts(): + host = self.nodes[host_dpid] + lg.info('%s ' % host.name) + host.terminate() + lg.info('\n') + lg.info('*** Stopping %i switches\n' % len(self.topo.switches())) + for switch_dpid in self.topo.switches(): + switch = self.nodes[switch_dpid] + lg.info('%s' % switch.name) + switch.stop() + lg.info('\n') + lg.info('*** Stopping controller\n') + #for controller in self.controllers.iteriterms(): + self.controllers['c0'].stop() + lg.info('*** Test complete\n') + + def run(self, test, **params): + '''Perform a complete start/test/stop cycle.''' + self.start() + lg.info('*** Running test\n') + result = test(self, **params) + self.stop() + return result + + @staticmethod + def _parse_ping(pingOutput): + '''Parse ping output and return packets sent, received.''' + r = r'(\d+) packets transmitted, (\d+) received' + m = re.search( r, pingOutput ) + if m == None: + lg.error('*** Error: could not parse ping output: %s\n' % + pingOutput) + exit(1) + sent, received = int(m.group(1)), int(m.group(2)) + return sent, received + + def ping_test(self, hosts = None, verbose = False): + '''Ping between all specified hosts. + + @param hosts list of host DPIDs + @param verbose verbose printing + @return ploss packet loss percentage + ''' + #self.start() + # check if running - only then, start? + packets = 0 + lost = 0 + if not hosts: + hosts = self.topo.hosts() + for node_dpid in hosts: + node = self.nodes[node_dpid] + if verbose: + lg.info('%s -> ' % node.name) + for dest_dpid in hosts: + dest = self.nodes[dest_dpid] + if node != dest: + result = node.cmd('ping -c1 ' + dest.IP()) + sent, received = self._parse_ping(result) + packets += sent + if received > sent: + lg.error('*** Error: received too many packets') + lg.error('%s' % result) + node.cmdPrint('route') + exit( 1 ) + lost += sent - received + if verbose: + lg.info(('%s ' % dest.name) if received else 'X ') + if verbose: + lg.info('\n') + ploss = 100 * lost/packets + if verbose: + lg.info('%d%% packet loss (%d/%d lost)\n' % (ploss, lost, packets)) + #flush() + #self.stop() + return ploss + + def interact(self): + '''Start network and run our simple CLI.''' + self.run(Cli) + + +class Cli(object): + '''Simple command-line interface to talk to nodes.''' + cmds = ['?', 'help', 'nodes', 'net', 'sh', 'ping_all', 'exit', \ + 'ping_pair'] #'iperf' + + def __init__(self, mininet): + self.mn = mininet + self.nodemap = {} # map names to Node objects + for node in self.mn.nodes.values(): + self.nodemap[node.name] = node + self.nodemap['c0'] = self.mn.controllers['c0'] + self.nodelist = self.nodemap.values() + self.run() + + # Commands + def help(self, args): + '''Semi-useful help for CLI.''' + help_str = 'Available commands are:' + str(self.cmds) + '\n' + \ + 'You may also send a command to a node using:\n' + \ + ' <node> command {args}\n' + \ + 'For example:\n' + \ + ' mininet> h0 ifconfig\n' + \ + '\n' + \ + 'The interpreter automatically substitutes IP ' + \ + 'addresses\n' + \ + 'for node names, so commands like\n' + \ + ' mininet> h0 ping -c1 h1\n' + \ + 'should work.\n' + \ + '\n\n' + \ + 'Interactive commands are not really 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' + print(help_str) + + def nodes(self, args): + '''List all nodes.''' + lg.info('available nodes are: \n%s\n', + ' '.join([node.name for node in sorted(self.nodelist)])) + + def net(self, args): + '''List network connection.''' + for switch_dpid in self.mn.topo.switches(): + switch = self.mn.nodes[switch_dpid] + lg.info('%s <->', switch.name) + for intf in switch.intfs: + node, remoteIntf = switch.connection[intf] + lg.info(' %s' % node.name) + lg.info('\n') + + def sh(self, args): + '''Run an external shell command''' + call( [ 'sh', '-c' ] + args ) + + def ping_all(self, args): + '''Ping between all hosts.''' + self.mn.ping_test(verbose = True) + + def ping_pair(self, args): + '''Ping between first two hosts, useful for testing.''' + hosts_unsorted = sorted(self.mn.topo.hosts()) + hosts = [hosts_unsorted[0], hosts_unsorted[1]] + self.mn.ping_test(hosts = hosts, verbose = True) + + def run(self): + '''Read and execute commands.''' + lg.warn('*** Starting CLI:\n') + while True: + lg.warn('mininet> ') + input = sys.stdin.readline() + if input == '': + break + if input[-1] == '\n': + input = input[:-1] + cmd = input.split(' ') + first = cmd[0] + rest = cmd[1:] + if first in self.cmds and hasattr(self, first): + getattr(self, first)(rest) + elif first in self.nodemap and rest != []: + node = self.nodemap[first] + # Substitute IP addresses for node names in command + rest = [self.nodemap[arg].IP() if arg in self.nodemap else arg + for arg in rest] + rest = ' '.join(rest) + # Interactive commands don't work yet, and + # there are still issues with control-c + lg.warn('*** %s: running %s\n' % (node.name, rest)) + node.sendCmd( rest ) + while True: + try: + done, data = node.monitor() + lg.info('%s\n' % data) + if done: + break + except KeyboardInterrupt: + node.sendInt() + elif first == '': + pass + elif first in ['exit', 'quit']: + break + elif first == '?': + self.help( rest ) + else: + lg.error('CLI: unknown node or command: < %s >\n' % first) + #lg.info('*** CLI: command complete\n') + return 'exited by user command' + + +class NOXController(Controller): + '''Controller to run a NOX application.''' + def __init__(self, name, nox_args = None, **kwargs): + '''Init. + + @param name name to give controller + @param nox_args list of args to use with NOX + ''' + if not nox_args: + nox_args = ['packetdump'] + nox_core_dir = os.environ['NOX_CORE_DIR'] + if not nox_core_dir: + raise Exception('please set NOX_CORE_DIR env var\n') + Controller.__init__(self, name, + controller = nox_core_dir + '/nox_core', + cargs = '--libdir=/usr/local/lib -v -i ptcp: ' + \ + ' '.join(nox_args), + cdir = nox_core_dir, **kwargs) + + +class ControllerParams(object): + '''Container for controller IP parameters.''' + def __init__(self, ip, subnet_size): + '''Init. + + @param ip integer, controller IP + @param subnet_size integer, ex 8 for slash-8, covering 17M + ''' + self.ip = ip + self.subnet_size = subnet_size + + if __name__ == '__main__': - init() - network = FatTree( depth=4, kernel=True, Controller=NoxController) - network.run( Cli ) + init() + controller_params = ControllerParams(0x0a000000, 8) # 10.0.0.0/8 + mn = Mininet(FatTreeTopo(4), Switch, Host, NOXController, + controller_params) + mn.interact() \ No newline at end of file