1#!/usr/bin/env python 2# Copyright 2010 Google Inc. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Provides cross-platform utility functions. 17 18Example: 19 import platformsettings 20 ip = platformsettings.get_server_ip_address() 21 22Functions with "_temporary_" in their name automatically clean-up upon 23termination (via the atexit module). 24 25For the full list of functions, see the bottom of the file. 26""" 27 28import atexit 29import distutils.spawn 30import distutils.version 31import fileinput 32import logging 33import os 34import platform 35import re 36import socket 37import stat 38import subprocess 39import sys 40import time 41import urlparse 42 43 44class PlatformSettingsError(Exception): 45 """Module catch-all error.""" 46 pass 47 48 49class DnsReadError(PlatformSettingsError): 50 """Raised when unable to read DNS settings.""" 51 pass 52 53 54class DnsUpdateError(PlatformSettingsError): 55 """Raised when unable to update DNS settings.""" 56 pass 57 58 59class NotAdministratorError(PlatformSettingsError): 60 """Raised when not running as administrator.""" 61 pass 62 63 64class CalledProcessError(PlatformSettingsError): 65 """Raised when a _check_output() process returns a non-zero exit status.""" 66 def __init__(self, returncode, cmd): 67 super(CalledProcessError, self).__init__() 68 self.returncode = returncode 69 self.cmd = cmd 70 71 def __str__(self): 72 return 'Command "%s" returned non-zero exit status %d' % ( 73 ' '.join(self.cmd), self.returncode) 74 75 76def FindExecutable(executable): 77 """Finds the given executable in PATH. 78 79 Since WPR may be invoked as sudo, meaning PATH is empty, we also hardcode a 80 few common paths. 81 82 Returns: 83 The fully qualified path with .exe appended if appropriate or None if it 84 doesn't exist. 85 """ 86 return distutils.spawn.find_executable(executable, 87 os.pathsep.join([os.environ['PATH'], 88 '/sbin', 89 '/usr/bin', 90 '/usr/sbin/', 91 '/usr/local/sbin', 92 ])) 93 94def HasSniSupport(): 95 try: 96 import OpenSSL 97 return (distutils.version.StrictVersion(OpenSSL.__version__) >= 98 distutils.version.StrictVersion('0.13')) 99 except ImportError: 100 return False 101 102 103def SupportsFdLimitControl(): 104 """Whether the platform supports changing the process fd limit.""" 105 return os.name is 'posix' 106 107 108def GetFdLimit(): 109 """Returns a tuple of (soft_limit, hard_limit).""" 110 import resource 111 return resource.getrlimit(resource.RLIMIT_NOFILE) 112 113 114def AdjustFdLimit(new_soft_limit, new_hard_limit): 115 """Sets a new soft and hard limit for max number of fds.""" 116 import resource 117 resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft_limit, new_hard_limit)) 118 119 120class SystemProxy(object): 121 """A host/port pair for a HTTP or HTTPS proxy configuration.""" 122 123 def __init__(self, host, port): 124 """Initialize a SystemProxy instance. 125 126 Args: 127 host: a host name or IP address string (e.g. "example.com" or "1.1.1.1"). 128 port: a port string or integer (e.g. "8888" or 8888). 129 """ 130 self.host = host 131 self.port = int(port) if port else None 132 133 def __nonzero__(self): 134 """True if the host is set.""" 135 return bool(self.host) 136 137 @classmethod 138 def from_url(cls, proxy_url): 139 """Create a SystemProxy instance. 140 141 If proxy_url is None, an empty string, or an invalid URL, the 142 SystemProxy instance with have None and None for the host and port 143 (no exception is raised). 144 145 Args: 146 proxy_url: a proxy url string such as "http://proxy.com:8888/". 147 Returns: 148 a System proxy instance. 149 """ 150 if proxy_url: 151 parse_result = urlparse.urlparse(proxy_url) 152 return cls(parse_result.hostname, parse_result.port) 153 return cls(None, None) 154 155 156class _BasePlatformSettings(object): 157 158 def get_system_logging_handler(self): 159 """Return a handler for the logging module (optional).""" 160 return None 161 162 def rerun_as_administrator(self): 163 """If needed, rerun the program with administrative privileges. 164 165 Raises NotAdministratorError if unable to rerun. 166 """ 167 pass 168 169 def timer(self): 170 """Return the current time in seconds as a floating point number.""" 171 return time.time() 172 173 def get_server_ip_address(self, is_server_mode=False): 174 """Returns the IP address to use for dnsproxy and ipfw.""" 175 if is_server_mode: 176 return socket.gethostbyname(socket.gethostname()) 177 return '127.0.0.1' 178 179 def get_httpproxy_ip_address(self, is_server_mode=False): 180 """Returns the IP address to use for httpproxy.""" 181 if is_server_mode: 182 return '0.0.0.0' 183 return '127.0.0.1' 184 185 def get_system_proxy(self, use_ssl): 186 """Returns the system HTTP(S) proxy host, port.""" 187 del use_ssl 188 return SystemProxy(None, None) 189 190 def _ipfw_cmd(self): 191 raise NotImplementedError 192 193 def ipfw(self, *args): 194 ipfw_cmd = (self._ipfw_cmd(), ) + args 195 return self._check_output(*ipfw_cmd, elevate_privilege=True) 196 197 def has_ipfw(self): 198 try: 199 self.ipfw('list') 200 return True 201 except AssertionError as e: 202 logging.warning('Failed to start ipfw command. ' 203 'Error: %s' % e.message) 204 return False 205 206 def _get_cwnd(self): 207 return None 208 209 def _set_cwnd(self, args): 210 pass 211 212 def _elevate_privilege_for_cmd(self, args): 213 return args 214 215 def _check_output(self, *args, **kwargs): 216 """Run Popen(*args) and return its output as a byte string. 217 218 Python 2.7 has subprocess.check_output. This is essentially the same 219 except that, as a convenience, all the positional args are used as 220 command arguments and the |elevate_privilege| kwarg is supported. 221 222 Args: 223 *args: sequence of program arguments 224 elevate_privilege: Run the command with elevated privileges. 225 Raises: 226 CalledProcessError if the program returns non-zero exit status. 227 Returns: 228 output as a byte string. 229 """ 230 command_args = [str(a) for a in args] 231 232 if os.path.sep not in command_args[0]: 233 qualified_command = FindExecutable(command_args[0]) 234 assert qualified_command, 'Failed to find %s in path' % command_args[0] 235 command_args[0] = qualified_command 236 237 if kwargs.get('elevate_privilege'): 238 command_args = self._elevate_privilege_for_cmd(command_args) 239 240 logging.debug(' '.join(command_args)) 241 process = subprocess.Popen( 242 command_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 243 output = process.communicate()[0] 244 retcode = process.poll() 245 if retcode: 246 raise CalledProcessError(retcode, command_args) 247 return output 248 249 def set_temporary_tcp_init_cwnd(self, cwnd): 250 cwnd = int(cwnd) 251 original_cwnd = self._get_cwnd() 252 if original_cwnd is None: 253 raise PlatformSettingsError('Unable to get current tcp init_cwnd.') 254 if cwnd == original_cwnd: 255 logging.info('TCP init_cwnd already set to target value: %s', cwnd) 256 else: 257 self._set_cwnd(cwnd) 258 if self._get_cwnd() == cwnd: 259 logging.info('Changed cwnd to %s', cwnd) 260 atexit.register(self._set_cwnd, original_cwnd) 261 else: 262 logging.error('Unable to update cwnd to %s', cwnd) 263 264 def setup_temporary_loopback_config(self): 265 """Setup the loopback interface similar to real interface. 266 267 We use loopback for much of our testing, and on some systems, loopback 268 behaves differently from real interfaces. 269 """ 270 logging.error('Platform does not support loopback configuration.') 271 272 def _save_primary_interface_properties(self): 273 self._orig_nameserver = self.get_original_primary_nameserver() 274 275 def _restore_primary_interface_properties(self): 276 self._set_primary_nameserver(self._orig_nameserver) 277 278 def _get_primary_nameserver(self): 279 raise NotImplementedError 280 281 def _set_primary_nameserver(self, _): 282 raise NotImplementedError 283 284 def get_original_primary_nameserver(self): 285 if not hasattr(self, '_original_nameserver'): 286 self._original_nameserver = self._get_primary_nameserver() 287 logging.info('Saved original primary DNS nameserver: %s', 288 self._original_nameserver) 289 return self._original_nameserver 290 291 def set_temporary_primary_nameserver(self, nameserver): 292 self._save_primary_interface_properties() 293 self._set_primary_nameserver(nameserver) 294 if self._get_primary_nameserver() == nameserver: 295 logging.info('Changed temporary primary nameserver to %s', nameserver) 296 atexit.register(self._restore_primary_interface_properties) 297 else: 298 raise self._get_dns_update_error() 299 300 301class _PosixPlatformSettings(_BasePlatformSettings): 302 303 # pylint: disable=abstract-method 304 # Suppress lint check for _get_primary_nameserver & _set_primary_nameserver 305 306 def rerun_as_administrator(self): 307 """If needed, rerun the program with administrative privileges. 308 309 Raises NotAdministratorError if unable to rerun. 310 """ 311 if os.geteuid() != 0: 312 logging.warn('Rerunning with sudo: %s', sys.argv) 313 os.execv('/usr/bin/sudo', ['--'] + sys.argv) 314 315 def _elevate_privilege_for_cmd(self, args): 316 def IsSetUID(path): 317 return (os.stat(path).st_mode & stat.S_ISUID) == stat.S_ISUID 318 319 def IsElevated(): 320 p = subprocess.Popen( 321 ['sudo', '-nv'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, 322 stderr=subprocess.STDOUT) 323 stdout = p.communicate()[0] 324 # Some versions of sudo set the returncode based on whether sudo requires 325 # a password currently. Other versions return output when password is 326 # required and no output when the user is already authenticated. 327 return not p.returncode and not stdout 328 329 if not IsSetUID(args[0]): 330 args = ['sudo'] + args 331 332 if not IsElevated(): 333 print 'WPR needs to run %s under sudo. Please authenticate.' % args[1] 334 subprocess.check_call(['sudo', '-v']) # Synchronously authenticate. 335 336 prompt = ('Would you like to always allow %s to run without sudo ' 337 '(via `sudo chmod +s %s`)? (y/N)' % (args[1], args[1])) 338 if raw_input(prompt).lower() == 'y': 339 subprocess.check_call(['sudo', 'chmod', '+s', args[1]]) 340 return args 341 342 def get_system_proxy(self, use_ssl): 343 """Returns the system HTTP(S) proxy host, port.""" 344 proxy_url = os.environ.get('https_proxy' if use_ssl else 'http_proxy') 345 return SystemProxy.from_url(proxy_url) 346 347 def _ipfw_cmd(self): 348 return 'ipfw' 349 350 def _get_dns_update_error(self): 351 return DnsUpdateError('Did you run under sudo?') 352 353 def _sysctl(self, *args, **kwargs): 354 sysctl_args = [FindExecutable('sysctl')] 355 if kwargs.get('use_sudo'): 356 sysctl_args = self._elevate_privilege_for_cmd(sysctl_args) 357 sysctl_args.extend(str(a) for a in args) 358 sysctl = subprocess.Popen( 359 sysctl_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 360 stdout = sysctl.communicate()[0] 361 return sysctl.returncode, stdout 362 363 def has_sysctl(self, name): 364 if not hasattr(self, 'has_sysctl_cache'): 365 self.has_sysctl_cache = {} 366 if name not in self.has_sysctl_cache: 367 self.has_sysctl_cache[name] = self._sysctl(name)[0] == 0 368 return self.has_sysctl_cache[name] 369 370 def set_sysctl(self, name, value): 371 rv = self._sysctl('%s=%s' % (name, value), use_sudo=True)[0] 372 if rv != 0: 373 logging.error('Unable to set sysctl %s: %s', name, rv) 374 375 def get_sysctl(self, name): 376 rv, value = self._sysctl('-n', name) 377 if rv == 0: 378 return value 379 else: 380 logging.error('Unable to get sysctl %s: %s', name, rv) 381 return None 382 383 384class _OsxPlatformSettings(_PosixPlatformSettings): 385 LOCAL_SLOWSTART_MIB_NAME = 'net.inet.tcp.local_slowstart_flightsize' 386 387 def _scutil(self, cmd): 388 scutil = subprocess.Popen([FindExecutable('scutil')], 389 stdin=subprocess.PIPE, stdout=subprocess.PIPE) 390 return scutil.communicate(cmd)[0] 391 392 def _ifconfig(self, *args): 393 return self._check_output('ifconfig', *args, elevate_privilege=True) 394 395 def set_sysctl(self, name, value): 396 rv = self._sysctl('-w', '%s=%s' % (name, value), use_sudo=True)[0] 397 if rv != 0: 398 logging.error('Unable to set sysctl %s: %s', name, rv) 399 400 def _get_cwnd(self): 401 return int(self.get_sysctl(self.LOCAL_SLOWSTART_MIB_NAME)) 402 403 def _set_cwnd(self, size): 404 self.set_sysctl(self.LOCAL_SLOWSTART_MIB_NAME, size) 405 406 def _get_loopback_mtu(self): 407 config = self._ifconfig('lo0') 408 match = re.search(r'\smtu\s+(\d+)', config) 409 return int(match.group(1)) if match else None 410 411 def setup_temporary_loopback_config(self): 412 """Configure loopback to temporarily use reasonably sized frames. 413 414 OS X uses jumbo frames by default (16KB). 415 """ 416 TARGET_LOOPBACK_MTU = 1500 417 original_mtu = self._get_loopback_mtu() 418 if original_mtu is None: 419 logging.error('Unable to read loopback mtu. Setting left unchanged.') 420 return 421 if original_mtu == TARGET_LOOPBACK_MTU: 422 logging.debug('Loopback MTU already has target value: %d', original_mtu) 423 else: 424 self._ifconfig('lo0', 'mtu', TARGET_LOOPBACK_MTU) 425 if self._get_loopback_mtu() == TARGET_LOOPBACK_MTU: 426 logging.debug('Set loopback MTU to %d (was %d)', 427 TARGET_LOOPBACK_MTU, original_mtu) 428 atexit.register(self._ifconfig, 'lo0', 'mtu', original_mtu) 429 else: 430 logging.error('Unable to change loopback MTU from %d to %d', 431 original_mtu, TARGET_LOOPBACK_MTU) 432 433 def _get_dns_service_key(self): 434 output = self._scutil('show State:/Network/Global/IPv4') 435 lines = output.split('\n') 436 for line in lines: 437 key_value = line.split(' : ') 438 if key_value[0] == ' PrimaryService': 439 return 'State:/Network/Service/%s/DNS' % key_value[1] 440 raise DnsReadError('Unable to find DNS service key: %s', output) 441 442 def _get_primary_nameserver(self): 443 output = self._scutil('show %s' % self._get_dns_service_key()) 444 match = re.search( 445 br'ServerAddresses\s+:\s+<array>\s+{\s+0\s+:\s+((\d{1,3}\.){3}\d{1,3})', 446 output) 447 if match: 448 return match.group(1) 449 else: 450 raise DnsReadError('Unable to find primary DNS server: %s', output) 451 452 def _set_primary_nameserver(self, dns): 453 command = '\n'.join([ 454 'd.init', 455 'd.add ServerAddresses * %s' % dns, 456 'set %s' % self._get_dns_service_key() 457 ]) 458 self._scutil(command) 459 460 461class _FreeBSDPlatformSettings(_PosixPlatformSettings): 462 """Partial implementation for FreeBSD. Does not allow a DNS server to be 463 launched nor ipfw to be used. 464 """ 465 RESOLV_CONF = '/etc/resolv.conf' 466 467 def _get_default_route_line(self): 468 raise NotImplementedError 469 470 def _set_cwnd(self, cwnd): 471 raise NotImplementedError 472 473 def _get_cwnd(self): 474 raise NotImplementedError 475 476 def setup_temporary_loopback_config(self): 477 raise NotImplementedError 478 479 def _write_resolve_conf(self, dns): 480 raise NotImplementedError 481 482 def _get_primary_nameserver(self): 483 try: 484 resolv_file = open(self.RESOLV_CONF) 485 except IOError: 486 raise DnsReadError() 487 for line in resolv_file: 488 if line.startswith('nameserver '): 489 return line.split()[1] 490 raise DnsReadError() 491 492 def _set_primary_nameserver(self, dns): 493 raise NotImplementedError 494 495 496class _LinuxPlatformSettings(_PosixPlatformSettings): 497 """The following thread recommends a way to update DNS on Linux: 498 499 http://ubuntuforums.org/showthread.php?t=337553 500 501 sudo cp /etc/dhcp3/dhclient.conf /etc/dhcp3/dhclient.conf.bak 502 sudo gedit /etc/dhcp3/dhclient.conf 503 #prepend domain-name-servers 127.0.0.1; 504 prepend domain-name-servers 208.67.222.222, 208.67.220.220; 505 506 prepend domain-name-servers 208.67.222.222, 208.67.220.220; 507 request subnet-mask, broadcast-address, time-offset, routers, 508 domain-name, domain-name-servers, host-name, 509 netbios-name-servers, netbios-scope; 510 #require subnet-mask, domain-name-servers; 511 512 sudo /etc/init.d/networking restart 513 514 The code below does not try to change dchp and does not restart networking. 515 Update this as needed to make it more robust on more systems. 516 """ 517 RESOLV_CONF = '/etc/resolv.conf' 518 ROUTE_RE = re.compile('initcwnd (\d+)') 519 TCP_BASE_MSS = 'net.ipv4.tcp_base_mss' 520 TCP_MTU_PROBING = 'net.ipv4.tcp_mtu_probing' 521 522 def _get_default_route_line(self): 523 stdout = self._check_output('ip', 'route') 524 for line in stdout.split('\n'): 525 if line.startswith('default'): 526 return line 527 return None 528 529 def _set_cwnd(self, cwnd): 530 default_line = self._get_default_route_line() 531 self._check_output( 532 'ip', 'route', 'change', default_line, 'initcwnd', str(cwnd)) 533 534 def _get_cwnd(self): 535 default_line = self._get_default_route_line() 536 m = self.ROUTE_RE.search(default_line) 537 if m: 538 return int(m.group(1)) 539 # If 'initcwnd' wasn't found, then 0 means it's the system default. 540 return 0 541 542 def setup_temporary_loopback_config(self): 543 """Setup Linux to temporarily use reasonably sized frames. 544 545 Linux uses jumbo frames by default (16KB), using the combination 546 of MTU probing and a base MSS makes it use normal sized packets. 547 548 The reason this works is because tcp_base_mss is only used when MTU 549 probing is enabled. And since we're using the max value, it will 550 always use the reasonable size. This is relevant for server-side realism. 551 The client-side will vary depending on the client TCP stack config. 552 """ 553 ENABLE_MTU_PROBING = 2 554 original_probing = self.get_sysctl(self.TCP_MTU_PROBING) 555 self.set_sysctl(self.TCP_MTU_PROBING, ENABLE_MTU_PROBING) 556 atexit.register(self.set_sysctl, self.TCP_MTU_PROBING, original_probing) 557 558 TCP_FULL_MSS = 1460 559 original_mss = self.get_sysctl(self.TCP_BASE_MSS) 560 self.set_sysctl(self.TCP_BASE_MSS, TCP_FULL_MSS) 561 atexit.register(self.set_sysctl, self.TCP_BASE_MSS, original_mss) 562 563 def _write_resolve_conf(self, dns): 564 is_first_nameserver_replaced = False 565 # The fileinput module uses sys.stdout as the edited file output. 566 for line in fileinput.input(self.RESOLV_CONF, inplace=1, backup='.bak'): 567 if line.startswith('nameserver ') and not is_first_nameserver_replaced: 568 print 'nameserver %s' % dns 569 is_first_nameserver_replaced = True 570 else: 571 print line, 572 if not is_first_nameserver_replaced: 573 raise DnsUpdateError('Could not find a suitable nameserver entry in %s' % 574 self.RESOLV_CONF) 575 576 def _get_primary_nameserver(self): 577 try: 578 resolv_file = open(self.RESOLV_CONF) 579 except IOError: 580 raise DnsReadError() 581 for line in resolv_file: 582 if line.startswith('nameserver '): 583 return line.split()[1] 584 raise DnsReadError() 585 586 def _set_primary_nameserver(self, dns): 587 """Replace the first nameserver entry with the one given.""" 588 try: 589 self._write_resolve_conf(dns) 590 except OSError, e: 591 if 'Permission denied' in e: 592 raise self._get_dns_update_error() 593 raise 594 595 596class _WindowsPlatformSettings(_BasePlatformSettings): 597 598 # pylint: disable=abstract-method 599 # Suppress lint check for _ipfw_cmd 600 601 def get_system_logging_handler(self): 602 """Return a handler for the logging module (optional). 603 604 For Windows, output can be viewed with DebugView. 605 http://technet.microsoft.com/en-us/sysinternals/bb896647.aspx 606 """ 607 import ctypes 608 output_debug_string = ctypes.windll.kernel32.OutputDebugStringA 609 output_debug_string.argtypes = [ctypes.c_char_p] 610 class DebugViewHandler(logging.Handler): 611 def emit(self, record): 612 output_debug_string('[wpr] ' + self.format(record)) 613 return DebugViewHandler() 614 615 def rerun_as_administrator(self): 616 """If needed, rerun the program with administrative privileges. 617 618 Raises NotAdministratorError if unable to rerun. 619 """ 620 import ctypes 621 if not ctypes.windll.shell32.IsUserAnAdmin(): 622 raise NotAdministratorError('Rerun with administrator privileges.') 623 #os.execv('runas', sys.argv) # TODO: replace needed Windows magic 624 625 def timer(self): 626 """Return the current time in seconds as a floating point number. 627 628 From time module documentation: 629 On Windows, this function [time.clock()] returns wall-clock 630 seconds elapsed since the first call to this function, as a 631 floating point number, based on the Win32 function 632 QueryPerformanceCounter(). The resolution is typically better 633 than one microsecond. 634 """ 635 return time.clock() 636 637 def _arp(self, *args): 638 return self._check_output('arp', *args) 639 640 def _route(self, *args): 641 return self._check_output('route', *args) 642 643 def _ipconfig(self, *args): 644 return self._check_output('ipconfig', *args) 645 646 def _get_mac_address(self, ip): 647 """Return the MAC address for the given ip.""" 648 ip_re = re.compile(r'^\s*IP(?:v4)? Address[ .]+:\s+([0-9.]+)') 649 for line in self._ipconfig('/all').splitlines(): 650 if line[:1].isalnum(): 651 current_ip = None 652 current_mac = None 653 elif ':' in line: 654 line = line.strip() 655 ip_match = ip_re.match(line) 656 if ip_match: 657 current_ip = ip_match.group(1) 658 elif line.startswith('Physical Address'): 659 current_mac = line.split(':', 1)[1].lstrip() 660 if current_ip == ip and current_mac: 661 return current_mac 662 return None 663 664 def setup_temporary_loopback_config(self): 665 """On Windows, temporarily route the server ip to itself.""" 666 ip = self.get_server_ip_address() 667 mac_address = self._get_mac_address(ip) 668 if self.mac_address: 669 self._arp('-s', ip, self.mac_address) 670 self._route('add', ip, ip, 'mask', '255.255.255.255') 671 atexit.register(self._arp, '-d', ip) 672 atexit.register(self._route, 'delete', ip, ip, 'mask', '255.255.255.255') 673 else: 674 logging.warn('Unable to configure loopback: MAC address not found.') 675 # TODO(slamm): Configure cwnd, MTU size 676 677 def _get_dns_update_error(self): 678 return DnsUpdateError('Did you run as administrator?') 679 680 def _netsh_show_dns(self): 681 """Return DNS information: 682 683 Example output: 684 Configuration for interface "Local Area Connection 3" 685 DNS servers configured through DHCP: None 686 Register with which suffix: Primary only 687 688 Configuration for interface "Wireless Network Connection 2" 689 DNS servers configured through DHCP: 192.168.1.1 690 Register with which suffix: Primary only 691 """ 692 return self._check_output('netsh', 'interface', 'ip', 'show', 'dns') 693 694 def _netsh_set_dns(self, iface_name, addr): 695 """Modify DNS information on the primary interface.""" 696 output = self._check_output('netsh', 'interface', 'ip', 'set', 'dns', 697 iface_name, 'static', addr) 698 699 def _netsh_set_dns_dhcp(self, iface_name): 700 """Modify DNS information on the primary interface.""" 701 output = self._check_output('netsh', 'interface', 'ip', 'set', 'dns', 702 iface_name, 'dhcp') 703 704 def _get_interfaces_with_dns(self): 705 output = self._netsh_show_dns() 706 lines = output.split('\n') 707 iface_re = re.compile(r'^Configuration for interface \"(?P<name>.*)\"') 708 dns_re = re.compile(r'(?P<kind>.*):\s+(?P<dns>\d+\.\d+\.\d+\.\d+)') 709 iface_name = None 710 iface_dns = None 711 iface_kind = None 712 ifaces = [] 713 for line in lines: 714 iface_match = iface_re.match(line) 715 if iface_match: 716 iface_name = iface_match.group('name') 717 dns_match = dns_re.match(line) 718 if dns_match: 719 iface_dns = dns_match.group('dns') 720 iface_dns_config = dns_match.group('kind').strip() 721 if iface_dns_config == "Statically Configured DNS Servers": 722 iface_kind = "static" 723 elif iface_dns_config == "DNS servers configured through DHCP": 724 iface_kind = "dhcp" 725 if iface_name and iface_dns and iface_kind: 726 ifaces.append((iface_dns, iface_name, iface_kind)) 727 iface_name = None 728 iface_dns = None 729 return ifaces 730 731 def _save_primary_interface_properties(self): 732 # TODO(etienneb): On windows, an interface can have multiple DNS server 733 # configured. We should save/restore all of them. 734 ifaces = self._get_interfaces_with_dns() 735 self._primary_interfaces = ifaces 736 737 def _restore_primary_interface_properties(self): 738 for iface in self._primary_interfaces: 739 (iface_dns, iface_name, iface_kind) = iface 740 self._netsh_set_dns(iface_name, iface_dns) 741 if iface_kind == "dhcp": 742 self._netsh_set_dns_dhcp(iface_name) 743 744 def _get_primary_nameserver(self): 745 ifaces = self._get_interfaces_with_dns() 746 if not len(ifaces): 747 raise DnsUpdateError("Interface with valid DNS configured not found.") 748 (iface_dns, iface_name, iface_kind) = ifaces[0] 749 return iface_dns 750 751 def _set_primary_nameserver(self, dns): 752 for iface in self._primary_interfaces: 753 (iface_dns, iface_name, iface_kind) = iface 754 self._netsh_set_dns(iface_name, dns) 755 756 757class _WindowsXpPlatformSettings(_WindowsPlatformSettings): 758 def _ipfw_cmd(self): 759 return (r'third_party\ipfw_win32\ipfw.exe',) 760 761 762def _new_platform_settings(system, release): 763 """Make a new instance of PlatformSettings for the current system.""" 764 if system == 'Darwin': 765 return _OsxPlatformSettings() 766 if system == 'Linux': 767 return _LinuxPlatformSettings() 768 if system == 'Windows' and release == 'XP': 769 return _WindowsXpPlatformSettings() 770 if system == 'Windows': 771 return _WindowsPlatformSettings() 772 if system == 'FreeBSD': 773 return _FreeBSDPlatformSettings() 774 raise NotImplementedError('Sorry %s %s is not supported.' % (system, release)) 775 776 777# Create one instance of the platform-specific settings and 778# make the functions available at the module-level. 779_inst = _new_platform_settings(platform.system(), platform.release()) 780 781get_system_logging_handler = _inst.get_system_logging_handler 782rerun_as_administrator = _inst.rerun_as_administrator 783timer = _inst.timer 784 785get_server_ip_address = _inst.get_server_ip_address 786get_httpproxy_ip_address = _inst.get_httpproxy_ip_address 787get_system_proxy = _inst.get_system_proxy 788ipfw = _inst.ipfw 789has_ipfw = _inst.has_ipfw 790set_temporary_tcp_init_cwnd = _inst.set_temporary_tcp_init_cwnd 791setup_temporary_loopback_config = _inst.setup_temporary_loopback_config 792 793get_original_primary_nameserver = _inst.get_original_primary_nameserver 794set_temporary_primary_nameserver = _inst.set_temporary_primary_nameserver 795