| OLD | NEW | 
|---|
| 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be | 
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. | 
| 4 | 4 | 
| 5 """Traffic control library for constraining the network configuration on a port. | 5 """Traffic control library for constraining the network configuration on a port. | 
| 6 | 6 | 
| 7 The traffic controller sets up a constrained 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. | 8 Traffic to the constrained port is forwarded to a specified server port. | 
| 9 """ | 9 """ | 
| 10 | 10 | 
| 11 import logging | 11 import logging | 
| 12 import re | 12 import re | 
| 13 import subprocess | 13 import subprocess | 
| 14 | 14 | 
| 15 # The maximum bandwidth limit. | 15 # The maximum bandwidth limit. | 
| 16 _DEFAULT_MAX_BANDWIDTH_KBPS = 1000000 | 16 _DEFAULT_MAX_BANDWIDTH_KBIT = 1000000 | 
| 17 | 17 | 
| 18 | 18 | 
| 19 class TrafficControlError(BaseException): | 19 class TrafficControlError(BaseException): | 
| 20   """Exception raised for errors in traffic control library. | 20   """Exception raised for errors in traffic control library. | 
| 21 | 21 | 
| 22   Attributes: | 22   Attributes: | 
| 23     msg: User defined error message. | 23     msg: User defined error message. | 
| 24     cmd: Command for which the exception was raised. | 24     cmd: Command for which the exception was raised. | 
| 25     returncode: Return code of running the command. | 25     returncode: Return code of running the command. | 
| 26     stdout: Output of running the command. | 26     stdout: Output of running the command. | 
| (...skipping 16 matching lines...) Expand all  Loading... | 
| 43   Imposes packet level constraints such as bandwidth, latency, and packet loss | 43   Imposes packet level constraints such as bandwidth, latency, and packet loss | 
| 44   on a given port using the specified configuration dictionary. Traffic to that | 44   on a given port using the specified configuration dictionary. Traffic to that | 
| 45   port is forwarded to a specified server port. | 45   port is forwarded to a specified server port. | 
| 46 | 46 | 
| 47   Args: | 47   Args: | 
| 48     config: Constraint configuration dictionary, format: | 48     config: Constraint configuration dictionary, format: | 
| 49       port: Port to constrain (integer 1-65535). | 49       port: Port to constrain (integer 1-65535). | 
| 50       server_port: Port to redirect traffic on [port] to (integer 1-65535). | 50       server_port: Port to redirect traffic on [port] to (integer 1-65535). | 
| 51       interface: Network interface name (string). | 51       interface: Network interface name (string). | 
| 52       latency: Delay added on each packet sent (integer in ms). | 52       latency: Delay added on each packet sent (integer in ms). | 
| 53       bandwidth: Maximum allowed upload bandwidth (integer in kbps). | 53       bandwidth: Maximum allowed upload bandwidth (integer in kbit/s). | 
| 54       loss: Percentage of packets to drop (integer 0-100). | 54       loss: Percentage of packets to drop (integer 0-100). | 
| 55 | 55 | 
| 56   Raises: | 56   Raises: | 
| 57     TrafficControlError: If any operation fails. The message in the exception | 57     TrafficControlError: If any operation fails. The message in the exception | 
| 58     describes what failed. | 58     describes what failed. | 
| 59   """ | 59   """ | 
| 60   _CheckArgsExist(config, 'interface', 'port', 'server_port') | 60   _CheckArgsExist(config, 'interface', 'port', 'server_port') | 
| 61   _AddRootQdisc(config['interface']) | 61   _AddRootQdisc(config['interface']) | 
| 62 | 62 | 
| 63   try: | 63   try: | 
| (...skipping 15 matching lines...) Expand all  Loading... | 
| 79   the constrained port to a specified server port. | 79   the constrained port to a specified server port. | 
| 80 | 80 | 
| 81   The original constrained network configuration used to create the constrained | 81   The original constrained network configuration used to create the constrained | 
| 82   port must be passed in. | 82   port must be passed in. | 
| 83 | 83 | 
| 84   Args: | 84   Args: | 
| 85     config: Constraint configuration dictionary, format: | 85     config: Constraint configuration dictionary, format: | 
| 86       port: Port to constrain (integer 1-65535). | 86       port: Port to constrain (integer 1-65535). | 
| 87       server_port: Port to redirect traffic on [port] to (integer 1-65535). | 87       server_port: Port to redirect traffic on [port] to (integer 1-65535). | 
| 88       interface: Network interface name (string). | 88       interface: Network interface name (string). | 
| 89       bandwidth: Maximum allowed upload bandwidth (integer in kbps). | 89       bandwidth: Maximum allowed upload bandwidth (integer in kbit/s). | 
| 90 | 90 | 
| 91   Raises: | 91   Raises: | 
| 92     TrafficControlError: If any operation fails. The message in the exception | 92     TrafficControlError: If any operation fails. The message in the exception | 
| 93     describes what failed. | 93     describes what failed. | 
| 94   """ | 94   """ | 
| 95   _CheckArgsExist(config, 'interface', 'port', 'server_port') | 95   _CheckArgsExist(config, 'interface', 'port', 'server_port') | 
| 96   try: | 96   try: | 
| 97     # Delete filters first so it frees the class. | 97     # Delete filters first so it frees the class. | 
| 98     _DeleteFilter(config['interface'], config['port']) | 98     _DeleteFilter(config['interface'], config['port']) | 
| 99   finally: | 99   finally: | 
| (...skipping 11 matching lines...) Expand all  Loading... | 
| 111   Args: | 111   Args: | 
| 112     config: Constraint configuration dictionary, format: | 112     config: Constraint configuration dictionary, format: | 
| 113       interface: Network interface name (string). | 113       interface: Network interface name (string). | 
| 114 | 114 | 
| 115   Raises: | 115   Raises: | 
| 116     TrafficControlError: If any operation fails. The message in the exception | 116     TrafficControlError: If any operation fails. The message in the exception | 
| 117     describes what failed. | 117     describes what failed. | 
| 118   """ | 118   """ | 
| 119   _CheckArgsExist(config, 'interface') | 119   _CheckArgsExist(config, 'interface') | 
| 120 | 120 | 
| 121   command = ['tc', 'qdisc', 'del', 'dev', config['interface'], 'root'] | 121   command = ['sudo', 'tc', 'qdisc', 'del', 'dev', config['interface'], 'root'] | 
| 122   try: | 122   try: | 
| 123     _Exec(command, msg='Could not delete root qdisc.') | 123     _Exec(command, msg='Could not delete root qdisc.') | 
| 124   finally: | 124   finally: | 
| 125     _DeleteAllIpTableRules() | 125     _DeleteAllIpTableRules() | 
| 126 | 126 | 
| 127 | 127 | 
| 128 def _CheckArgsExist(config, *args): | 128 def _CheckArgsExist(config, *args): | 
| 129   """Check that the args exist in config dictionary and are not None. | 129   """Check that the args exist in config dictionary and are not None. | 
| 130 | 130 | 
| 131   Args: | 131   Args: | 
| (...skipping 11 matching lines...) Expand all  Loading... | 
| 143 def _AddRootQdisc(interface): | 143 def _AddRootQdisc(interface): | 
| 144   """Sets up the default root qdisc. | 144   """Sets up the default root qdisc. | 
| 145 | 145 | 
| 146   Args: | 146   Args: | 
| 147     interface: Network interface name. | 147     interface: Network interface name. | 
| 148 | 148 | 
| 149   Raises: | 149   Raises: | 
| 150     TrafficControlError: If adding the root qdisc fails for a reason other than | 150     TrafficControlError: If adding the root qdisc fails for a reason other than | 
| 151     it already exists. | 151     it already exists. | 
| 152   """ | 152   """ | 
| 153   command = ['tc', 'qdisc', 'add', 'dev', interface, 'root', 'handle', '1:', | 153   command = ['sudo', 'tc', 'qdisc', 'add', 'dev', interface, 'root', 'handle', | 
| 154              'htb'] | 154              '1:', 'htb'] | 
| 155   try: | 155   try: | 
| 156     _Exec(command, msg=('Error creating root qdisc. ' | 156     _Exec(command, msg=('Error creating root qdisc. ' | 
| 157                         'Make sure you have root access')) | 157                         'Make sure you have root access')) | 
| 158   except TrafficControlError as e: | 158   except TrafficControlError as e: | 
| 159     # Ignore the error if root already exists. | 159     # Ignore the error if root already exists. | 
| 160     if not 'File exists' in e.error: | 160     if not 'File exists' in e.error: | 
| 161       raise e | 161       raise e | 
| 162 | 162 | 
| 163 | 163 | 
| 164 def _ConfigureClass(option, config): | 164 def _ConfigureClass(option, config): | 
| 165   """Adds or deletes a class and qdisc attached to the root. | 165   """Adds or deletes a class and qdisc attached to the root. | 
| 166 | 166 | 
| 167   The class specifies bandwidth, and qdisc specifies delay and packet loss. The | 167   The class specifies bandwidth, and qdisc specifies delay and packet loss. The | 
| 168   class ID is based on the config port. | 168   class ID is based on the config port. | 
| 169 | 169 | 
| 170   Args: | 170   Args: | 
| 171     option: Adds or deletes a class option [add|del]. | 171     option: Adds or deletes a class option [add|del]. | 
| 172     config: Constraint configuration dictionary, format: | 172     config: Constraint configuration dictionary, format: | 
| 173       port: Port to constrain (integer 1-65535). | 173       port: Port to constrain (integer 1-65535). | 
| 174       interface: Network interface name (string). | 174       interface: Network interface name (string). | 
| 175       bandwidth: Maximum allowed upload bandwidth (integer in kbps). | 175       bandwidth: Maximum allowed upload bandwidth (integer in kbit/s). | 
| 176   """ | 176   """ | 
| 177   # Use constrained port as class ID so we can attach the qdisc and filter to | 177   # Use constrained port as class ID so we can attach the qdisc and filter to | 
| 178   # it, as well as delete the class, using only the port number. | 178   # it, as well as delete the class, using only the port number. | 
| 179   class_id = '1:%x' % config['port'] | 179   class_id = '1:%x' % config['port'] | 
| 180   if 'bandwidth' not in config.keys() or not config['bandwidth']: | 180   if 'bandwidth' not in config.keys() or not config['bandwidth']: | 
| 181     bandwidth = _DEFAULT_MAX_BANDWIDTH_KBPS | 181     bandwidth = _DEFAULT_MAX_BANDWIDTH_KBIT | 
| 182   else: | 182   else: | 
| 183     bandwidth = config['bandwidth'] | 183     bandwidth = config['bandwidth'] | 
| 184 | 184 | 
| 185   bandwidth = '%dkbps' % bandwidth | 185   bandwidth = '%dkbit' % bandwidth | 
| 186   command = ['tc', 'class', option, 'dev', config['interface'], 'parent', '1:', | 186   command = ['sudo', 'tc', 'class', option, 'dev', config['interface'], | 
| 187              'classid', class_id, 'htb', 'rate', bandwidth, 'ceil', bandwidth] | 187              'parent', '1:', 'classid', class_id, 'htb', 'rate', bandwidth, | 
|  | 188              'ceil', bandwidth] | 
| 188   _Exec(command, msg=('Error configuring class ID %s using "%s" command.' % | 189   _Exec(command, msg=('Error configuring class ID %s using "%s" command.' % | 
| 189                       (class_id, option))) | 190                       (class_id, option))) | 
| 190 | 191 | 
| 191 | 192 | 
| 192 def _AddSubQdisc(config): | 193 def _AddSubQdisc(config): | 
| 193   """Adds a qdisc attached to the class identified by the config port. | 194   """Adds a qdisc attached to the class identified by the config port. | 
| 194 | 195 | 
| 195   Args: | 196   Args: | 
| 196     config: Constraint configuration dictionary, format: | 197     config: Constraint configuration dictionary, format: | 
| 197       port: Port to constrain (integer 1-65535). | 198       port: Port to constrain (integer 1-65535). | 
| 198       interface: Network interface name (string). | 199       interface: Network interface name (string). | 
| 199       latency: Delay added on each packet sent (integer in ms). | 200       latency: Delay added on each packet sent (integer in ms). | 
| 200       loss: Percentage of packets to drop (integer 0-100). | 201       loss: Percentage of packets to drop (integer 0-100). | 
| 201   """ | 202   """ | 
| 202   port_hex = '%x' % config['port'] | 203   port_hex = '%x' % config['port'] | 
| 203   class_id = '1:%x' % config['port'] | 204   class_id = '1:%x' % config['port'] | 
| 204   command = ['tc', 'qdisc', 'add', 'dev', config['interface'], 'parent', | 205   command = ['sudo', 'tc', 'qdisc', 'add', 'dev', config['interface'], 'parent', | 
| 205              class_id, 'handle', port_hex + ':0', 'netem'] | 206              class_id, 'handle', port_hex + ':0', 'netem'] | 
| 206 | 207 | 
| 207   # Check if packet-loss is set in the configuration. | 208   # Check if packet-loss is set in the configuration. | 
| 208   if 'loss' in config.keys() and config['loss']: | 209   if 'loss' in config.keys() and config['loss']: | 
| 209     loss = '%d%%' % config['loss'] | 210     loss = '%d%%' % config['loss'] | 
| 210     command.extend(['loss', loss]) | 211     command.extend(['loss', loss]) | 
| 211   # Check if latency is set in the configuration. | 212   # Check if latency is set in the configuration. | 
| 212   if 'latency' in config.keys() and config['latency']: | 213   if 'latency' in config.keys() and config['latency']: | 
| 213     latency = '%dms' % config['latency'] | 214     latency = '%dms' % config['latency'] | 
| 214     command.extend(['delay', latency]) | 215     command.extend(['delay', latency]) | 
| 215 | 216 | 
| 216   _Exec(command, msg='Could not attach qdisc to class ID %s.' % class_id) | 217   _Exec(command, msg='Could not attach qdisc to class ID %s.' % class_id) | 
| 217 | 218 | 
| 218 | 219 | 
| 219 def _AddFilter(interface, port): | 220 def _AddFilter(interface, port): | 
| 220   """Redirects packets coming to a specified port into the constrained class. | 221   """Redirects packets coming to a specified port into the constrained class. | 
| 221 | 222 | 
| 222   Args: | 223   Args: | 
| 223     interface: Interface name to attach the filter to (string). | 224     interface: Interface name to attach the filter to (string). | 
| 224     port: Port number to filter packets with (integer 1-65535). | 225     port: Port number to filter packets with (integer 1-65535). | 
| 225   """ | 226   """ | 
| 226   class_id = '1:%x' % port | 227   class_id = '1:%x' % port | 
| 227 | 228 | 
| 228   command = ['tc', 'filter', 'add', 'dev', interface, 'protocol', 'ip', | 229   command = ['sudo', 'tc', 'filter', 'add', 'dev', interface, 'protocol', 'ip', | 
| 229              'parent', '1:', 'prio', '1', 'u32', 'match', 'ip', 'sport', port, | 230              'parent', '1:', 'prio', '1', 'u32', 'match', 'ip', 'sport', port, | 
| 230              '0xffff', 'flowid', class_id] | 231              '0xffff', 'flowid', class_id] | 
| 231   _Exec(command, msg='Error adding filter on port %d.' % port) | 232   _Exec(command, msg='Error adding filter on port %d.' % port) | 
| 232 | 233 | 
| 233 | 234 | 
| 234 def _DeleteFilter(interface, port): | 235 def _DeleteFilter(interface, port): | 
| 235   """Deletes the filter attached to the configured port. | 236   """Deletes the filter attached to the configured port. | 
| 236 | 237 | 
| 237   Args: | 238   Args: | 
| 238     interface: Interface name the filter is attached to (string). | 239     interface: Interface name the filter is attached to (string). | 
| 239     port: Port number being filtered (integer 1-65535). | 240     port: Port number being filtered (integer 1-65535). | 
| 240   """ | 241   """ | 
| 241   handle_id = _GetFilterHandleId(interface, port) | 242   handle_id = _GetFilterHandleId(interface, port) | 
| 242   command = ['tc', 'filter', 'del', 'dev', interface, 'protocol', 'ip', | 243   command = ['sudo', 'tc', 'filter', 'del', 'dev', interface, 'protocol', 'ip', | 
| 243              'parent', '1:0', 'handle', handle_id, 'prio', '1', 'u32'] | 244              'parent', '1:0', 'handle', handle_id, 'prio', '1', 'u32'] | 
| 244   _Exec(command, msg='Error deleting filter on port %d.' % port) | 245   _Exec(command, msg='Error deleting filter on port %d.' % port) | 
| 245 | 246 | 
| 246 | 247 | 
| 247 def _GetFilterHandleId(interface, port): | 248 def _GetFilterHandleId(interface, port): | 
| 248   """Searches for the handle ID of the filter identified by the config port. | 249   """Searches for the handle ID of the filter identified by the config port. | 
| 249 | 250 | 
| 250   Args: | 251   Args: | 
| 251     interface: Interface name the filter is attached to (string). | 252     interface: Interface name the filter is attached to (string). | 
| 252     port: Port number being filtered (integer 1-65535). | 253     port: Port number being filtered (integer 1-65535). | 
| 253 | 254 | 
| 254   Returns: | 255   Returns: | 
| 255     The handle ID. | 256     The handle ID. | 
| 256 | 257 | 
| 257   Raises: | 258   Raises: | 
| 258     TrafficControlError: If handle ID was not found. | 259     TrafficControlError: If handle ID was not found. | 
| 259   """ | 260   """ | 
| 260   command = ['tc', 'filter', 'list', 'dev', interface, 'parent', '1:'] | 261   command = ['sudo', 'tc', 'filter', 'list', 'dev', interface, 'parent', '1:'] | 
| 261   output = _Exec(command, msg='Error listing filters.') | 262   output = _Exec(command, msg='Error listing filters.') | 
| 262   # Search for the filter handle ID associated with class ID '1:port'. | 263   # Search for the filter handle ID associated with class ID '1:port'. | 
| 263   handle_id_re = re.search( | 264   handle_id_re = re.search( | 
| 264       '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output) | 265       '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output) | 
| 265   if handle_id_re: | 266   if handle_id_re: | 
| 266     return handle_id_re.group(1) | 267     return handle_id_re.group(1) | 
| 267   raise TrafficControlError(('Could not find filter handle ID for class ID ' | 268   raise TrafficControlError(('Could not find filter handle ID for class ID ' | 
| 268                              '1:%x.') % port) | 269                              '1:%x.') % port) | 
| 269 | 270 | 
| 270 | 271 | 
| 271 def _AddIptableRule(interface, port, server_port): | 272 def _AddIptableRule(interface, port, server_port): | 
| 272   """Forwards traffic from constrained port to a specified server port. | 273   """Forwards traffic from constrained port to a specified server port. | 
| 273 | 274 | 
| 274   Args: | 275   Args: | 
| 275     interface: Interface name to attach the filter to (string). | 276     interface: Interface name to attach the filter to (string). | 
| 276     port: Port of incoming packets (integer 1-65535). | 277     port: Port of incoming packets (integer 1-65535). | 
| 277     server_port: Server port to forward the packets to (integer 1-65535). | 278     server_port: Server port to forward the packets to (integer 1-65535). | 
| 278   """ | 279   """ | 
| 279   # Preroute rules for accessing the port through external connections. | 280   # Preroute rules for accessing the port through external connections. | 
| 280   command = ['iptables', '-t', 'nat', '-A', 'PREROUTING', '-i', interface, '-p', | 281   command = ['sudo', 'iptables', '-t', 'nat', '-A', 'PREROUTING', '-i', | 
| 281              'tcp', '--dport', port, '-j', 'REDIRECT', '--to-port', server_port] | 282              interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT', | 
|  | 283              '--to-port', server_port] | 
| 282   _Exec(command, msg='Error adding iptables rule for port %d.' % port) | 284   _Exec(command, msg='Error adding iptables rule for port %d.' % port) | 
| 283 | 285 | 
| 284   # Output rules for accessing the rule through localhost or 127.0.0.1 | 286   # Output rules for accessing the rule through localhost or 127.0.0.1 | 
| 285   command = ['iptables', '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp', '--dport', | 287   command = ['sudo', 'iptables', '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp', | 
| 286              port, '-j', 'REDIRECT', '--to-port', server_port] | 288              '--dport', port, '-j', 'REDIRECT', '--to-port', server_port] | 
| 287   _Exec(command, msg='Error adding iptables rule for port %d.' % port) | 289   _Exec(command, msg='Error adding iptables rule for port %d.' % port) | 
| 288 | 290 | 
| 289 | 291 | 
| 290 def _DeleteIptableRule(interface, port, server_port): | 292 def _DeleteIptableRule(interface, port, server_port): | 
| 291   """Deletes the iptable rule associated with specified port number. | 293   """Deletes the iptable rule associated with specified port number. | 
| 292 | 294 | 
| 293   Args: | 295   Args: | 
| 294     interface: Interface name to attach the filter to (string). | 296     interface: Interface name to attach the filter to (string). | 
| 295     port: Port of incoming packets (integer 1-65535). | 297     port: Port of incoming packets (integer 1-65535). | 
| 296     server_port: Server port packets are forwarded to (integer 1-65535). | 298     server_port: Server port packets are forwarded to (integer 1-65535). | 
| 297   """ | 299   """ | 
| 298   command = ['iptables', '-t', 'nat', '-D', 'PREROUTING', '-i', interface, '-p', | 300   command = ['sudo', 'iptables', '-t', 'nat', '-D', 'PREROUTING', '-i', | 
| 299              'tcp', '--dport', port, '-j', 'REDIRECT', '--to-port', server_port] | 301              interface, '-p', 'tcp', '--dport', port, '-j', 'REDIRECT', | 
|  | 302              '--to-port', server_port] | 
| 300   _Exec(command, msg='Error deleting iptables rule for port %d.' % port) | 303   _Exec(command, msg='Error deleting iptables rule for port %d.' % port) | 
| 301 | 304 | 
| 302   command = ['iptables', '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp', '--dport', | 305   command = ['sudo', 'iptables', '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp', | 
| 303              port, '-j', 'REDIRECT', '--to-port', server_port] | 306              '--dport', port, '-j', 'REDIRECT', '--to-port', server_port] | 
| 304   _Exec(command, msg='Error adding iptables rule for port %d.' % port) | 307   _Exec(command, msg='Error adding iptables rule for port %d.' % port) | 
| 305 | 308 | 
| 306 | 309 | 
| 307 def _DeleteAllIpTableRules(): | 310 def _DeleteAllIpTableRules(): | 
| 308   """Deletes all iptables rules.""" | 311   """Deletes all iptables rules.""" | 
| 309   command = ['iptables', '-t', 'nat', '-F'] | 312   command = ['sudo', 'iptables', '-t', 'nat', '-F'] | 
| 310   _Exec(command, msg='Error deleting all iptables rules.') | 313   _Exec(command, msg='Error deleting all iptables rules.') | 
| 311 | 314 | 
| 312 | 315 | 
| 313 def _Exec(command, msg=None): | 316 def _Exec(command, msg=None): | 
| 314   """Executes a command. | 317   """Executes a command. | 
| 315 | 318 | 
| 316   Args: | 319   Args: | 
| 317     command: Command list to execute. | 320     command: Command list to execute. | 
| 318     msg: Message describing the error in case the command fails. | 321     msg: Message describing the error in case the command fails. | 
| 319 | 322 | 
| 320   Returns: | 323   Returns: | 
| 321     The standard output from running the command. | 324     The standard output from running the command. | 
| 322 | 325 | 
| 323   Raises: | 326   Raises: | 
| 324     TrafficControlError: If command fails. Message is set by the msg parameter. | 327     TrafficControlError: If command fails. Message is set by the msg parameter. | 
| 325   """ | 328   """ | 
| 326   cmd_list = [str(x) for x in command] | 329   cmd_list = [str(x) for x in command] | 
| 327   cmd = ' '.join(cmd_list) | 330   cmd = ' '.join(cmd_list) | 
| 328   logging.debug('Running command: %s', cmd) | 331   logging.debug('Running command: %s', cmd) | 
| 329 | 332 | 
| 330   p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | 333   p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | 
| 331   output, error = p.communicate() | 334   output, error = p.communicate() | 
| 332   if p.returncode != 0: | 335   if p.returncode != 0: | 
| 333     raise TrafficControlError(msg, cmd, p.returncode, output, error) | 336     raise TrafficControlError(msg, cmd, p.returncode, output, error) | 
| 334   return output.strip() | 337   return output.strip() | 
| OLD | NEW | 
|---|