cc: Fix logic for detecting when raster tasks were throttled
[chromium-blink-merge.git] / media / tools / constrained_network_server / traffic_control.py
blobe94cc8dc9ba7be0b81a6981da2f6a531d26e7411
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Traffic control library for constraining the network configuration on a port.
7 The traffic controller sets up a constrained network configuration on a port.
8 Traffic to the constrained port is forwarded to a specified server port.
9 """
11 import logging
12 import os
13 import re
14 import subprocess
16 # The maximum bandwidth limit.
17 _DEFAULT_MAX_BANDWIDTH_KBIT = 1000000
20 class TrafficControlError(BaseException):
21 """Exception raised for errors in traffic control library.
23 Attributes:
24 msg: User defined error message.
25 cmd: Command for which the exception was raised.
26 returncode: Return code of running the command.
27 stdout: Output of running the command.
28 stderr: Error output of running the command.
29 """
31 def __init__(self, msg, cmd=None, returncode=None, output=None,
32 error=None):
33 BaseException.__init__(self, msg)
34 self.msg = msg
35 self.cmd = cmd
36 self.returncode = returncode
37 self.output = output
38 self.error = error
41 def CheckRequirements():
42 """Checks if permissions are available to run traffic control commands.
44 Raises:
45 TrafficControlError: If permissions to run traffic control commands are not
46 available.
47 """
48 if os.geteuid() != 0:
49 _Exec(['sudo', '-n', 'tc', '-help'],
50 msg=('Cannot run \'tc\' command. Traffic Control must be run as root '
51 'or have password-less sudo access to this command.'))
52 _Exec(['sudo', '-n', 'iptables', '-help'],
53 msg=('Cannot run \'iptables\' command. Traffic Control must be run '
54 'as root or have password-less sudo access to this command.'))
57 def CreateConstrainedPort(config):
58 """Creates a new constrained port.
60 Imposes packet level constraints such as bandwidth, latency, and packet loss
61 on a given port using the specified configuration dictionary. Traffic to that
62 port is forwarded to a specified server port.
64 Args:
65 config: Constraint configuration dictionary, format:
66 port: Port to constrain (integer 1-65535).
67 server_port: Port to redirect traffic on [port] to (integer 1-65535).
68 interface: Network interface name (string).
69 latency: Delay added on each packet sent (integer in ms).
70 bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
71 loss: Percentage of packets to drop (integer 0-100).
73 Raises:
74 TrafficControlError: If any operation fails. The message in the exception
75 describes what failed.
76 """
77 _CheckArgsExist(config, 'interface', 'port', 'server_port')
78 _AddRootQdisc(config['interface'])
80 try:
81 _ConfigureClass('add', config)
82 _AddSubQdisc(config)
83 _AddFilter(config['interface'], config['port'])
84 _AddIptableRule(config['interface'], config['port'], config['server_port'])
85 except TrafficControlError as e:
86 logging.debug('Error creating constrained port %d.\nError: %s\n'
87 'Deleting constrained port.', config['port'], e.error)
88 DeleteConstrainedPort(config)
89 raise e
92 def DeleteConstrainedPort(config):
93 """Deletes an existing constrained port.
95 Deletes constraints set on a given port and the traffic forwarding rule from
96 the constrained port to a specified server port.
98 The original constrained network configuration used to create the constrained
99 port must be passed in.
101 Args:
102 config: Constraint configuration dictionary, format:
103 port: Port to constrain (integer 1-65535).
104 server_port: Port to redirect traffic on [port] to (integer 1-65535).
105 interface: Network interface name (string).
106 bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
108 Raises:
109 TrafficControlError: If any operation fails. The message in the exception
110 describes what failed.
112 _CheckArgsExist(config, 'interface', 'port', 'server_port')
113 try:
114 # Delete filters first so it frees the class.
115 _DeleteFilter(config['interface'], config['port'])
116 finally:
117 try:
118 # Deleting the class deletes attached qdisc as well.
119 _ConfigureClass('del', config)
120 finally:
121 _DeleteIptableRule(config['interface'], config['port'],
122 config['server_port'])
125 def TearDown(config):
126 """Deletes the root qdisc and all iptables rules.
128 Args:
129 config: Constraint configuration dictionary, format:
130 interface: Network interface name (string).
132 Raises:
133 TrafficControlError: If any operation fails. The message in the exception
134 describes what failed.
136 _CheckArgsExist(config, 'interface')
138 command = ['sudo', 'tc', 'qdisc', 'del', 'dev', config['interface'], 'root']
139 try:
140 _Exec(command, msg='Could not delete root qdisc.')
141 finally:
142 _DeleteAllIpTableRules()
145 def _CheckArgsExist(config, *args):
146 """Check that the args exist in config dictionary and are not None.
148 Args:
149 config: Any dictionary.
150 *args: The list of key names to check.
152 Raises:
153 TrafficControlError: If any key name does not exist in config or is None.
155 for key in args:
156 if key not in config.keys() or config[key] is None:
157 raise TrafficControlError('Missing "%s" parameter.' % key)
160 def _AddRootQdisc(interface):
161 """Sets up the default root qdisc.
163 Args:
164 interface: Network interface name.
166 Raises:
167 TrafficControlError: If adding the root qdisc fails for a reason other than
168 it already exists.
170 command = ['sudo', 'tc', 'qdisc', 'add', 'dev', interface, 'root', 'handle',
171 '1:', 'htb']
172 try:
173 _Exec(command, msg=('Error creating root qdisc. '
174 'Make sure you have root access'))
175 except TrafficControlError as e:
176 # Ignore the error if root already exists.
177 if not 'File exists' in e.error:
178 raise e
181 def _ConfigureClass(option, config):
182 """Adds or deletes a class and qdisc attached to the root.
184 The class specifies bandwidth, and qdisc specifies delay and packet loss. The
185 class ID is based on the config port.
187 Args:
188 option: Adds or deletes a class option [add|del].
189 config: Constraint configuration dictionary, format:
190 port: Port to constrain (integer 1-65535).
191 interface: Network interface name (string).
192 bandwidth: Maximum allowed upload bandwidth (integer in kbit/s).
194 # Use constrained port as class ID so we can attach the qdisc and filter to
195 # it, as well as delete the class, using only the port number.
196 class_id = '1:%x' % config['port']
197 if 'bandwidth' not in config.keys() or not config['bandwidth']:
198 bandwidth = _DEFAULT_MAX_BANDWIDTH_KBIT
199 else:
200 bandwidth = config['bandwidth']
202 bandwidth = '%dkbit' % bandwidth
203 command = ['sudo', 'tc', 'class', option, 'dev', config['interface'],
204 'parent', '1:', 'classid', class_id, 'htb', 'rate', bandwidth,
205 'ceil', bandwidth]
206 _Exec(command, msg=('Error configuring class ID %s using "%s" command.' %
207 (class_id, option)))
210 def _AddSubQdisc(config):
211 """Adds a qdisc attached to the class identified by the config port.
213 Args:
214 config: Constraint configuration dictionary, format:
215 port: Port to constrain (integer 1-65535).
216 interface: Network interface name (string).
217 latency: Delay added on each packet sent (integer in ms).
218 loss: Percentage of packets to drop (integer 0-100).
220 port_hex = '%x' % config['port']
221 class_id = '1:%x' % config['port']
222 command = ['sudo', 'tc', 'qdisc', 'add', 'dev', config['interface'], 'parent',
223 class_id, 'handle', port_hex + ':0', 'netem']
225 # Check if packet-loss is set in the configuration.
226 if 'loss' in config.keys() and config['loss']:
227 loss = '%d%%' % config['loss']
228 command.extend(['loss', loss])
229 # Check if latency is set in the configuration.
230 if 'latency' in config.keys() and config['latency']:
231 latency = '%dms' % config['latency']
232 command.extend(['delay', latency])
234 _Exec(command, msg='Could not attach qdisc to class ID %s.' % class_id)
237 def _AddFilter(interface, port):
238 """Redirects packets coming to a specified port into the constrained class.
240 Args:
241 interface: Interface name to attach the filter to (string).
242 port: Port number to filter packets with (integer 1-65535).
244 class_id = '1:%x' % port
246 command = ['sudo', 'tc', 'filter', 'add', 'dev', interface, 'protocol', 'ip',
247 'parent', '1:', 'prio', '1', 'u32', 'match', 'ip', 'sport', port,
248 '0xffff', 'flowid', class_id]
249 _Exec(command, msg='Error adding filter on port %d.' % port)
252 def _DeleteFilter(interface, port):
253 """Deletes the filter attached to the configured port.
255 Args:
256 interface: Interface name the filter is attached to (string).
257 port: Port number being filtered (integer 1-65535).
259 handle_id = _GetFilterHandleId(interface, port)
260 command = ['sudo', 'tc', 'filter', 'del', 'dev', interface, 'protocol', 'ip',
261 'parent', '1:0', 'handle', handle_id, 'prio', '1', 'u32']
262 _Exec(command, msg='Error deleting filter on port %d.' % port)
265 def _GetFilterHandleId(interface, port):
266 """Searches for the handle ID of the filter identified by the config port.
268 Args:
269 interface: Interface name the filter is attached to (string).
270 port: Port number being filtered (integer 1-65535).
272 Returns:
273 The handle ID.
275 Raises:
276 TrafficControlError: If handle ID was not found.
278 command = ['sudo', 'tc', 'filter', 'list', 'dev', interface, 'parent', '1:']
279 output = _Exec(command, msg='Error listing filters.')
280 # Search for the filter handle ID associated with class ID '1:port'.
281 handle_id_re = re.search(
282 '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output)
283 if handle_id_re:
284 return handle_id_re.group(1)
285 raise TrafficControlError(('Could not find filter handle ID for class ID '
286 '1:%x.') % port)
289 def _AddIptableRule(interface, port, server_port):
290 """Forwards traffic from constrained port to a specified server port.
292 Args:
293 interface: Interface name to attach the filter to (string).
294 port: Port of incoming packets (integer 1-65535).
295 server_port: Server port to forward the packets to (integer 1-65535).
297 # Preroute rules for accessing the port through external connections.
298 command = ['sudo', 'iptables', '-t', 'nat', '-A', 'PREROUTING', '-i',
299 interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT',
300 '--to-port', server_port]
301 _Exec(command, msg='Error adding iptables rule for port %d.' % port)
303 # Output rules for accessing the rule through localhost or 127.0.0.1
304 command = ['sudo', 'iptables', '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp',
305 '--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
306 _Exec(command, msg='Error adding iptables rule for port %d.' % port)
309 def _DeleteIptableRule(interface, port, server_port):
310 """Deletes the iptable rule associated with specified port number.
312 Args:
313 interface: Interface name to attach the filter to (string).
314 port: Port of incoming packets (integer 1-65535).
315 server_port: Server port packets are forwarded to (integer 1-65535).
317 command = ['sudo', 'iptables', '-t', 'nat', '-D', 'PREROUTING', '-i',
318 interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT',
319 '--to-port', server_port]
320 _Exec(command, msg='Error deleting iptables rule for port %d.' % port)
322 command = ['sudo', 'iptables', '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp',
323 '--dport', port, '-j', 'REDIRECT', '--to-port', server_port]
324 _Exec(command, msg='Error adding iptables rule for port %d.' % port)
327 def _DeleteAllIpTableRules():
328 """Deletes all iptables rules."""
329 command = ['sudo', 'iptables', '-t', 'nat', '-F']
330 _Exec(command, msg='Error deleting all iptables rules.')
333 def _Exec(command, msg=None):
334 """Executes a command.
336 Args:
337 command: Command list to execute.
338 msg: Message describing the error in case the command fails.
340 Returns:
341 The standard output from running the command.
343 Raises:
344 TrafficControlError: If command fails. Message is set by the msg parameter.
346 cmd_list = [str(x) for x in command]
347 cmd = ' '.join(cmd_list)
348 logging.debug('Running command: %s', cmd)
350 p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
351 output, error = p.communicate()
352 if p.returncode != 0:
353 raise TrafficControlError(msg, cmd, p.returncode, output, error)
354 return output.strip()