| 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 |