summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
authorshadi@chromium.org <shadi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-14 05:40:52 +0000
committershadi@chromium.org <shadi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-14 05:40:52 +0000
commit1e81311680250af51dcc56161231cd22f5debe9d (patch)
tree812ab67ba90f6204ef486fec0e6a6245d1f892f4 /media
parentc43f3ed89b9518a8fdb8d525cd1d216123a0d179 (diff)
downloadchromium_src-1e81311680250af51dcc56161231cd22f5debe9d.zip
chromium_src-1e81311680250af51dcc56161231cd22f5debe9d.tar.gz
chromium_src-1e81311680250af51dcc56161231cd22f5debe9d.tar.bz2
Constrained network scripts that configure a local network upload bandwidth,
latency, and packet loss on a specific port. These scripts will be used by the constrained network server (Issue 8528049) to serve files on a configured network for testing purposes. BUG=104254 TEST=Ran locally. Ran unit tests (more unittests will be added) Review URL: http://codereview.chromium.org/8566029 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@114355 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media')
-rwxr-xr-xmedia/tools/constrained_network_server/cn.py126
-rwxr-xr-xmedia/tools/constrained_network_server/traffic_control.py336
-rw-r--r--media/tools/constrained_network_server/traffic_control_test.py143
-rw-r--r--media/tools/constrained_network_server/traffic_control_unittest.py138
4 files changed, 743 insertions, 0 deletions
diff --git a/media/tools/constrained_network_server/cn.py b/media/tools/constrained_network_server/cn.py
new file mode 100755
index 0000000..c2fd877
--- /dev/null
+++ b/media/tools/constrained_network_server/cn.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A script for configuring constraint networks.
+
+Sets up a constrained network configuration on a specific port. Traffic on this
+port will be redirected to another local server port.
+
+The configuration includes bandwidth, latency, and packet loss.
+"""
+
+import collections
+import logging
+import optparse
+import traffic_control
+
+# Default logging is ERROR. Use --verbose to enable DEBUG logging.
+_DEFAULT_LOG_LEVEL = logging.ERROR
+
+Dispatcher = collections.namedtuple('Dispatcher', ['dispatch', 'requires_ports',
+ 'desc'])
+
+# Map of command names to traffic_control functions.
+COMMANDS = {
+ # Adds a new constrained network configuration.
+ 'add': Dispatcher(traffic_control.CreateConstrainedPort,
+ requires_ports=True, desc='Add a new constrained port.'),
+
+ # Deletes an existing constrained network configuration.
+ 'del': Dispatcher(traffic_control.DeleteConstrainedPort,
+ requires_ports=True, desc='Delete a constrained port.'),
+
+ # Deletes all constrained network configurations.
+ 'teardown': Dispatcher(traffic_control.TearDown,
+ requires_ports=False,
+ desc='Teardown all constrained ports.')
+}
+
+
+def _ParseArgs():
+ """Define and parse command-line arguments.
+
+ Returns:
+ tuple as (command, configuration):
+ command: one of the possible commands to setup, delete or teardown the
+ constrained network.
+ configuration: a map of constrained network properties to their values.
+ """
+ parser = optparse.OptionParser()
+
+ indent_first = parser.formatter.indent_increment
+ opt_width = parser.formatter.help_position - indent_first
+
+ cmd_usage = []
+ for s in COMMANDS:
+ cmd_usage.append('%*s%-*s%s' %
+ (indent_first, '', opt_width, s, COMMANDS[s].desc))
+
+ parser.usage = ('usage: %%prog {%s} [options]\n\n%s' %
+ ('|'.join(COMMANDS.keys()), '\n'.join(cmd_usage)))
+
+ parser.add_option('--port', type='int',
+ help='The port to apply traffic control constraints to.')
+ parser.add_option('--server-port', type='int',
+ help='Port to forward traffic on --port to.')
+ parser.add_option('--bandwidth', type='int',
+ help=('Bandwidth of the network in kbps. Default: '
+ '%defaultkbps.'))
+ parser.add_option('--latency', type='int',
+ help=('Latency (delay) added to each outgoing packet in '
+ 'ms. Default: %defaultms.'))
+ parser.add_option('--loss', type='int',
+ help=('Packet-loss percentage on outgoing packets. '
+ 'Default: %default%.'))
+ parser.add_option('--interface', type='string',
+ help=('Interface to setup constraints on. Use lo for a '
+ 'local client. Default: %default.'))
+ parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
+ default=False, help='Turn on verbose output.')
+ options, args = parser.parse_args()
+
+ _SetLogger(options.verbose)
+
+ # Check a valid command was entered
+ if not args or args[0].lower() not in COMMANDS:
+ parser.error('Please specify a command {%s}.' % '|'.join(COMMANDS.keys()))
+ user_cmd = args[0].lower()
+
+ # Check if required options are available
+ if COMMANDS[user_cmd].requires_ports:
+ if not (options.port and options.server_port):
+ parser.error('Please provide port and server-port values.')
+
+ config = {
+ 'port': options.port,
+ 'server_port': options.server_port,
+ 'interface': options.interface,
+ 'latency': options.latency,
+ 'bandwidth': options.bandwidth,
+ 'loss': options.loss
+ }
+ return user_cmd, config
+
+
+def _SetLogger(verbose):
+ log_level = _DEFAULT_LOG_LEVEL
+ if verbose:
+ log_level = logging.DEBUG
+ logging.basicConfig(level=log_level, format='%(message)s')
+
+
+def Main():
+ """Get the command and configuration of the network to set up."""
+ user_cmd, config = _ParseArgs()
+
+ try:
+ COMMANDS[user_cmd].dispatch(config)
+ except traffic_control.TrafficControlError as e:
+ logging.error('Error: %s\n\nOutput: %s', e.msg, e.error)
+
+
+if __name__ == '__main__':
+ Main()
diff --git a/media/tools/constrained_network_server/traffic_control.py b/media/tools/constrained_network_server/traffic_control.py
new file mode 100755
index 0000000..d6527ff
--- /dev/null
+++ b/media/tools/constrained_network_server/traffic_control.py
@@ -0,0 +1,336 @@
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Traffic control library for constraining the network configuration on a port.
+
+The traffic controller sets up a constrained network configuration on a port.
+Traffic to the constrained port is forwarded to a specified server port.
+"""
+
+import logging
+import re
+import subprocess
+
+# The maximum bandwidth limit.
+_DEFAULT_MAX_BANDWIDTH_KBPS = 1000000
+
+
+class TrafficControlError(BaseException):
+ """Exception raised for errors in traffic control library.
+
+ Attributes:
+ msg: User defined error message.
+ cmd: Command for which the exception was raised.
+ returncode: Return code of running the command.
+ stdout: Output of running the command.
+ stderr: Error output of running the command.
+ """
+
+ def __init__(self, msg, cmd=None, returncode=None, output=None,
+ error=None):
+ BaseException.__init__(self, msg)
+ self.msg = msg
+ self.cmd = cmd
+ self.returncode = returncode
+ self.output = output
+ self.error = error
+
+
+def CreateConstrainedPort(config):
+ """Creates a new constrained port.
+
+ Imposes packet level constraints such as bandwidth, latency, and packet loss
+ on a given port using the specified configuration dictionary. Traffic to that
+ port is forwarded to a specified server port.
+
+ Args:
+ config: Constraint configuration dictionary, format:
+ port: Port to constrain (integer 1-65535).
+ server_port: Port to redirect traffic on [port] to (integer 1-65535).
+ interface: Network interface name (string).
+ latency: Delay added on each packet sent (integer in ms).
+ bandwidth: Maximum allowed upload bandwidth (integer in kbps).
+ loss: Percentage of packets to drop (integer 0-100).
+
+ Raises:
+ TrafficControlError: If any operation fails. The message in the exception
+ describes what failed.
+ """
+ _CheckArgsExist(config, 'interface', 'port', 'server_port')
+ _AddRootQdisc(config['interface'])
+
+ try:
+ _ConfigureClass('add', config)
+ _AddSubQdisc(config)
+ _AddFilter(config['interface'], config['port'])
+ _AddIptableRule(config['interface'], config['port'], config['server_port'])
+ except TrafficControlError as e:
+ logging.debug('Error creating constrained port %d.\nError: %s\n'
+ 'Deleting constrained port.', config['port'], e.error)
+ DeleteConstrainedPort(config)
+ raise e
+
+
+def DeleteConstrainedPort(config):
+ """Deletes an existing constrained port.
+
+ Deletes constraints set on a given port and the traffic forwarding rule from
+ the constrained port to a specified server port.
+
+ The original constrained network configuration used to create the constrained
+ port must be passed in.
+
+ Args:
+ config: Constraint configuration dictionary, format:
+ port: Port to constrain (integer 1-65535).
+ server_port: Port to redirect traffic on [port] to (integer 1-65535).
+ interface: Network interface name (string).
+ bandwidth: Maximum allowed upload bandwidth (integer in kbps).
+
+ Raises:
+ TrafficControlError: If any operation fails. The message in the exception
+ describes what failed.
+ """
+ _CheckArgsExist(config, 'interface', 'port', 'server_port')
+ try:
+ # Delete filters first so it frees the class.
+ _DeleteFilter(config['interface'], config['port'])
+ finally:
+ try:
+ # Deleting the class deletes attached qdisc as well.
+ _ConfigureClass('del', config)
+ finally:
+ _DeleteIptableRule(config['interface'], config['port'],
+ config['server_port'])
+
+
+def TearDown(config):
+ """Deletes the root qdisc and all iptables rules.
+
+ Args:
+ config: Constraint configuration dictionary, format:
+ interface: Network interface name (string).
+
+ Raises:
+ TrafficControlError: If any operation fails. The message in the exception
+ describes what failed.
+ """
+ _CheckArgsExist(config, 'interface')
+
+ command = ['tc', 'qdisc', 'del', 'dev', config['interface'], 'root']
+ try:
+ _Exec(command, msg='Could not delete root qdisc.')
+ finally:
+ _DeleteAllIpTableRules()
+
+
+def _CheckArgsExist(config, *args):
+ """Check that the args exist in config dictionary and are not None.
+
+ Args:
+ config: Any dictionary.
+ *args: The list of key names to check.
+
+ Raises:
+ TrafficControlError: If any key name does not exist in config or is None.
+ """
+ for key in args:
+ if key not in config.keys() or config[key] is None:
+ raise TrafficControlError('Missing "%s" parameter.' % key)
+
+
+def _AddRootQdisc(interface):
+ """Sets up the default root qdisc.
+
+ Args:
+ interface: Network interface name.
+
+ Raises:
+ TrafficControlError: If adding the root qdisc fails for a reason other than
+ it already exists.
+ """
+ command = ['tc', 'qdisc', 'add', 'dev', interface, 'root', 'handle', '1:',
+ 'htb']
+ try:
+ _Exec(command, msg=('Error creating root qdisc. '
+ 'Make sure you have root access'))
+ except TrafficControlError as e:
+ # Ignore the error if root already exists.
+ if not 'File exists' in e.error:
+ raise e
+
+
+def _ConfigureClass(option, config):
+ """Adds or deletes a class and qdisc attached to the root.
+
+ The class specifies bandwidth, and qdisc specifies delay and packet loss. The
+ class ID is based on the config port.
+
+ Args:
+ option: Adds or deletes a class option [add|del].
+ config: Constraint configuration dictionary, format:
+ port: Port to constrain (integer 1-65535).
+ interface: Network interface name (string).
+ bandwidth: Maximum allowed upload bandwidth (integer in kbps).
+ """
+ # Use constrained port as class ID so we can attach the qdisc and filter to
+ # it, as well as delete the class, using only the port number.
+ class_id = '1:%x' % config['port']
+ if 'bandwidth' not in config.keys() or not config['bandwidth']:
+ bandwidth = _DEFAULT_MAX_BANDWIDTH_KBPS
+ else:
+ bandwidth = config['bandwidth']
+
+ bandwidth = '%dkbps' % bandwidth
+ command = ['tc', 'class', option, 'dev', config['interface'], 'parent', '1:',
+ 'classid', class_id, 'htb', 'rate', bandwidth, 'ceil', bandwidth]
+ _Exec(command, msg=('Error configuring class ID %s using "%s" command.' %
+ (class_id, option)))
+
+
+def _AddSubQdisc(config):
+ """Adds a qdisc attached to the class identified by the config port.
+
+ Args:
+ config: Constraint configuration dictionary, format:
+ port: Port to constrain (integer 1-65535).
+ interface: Network interface name (string).
+ latency: Delay added on each packet sent (integer in ms).
+ loss: Percentage of packets to drop (integer 0-100).
+ """
+ port_hex = '%x' % config['port']
+ class_id = '1:%x' % config['port']
+ command = ['tc', 'qdisc', 'add', 'dev', config['interface'], 'parent',
+ class_id, 'handle', port_hex + ':0', 'netem']
+
+ # Check if packet-loss is set in the configuration.
+ if 'loss' in config.keys() and config['loss']:
+ loss = '%d%%' % config['loss']
+ command.extend(['loss', loss])
+ # Check if latency is set in the configuration.
+ if 'latency' in config.keys() and config['latency']:
+ latency = '%dms' % config['latency']
+ command.extend(['delay', latency])
+
+ _Exec(command, msg='Could not attach qdisc to class ID %s.' % class_id)
+
+
+def _AddFilter(interface, port):
+ """Redirects packets coming to a specified port into the constrained class.
+
+ Args:
+ interface: Interface name to attach the filter to (string).
+ port: Port number to filter packets with (integer 1-65535).
+ """
+ class_id = '1:%x' % port
+
+ command = ['tc', 'filter', 'add', 'dev', interface, 'protocol', 'ip',
+ 'parent', '1:', 'prio', '1', 'u32', 'match', 'ip', 'sport', port,
+ '0xffff', 'flowid', class_id]
+ _Exec(command, msg='Error adding filter on port %d.' % port)
+
+
+def _DeleteFilter(interface, port):
+ """Deletes the filter attached to the configured port.
+
+ Args:
+ interface: Interface name the filter is attached to (string).
+ port: Port number being filtered (integer 1-65535).
+ """
+ handle_id = _GetFilterHandleId(interface, port)
+ command = ['tc', 'filter', 'del', 'dev', interface, 'protocol', 'ip',
+ 'parent', '1:0', 'handle', handle_id, 'prio', '1', 'u32']
+ _Exec(command, msg='Error deleting filter on port %d.' % port)
+
+
+def _GetFilterHandleId(interface, port):
+ """Searches for the handle ID of the filter identified by the config port.
+
+ Args:
+ interface: Interface name the filter is attached to (string).
+ port: Port number being filtered (integer 1-65535).
+
+ Returns:
+ The handle ID.
+
+ Raises:
+ TrafficControlError: If handle ID was not found.
+ """
+ command = ['tc', 'filter', 'list', 'dev', interface, 'parent', '1:']
+ output = _Exec(command, msg='Error listing filters.')
+
+ # Search for the filter handle ID associated with class ID '1:port'.
+ handle_id_re = re.search(
+ '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x$)' % port, output)
+ if handle_id_re:
+ return handle_id_re.group(1)
+
+ raise TrafficControlError(('Could not find filter handle ID for class ID '
+ '1:%x.') % port)
+
+
+def _AddIptableRule(interface, port, server_port):
+ """Forwards traffic from constrained port to a specified server port.
+
+ Args:
+ interface: Interface name to attach the filter to (string).
+ port: Port of incoming packets (integer 1-65535).
+ server_port: Server port to forward the packets to (integer 1-65535).
+ """
+ # Preroute rules for accessing the port through external connections.
+ command = ['iptables', '-t', 'nat', '-A', 'PREROUTING', '-i', interface, '-p',
+ 'tcp', '--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
+ _Exec(command, msg='Error adding iptables rule for port %d.' % port)
+
+ # Output rules for accessing the rule through localhost or 127.0.0.1
+ command = ['iptables', '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp', '--dport',
+ port, '-j', 'REDIRECT', '--to-port', server_port]
+ _Exec(command, msg='Error adding iptables rule for port %d.' % port)
+
+
+def _DeleteIptableRule(interface, port, server_port):
+ """Deletes the iptable rule associated with specified port number.
+
+ Args:
+ interface: Interface name to attach the filter to (string).
+ port: Port of incoming packets (integer 1-65535).
+ server_port: Server port packets are forwarded to (integer 1-65535).
+ """
+ command = ['iptables', '-t', 'nat', '-D', 'PREROUTING', '-i', interface, '-p',
+ 'tcp', '--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
+ _Exec(command, msg='Error deleting iptables rule for port %d.' % port)
+
+ command = ['iptables', '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp', '--dport',
+ port, '-j', 'REDIRECT', '--to-port', server_port]
+ _Exec(command, msg='Error adding iptables rule for port %d.' % port)
+
+
+def _DeleteAllIpTableRules():
+ """Deletes all iptables rules."""
+ command = ['iptables', '-t', 'nat', '-F']
+ _Exec(command, msg='Error deleting all iptables rules.')
+
+
+def _Exec(command, msg=None):
+ """Executes a command.
+
+ Args:
+ command: Command list to execute.
+ msg: Message describing the error in case the command fails.
+
+ Returns:
+ The standard output from running the command.
+
+ Raises:
+ TrafficControlError: If command fails. Message is set by the msg parameter.
+ """
+ cmd_list = [str(x) for x in command]
+ cmd = ' '.join(cmd_list)
+ logging.debug('Running command: %s', cmd)
+
+ p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ output, error = p.communicate()
+ if p.returncode != 0:
+ raise TrafficControlError(msg, cmd, p.returncode, output, error)
+ return output.strip()
diff --git a/media/tools/constrained_network_server/traffic_control_test.py b/media/tools/constrained_network_server/traffic_control_test.py
new file mode 100644
index 0000000..583becb
--- /dev/null
+++ b/media/tools/constrained_network_server/traffic_control_test.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""End-to-end tests for traffic control library."""
+import os
+import re
+import sys
+import unittest
+
+import traffic_control
+
+
+class TrafficControlTests(unittest.TestCase):
+ """System tests for traffic_control functions.
+
+ These tests require root access.
+ """
+ # A dummy interface name to use instead of real interface.
+ _INTERFACE = 'myeth'
+
+ def setUp(self):
+ """Setup a dummy interface."""
+ # If we update to python version 2.7 or newer we can use setUpClass() or
+ # unittest.skipIf()
+ if os.getuid() != 0:
+ sys.exit('You need root access to run these tests.')
+
+ command = ['ip', 'link', 'add', 'name', self._INTERFACE, 'type', 'dummy']
+ traffic_control._Exec(command, 'Error creating dummy interface %s.' %
+ self._INTERFACE)
+
+ def tearDown(self):
+ """Teardown the dummy interface and any network constraints on it."""
+ # Deleting the dummy interface deletes all associated constraints.
+ command = ['ip', 'link', 'del', self._INTERFACE]
+ traffic_control._Exec(command)
+
+ def testExecOutput(self):
+ output = traffic_control._Exec(['echo', ' Test '])
+ self.assertEqual(output, 'Test')
+
+ def testExecException(self):
+ self.assertRaises(traffic_control.TrafficControlError,
+ traffic_control._Exec, command=['ls', '!doesntExist!'])
+
+ def testExecErrorCustomMsg(self):
+ try:
+ traffic_control._Exec(['ls', '!doesntExist!'], msg='test_msg')
+ self.fail('No exception raised for invalid command.')
+ except traffic_control.TrafficControlError as e:
+ self.assertEqual(e.msg, 'test_msg')
+
+ def testAddRootQdisc(self):
+ """Checks adding a root qdisc is successful."""
+ config = {'interface': self._INTERFACE}
+ root_detail = 'qdisc htb 1: root'
+ # Assert no htb root at startup.
+ command = ['tc', 'qdisc', 'ls', 'dev', config['interface']]
+ output = traffic_control._Exec(command)
+ self.assertFalse(root_detail in output)
+
+ traffic_control._AddRootQdisc(config['interface'])
+ output = traffic_control._Exec(command)
+ # Assert htb root is added
+ self.assertTrue(root_detail in output)
+
+ def testConfigureClassAdd(self):
+ """Checks adding and deleting a class to the root qdisc."""
+ config = {
+ 'interface': self._INTERFACE,
+ 'port': 12345,
+ 'server_port': 33333,
+ 'bandwidth': 2000
+ }
+ # Convert Kbps to Kbit
+ rate = config['bandwidth'] * 8
+ class_detail = ('class htb 1:%x root prio 0 rate %dKbit ceil %dKbit' %
+ (config['port'], rate, rate))
+
+ # Add root qdisc.
+ traffic_control._AddRootQdisc(config['interface'])
+
+ # Assert class does not exist prior to adding it.
+ command = ['tc', 'class', 'ls', 'dev', config['interface']]
+ output = traffic_control._Exec(command)
+ self.assertFalse(class_detail in output)
+
+ # Add class to root.
+ traffic_control._ConfigureClass('add', config)
+
+ # Assert class is added
+ command = ['tc', 'class', 'ls', 'dev', config['interface']]
+ output = traffic_control._Exec(command)
+ self.assertTrue(class_detail in output)
+
+ # Delete class
+ traffic_control._ConfigureClass('del', config)
+
+ # Assert class is deleted
+ command = ['tc', 'class', 'ls', 'dev', config['interface']]
+ output = traffic_control._Exec(command)
+ self.assertFalse(class_detail in output)
+
+ def testAddSubQdisc(self):
+ """Checks adding a sub qdisc to existing class."""
+ config = {
+ 'interface': self._INTERFACE,
+ 'port': 12345,
+ 'server_port': 33333,
+ 'bandwidth': 2000,
+ 'latency': 250,
+ 'loss': 5
+ }
+ qdisc_re_detail = ('qdisc netem %x: parent 1:%x .* delay %d.0ms loss %d%%' %
+ (config['port'], config['port'], config['latency'],
+ config['loss']))
+ # Add root qdisc.
+ traffic_control._AddRootQdisc(config['interface'])
+
+ # Add class to root.
+ traffic_control._ConfigureClass('add', config)
+
+ # Assert qdisc does not exist prior to adding it.
+ command = ['tc', 'qdisc', 'ls', 'dev', config['interface']]
+ output = traffic_control._Exec(command)
+ handle_id_re = re.search(qdisc_re_detail, output)
+ self.assertEqual(handle_id_re, None)
+
+ # Add qdisc to class
+ traffic_control._AddSubQdisc(config)
+
+ # Assert qdisc is added
+ command = ['tc', 'qdisc', 'ls', 'dev', config['interface']]
+ output = traffic_control._Exec(command)
+ handle_id_re = re.search(qdisc_re_detail, output)
+ self.assertNotEqual(handle_id_re, None)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/media/tools/constrained_network_server/traffic_control_unittest.py b/media/tools/constrained_network_server/traffic_control_unittest.py
new file mode 100644
index 0000000..29580b4
--- /dev/null
+++ b/media/tools/constrained_network_server/traffic_control_unittest.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Tests for traffic control library."""
+import unittest
+
+import traffic_control
+
+
+class TrafficControlUnitTests(unittest.TestCase):
+ """Unit tests for traffic control."""
+
+ # Stores commands called by the traffic control _Exec function.
+ commands = []
+
+ def _ExecMock(self, command, **kwargs):
+ """Mocks traffic_control._Exec and adds the command to commands list."""
+ cmd_list = [str(x) for x in command]
+ self.commands.append(' '.join(cmd_list))
+ return ''
+
+ def setUp(self):
+ """Resets the commands list and set the _Exec mock function."""
+ self.commands = []
+ self._old_Exec = traffic_control._Exec
+ traffic_control._Exec = self._ExecMock
+
+ def tearDown(self):
+ """Resets the _Exec mock function to the original."""
+ traffic_control._Exec = self._old_Exec
+
+ def testCreateConstrainedPort(self):
+ config = {
+ 'interface': 'fakeeth',
+ 'port': 12345,
+ 'server_port': 8888,
+ 'bandwidth': 256,
+ 'latency': 100,
+ 'loss': 2
+ }
+ traffic_control.CreateConstrainedPort(config)
+ expected = [
+ 'tc qdisc add dev fakeeth root handle 1: htb',
+ 'tc class add dev fakeeth parent 1: classid 1:3039 htb rate 256kbps '
+ 'ceil 256kbps',
+ 'tc qdisc add dev fakeeth parent 1:3039 handle 3039:0 netem loss 2% '
+ 'delay 100ms',
+ 'tc filter add dev fakeeth protocol ip parent 1: prio 1 u32 match ip '
+ 'sport 12345 0xffff flowid 1:3039',
+ 'iptables -t nat -A PREROUTING -i fakeeth -p tcp --dport 12345 -j '
+ 'REDIRECT --to-port 8888',
+ 'iptables -t nat -A OUTPUT -p tcp --dport 12345 -j REDIRECT --to-port '
+ '8888'
+ ]
+ self.assertEqual(expected, self.commands)
+
+ def testCreateConstrainedPortDefaults(self):
+ config = {
+ 'interface': 'fakeeth',
+ 'port': 12345,
+ 'server_port': 8888,
+ 'latency': None
+ }
+ traffic_control.CreateConstrainedPort(config)
+ expected = [
+ 'tc qdisc add dev fakeeth root handle 1: htb',
+ 'tc class add dev fakeeth parent 1: classid 1:3039 htb rate %dkbps '
+ 'ceil %dkbps' % (traffic_control._DEFAULT_MAX_BANDWIDTH_KBPS,
+ traffic_control._DEFAULT_MAX_BANDWIDTH_KBPS),
+ 'tc qdisc add dev fakeeth parent 1:3039 handle 3039:0 netem',
+ 'tc filter add dev fakeeth protocol ip parent 1: prio 1 u32 match ip '
+ 'sport 12345 0xffff flowid 1:3039',
+ 'iptables -t nat -A PREROUTING -i fakeeth -p tcp --dport 12345 -j '
+ 'REDIRECT --to-port 8888',
+ 'iptables -t nat -A OUTPUT -p tcp --dport 12345 -j REDIRECT --to-port '
+ '8888'
+ ]
+ self.assertEqual(expected, self.commands)
+
+ def testDeleteConstrainedPort(self):
+ config = {
+ 'interface': 'fakeeth',
+ 'port': 12345,
+ 'server_port': 8888,
+ 'bandwidth': 256,
+ }
+ _old_GetFilterHandleId = traffic_control._GetFilterHandleId
+ traffic_control._GetFilterHandleId = lambda interface, port: '800::800'
+
+ try:
+ traffic_control.DeleteConstrainedPort(config)
+ expected = [
+ 'tc filter del dev fakeeth protocol ip parent 1:0 handle 800::800 '
+ 'prio 1 u32',
+ 'tc class del dev fakeeth parent 1: classid 1:3039 htb rate 256kbps '
+ 'ceil 256kbps',
+ 'iptables -t nat -D PREROUTING -i fakeeth -p tcp --dport 12345 -j '
+ 'REDIRECT --to-port 8888',
+ 'iptables -t nat -D OUTPUT -p tcp --dport 12345 -j REDIRECT '
+ '--to-port 8888']
+ self.assertEqual(expected, self.commands)
+ finally:
+ traffic_control._GetFilterHandleId = _old_GetFilterHandleId
+
+ def testTearDown(self):
+ config = {'interface': 'fakeeth'}
+
+ traffic_control.TearDown(config)
+ expected = [
+ 'tc qdisc del dev fakeeth root',
+ 'iptables -t nat -F'
+ ]
+ self.assertEqual(expected, self.commands)
+
+ def testGetFilterHandleID(self):
+ # Check seach for handle ID command.
+ self.assertRaises(traffic_control.TrafficControlError,
+ traffic_control._GetFilterHandleId, 'fakeeth', 1)
+ self.assertEquals(self.commands, ['tc filter list dev fakeeth parent 1:'])
+
+ # Check with handle ID available.
+ traffic_control._Exec = (lambda command, msg:
+ 'filter parent 1: protocol ip'' pref 1 u32 fh 800::800 order 2048 key ht '
+ '800 bkt 0 flowid 1:1')
+ output = traffic_control._GetFilterHandleId('fakeeth', 1)
+ self.assertEqual(output, '800::800')
+
+ # Check with handle ID not available.
+ traffic_control._Exec = lambda command, msg: 'NO ID IN HERE'
+ self.assertRaises(traffic_control.TrafficControlError,
+ traffic_control._GetFilterHandleId, 'fakeeth', 1)
+
+
+if __name__ == '__main__':
+ unittest.main()