diff --git a/mininet/cli.py b/mininet/cli.py index f54d5c3..efb1808 100644 --- a/mininet/cli.py +++ b/mininet/cli.py @@ -148,6 +148,14 @@ class CLI( Cmd ): "Ping between first two hosts, useful for testing." self.mn.pingPair() + def do_pingallfull( self, _line ): + "Ping between first two hosts, returns all ping results." + self.mn.pingAllFull() + + def do_pingpairfull( self, _line ): + "Ping between first two hosts, returns all ping results." + self.mn.pingPairFull() + def do_iperf( self, line ): "Simple iperf TCP test between two (optionally specified) hosts." args = line.split() diff --git a/mininet/net.py b/mininet/net.py index f2b774a..066751c 100755 --- a/mininet/net.py +++ b/mininet/net.py @@ -428,9 +428,10 @@ class Mininet( object ): sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) return sent, received - def ping( self, hosts=None ): + def ping( self, hosts=None, timeout=None ): """Ping between all specified hosts. hosts: list of hosts + timeout: time to wait for a response, as string returns: ploss packet loss percentage""" # should we check if running? packets = 0 @@ -443,7 +444,10 @@ class Mininet( object ): output( '%s -> ' % node.name ) for dest in hosts: if node != dest: - result = node.cmd( 'ping -c1 ' + dest.IP() ) + opts = '' + if timeout: + opts = '-W %s' % timeout + result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) ) sent, received = self._parsePing( result ) packets += sent if received > sent: @@ -459,6 +463,61 @@ class Mininet( object ): ( ploss, lost, packets ) ) return ploss + @staticmethod + def _parsePingFull( pingOutput ): + "Parse ping output and return all data." + # Check for downed link + if 'connect: Network is unreachable' in pingOutput: + return (1, 0) + r = r'(\d+) packets transmitted, (\d+) received' + m = re.search( r, pingOutput ) + if m is None: + error( '*** Error: could not parse ping output: %s\n' % + pingOutput ) + return (1, 0, 0, 0, 0, 0) + sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) + r = r'rtt min/avg/max/mdev = ' + r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms' + m = re.search( r, pingOutput ) + rttmin = float( m.group( 1 ) ) + rttavg = float( m.group( 2 ) ) + rttmax = float( m.group( 3 ) ) + rttdev = float( m.group( 4 ) ) + return sent, received, rttmin, rttavg, rttmax, rttdev + + def pingFull( self, hosts=None, timeout=None ): + """Ping between all specified hosts and return all data. + hosts: list of hosts + timeout: time to wait for a response, as string + returns: all ping data; see function body.""" + # should we check if running? + # Each value is a tuple: (src, dsd, [all ping outputs]) + all_outputs = [] + if not hosts: + hosts = self.hosts + output( '*** Ping: testing ping reachability\n' ) + for node in hosts: + output( '%s -> ' % node.name ) + for dest in hosts: + if node != dest: + opts = '' + if timeout: + opts = '-W %s' % timeout + result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) ) + outputs = self._parsePingFull( result ) + sent, received, rttmin, rttavg, rttmax, rttdev = outputs + all_outputs.append( (node, dest, outputs) ) + output( ( '%s ' % dest.name ) if received else 'X ' ) + output( '\n' ) + output( "*** Results: \n" ) + for outputs in all_outputs: + src, dest, ping_outputs = outputs + sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs + output( " %s->%s: %s/%s, " % (src, dest, sent, received ) ) + output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" % + (rttmin, rttavg, rttmax, rttdev) ) + return all_outputs + def pingAll( self ): """Ping between all hosts. returns: ploss packet loss percentage""" @@ -470,6 +529,17 @@ class Mininet( object ): hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] return self.ping( hosts=hosts ) + def pingAllFull( self ): + """Ping between all hosts. + returns: ploss packet loss percentage""" + return self.pingFull() + + def pingPairFull( self ): + """Ping between first two hosts, useful for testing. + returns: ploss packet loss percentage""" + hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] + return self.pingFull( hosts=hosts ) + @staticmethod def _parseIperf( iperfOutput ): """Parse iperf output and return bandwidth. diff --git a/mininet/test/test_hifi.py b/mininet/test/test_hifi.py index 07437e8..ace7bb5 100755 --- a/mininet/test/test_hifi.py +++ b/mininet/test/test_hifi.py @@ -7,6 +7,8 @@ import unittest from mininet.net import Mininet from mininet.node import OVSKernelSwitch +from mininet.node import CPULimitedHost +from mininet.link import TCLink from mininet.topo import Topo from mininet.log import setLogLevel @@ -18,7 +20,11 @@ N = 2 class SingleSwitchOptionsTopo(Topo): "Single switch connected to n hosts." - def __init__(self, n=2, hopts={}, lopts={}): + def __init__(self, n=2, hopts=None, lopts=None): + if not hopts: + hopts = {} + if not lopts: + lopts = {} Topo.__init__(self, hopts=hopts, lopts=lopts) switch = self.addSwitch('s1') for h in range(n): @@ -31,31 +37,89 @@ class testOptionsTopo( unittest.TestCase ): def runOptionsTopoTest( self, n, hopts=None, lopts=None ): "Generic topology-with-options test runner." - mn = Mininet( SingleSwitchOptionsTopo( n=n, hopts=hopts ) ) + mn = Mininet( topo=SingleSwitchOptionsTopo( n=n, hopts=hopts, + lopts=lopts ), + host=CPULimitedHost, link=TCLink ) dropped = mn.run( mn.ping ) self.assertEqual( dropped, 0 ) + def assertWithinTolerance(self, measured, expected, tolerance_frac): + """Check that a given value is within a tolerance of expected + tolerance_frac: less-than-1.0 value; 0.8 would yield 20% tolerance. + """ + self.assertTrue( float(measured) >= float(expected) * tolerance_frac ) + self.assertTrue( float(measured) >= float(expected) * tolerance_frac ) + def testCPULimits( self ): - hopts = { 'cpu': 0.5 / N } - self.runOptionsTopoTest(N, hopts=hopts) + "Verify topology creation with CPU limits set for both schedulers." + CPU_FRACTION = 0.1 + CPU_TOLERANCE = 0.8 # CPU fraction below which test should fail + hopts = { 'cpu': CPU_FRACTION } + #self.runOptionsTopoTest( N, hopts=hopts ) + + mn = Mininet( SingleSwitchOptionsTopo( n=N, hopts=hopts ), + host=CPULimitedHost ) + mn.start() + results = mn.runCpuLimitTest( cpu=CPU_FRACTION ) + mn.stop() + for cpu in results: + self.assertWithinTolerance( cpu, CPU_FRACTION, CPU_TOLERANCE ) def testLinkBandwidth( self ): - lopts = { 'bw': 10, 'use_htb': True } - self.runOptionsTopoTest(N, lopts=lopts) + "Verify that link bandwidths are accurate within a bound." + BW = 5 # Mbps + BW_TOLERANCE = 0.8 # BW fraction below which test should fail + # Verify ability to create limited-link topo first; + lopts = { 'bw': BW, 'use_htb': True } + # Also verify correctness of limit limitng within a bound. + mn = Mininet( SingleSwitchOptionsTopo( n=N, lopts=lopts ), + link=TCLink ) + bw_strs = mn.run( mn.iperf ) + for bw_str in bw_strs: + bw = float( bw_str.split(' ')[0] ) + self.assertWithinTolerance( bw, BW, BW_TOLERANCE ) def testLinkDelay( self ): - lopts = { 'delay': '5ms', 'use_htb': True } - self.runOptionsTopoTest(N, lopts=lopts) + "Verify that link delays are accurate within a bound." + DELAY_MS = 15 + DELAY_TOLERANCE = 0.8 # Delay fraction below which test should fail + lopts = { 'delay': '%sms' % DELAY_MS, 'use_htb': True } + mn = Mininet( SingleSwitchOptionsTopo( n=N, lopts=lopts ), + link=TCLink ) + ping_delays = mn.run( mn.pingFull ) + test_outputs = ping_delays[0] + # Ignore unused variables below + # pylint: disable-msg=W0612 + node, dest, ping_outputs = test_outputs + sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs + self.assertEqual( sent, received ) + # pylint: enable-msg=W0612 + for rttval in [rttmin, rttavg, rttmax]: + # Multiply delay by 4 to cover there & back on two links + self.assertWithinTolerance( rttval, DELAY_MS * 4.0, + DELAY_TOLERANCE) def testLinkLoss( self ): - lopts = { 'loss': 10, 'use_htb': True } - self.runOptionsTopoTest(N, lopts=lopts) + "Verify that we see packet drops with a high configured loss rate." + LOSS_PERCENT = 99 + REPS = 1 + lopts = { 'loss': LOSS_PERCENT, 'use_htb': True } + mn = Mininet( topo=SingleSwitchOptionsTopo( n=N, lopts=lopts ), + host=CPULimitedHost, link=TCLink ) + # Drops are probabilistic, but the chance of no dropped packets is + # 1 in 100 million with 4 hops for a link w/99% loss. + dropped_total = 0 + mn.start() + for _ in range(REPS): + dropped_total += mn.ping(timeout='1') + mn.stop() + self.assertTrue(dropped_total > 0) - def testAllOptions( self ): - lopts = { 'bw': 10, 'delay': '5ms', 'loss': 10, 'use_htb': True } + def testMostOptions( self ): + "Verify topology creation with most link options and CPU limits." + lopts = { 'bw': 10, 'delay': '5ms', 'use_htb': True } hopts = { 'cpu': 0.5 / N } - self.runOptionsTopoTest(N, hopts=hopts, lopts=lopts) - + self.runOptionsTopoTest( N, hopts=hopts, lopts=lopts ) if __name__ == '__main__':