barfab@chromium.org | b6d2993 | 2012-04-11 09:46:43 +0200 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
Chris Masone | 440b4a8 | 2012-06-25 15:00:59 -0700 | [diff] [blame] | 5 | import common, constants, logging, os, socket, stat, sys, threading, time |
barfab@chromium.org | b6d2993 | 2012-04-11 09:46:43 +0200 | [diff] [blame] | 6 | |
Eric Li | f7b8192 | 2011-03-04 14:39:35 -0800 | [diff] [blame] | 7 | from autotest_lib.client.bin import utils |
Paul Stewart | 0b76ab3 | 2012-11-07 12:04:04 -0800 | [diff] [blame^] | 8 | from autotest_lib.client.common_lib import error |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 9 | |
| 10 | class LocalDns(object): |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 11 | """A wrapper around miniFakeDns that runs the server in a separate thread |
| 12 | and redirects all DNS queries to it. |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 13 | """ |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 14 | # This is a symlink. We look up the real path at runtime by following it. |
| 15 | _resolv_bak_file = 'resolv.conf.bak' |
| 16 | |
Chris Masone | 7d04f57 | 2010-05-30 18:07:11 -0700 | [diff] [blame] | 17 | def __init__(self, fake_ip="127.0.0.1", local_port=53): |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 18 | import miniFakeDns # So we don't need to install it in the chroot. |
Chris Masone | e0d3913 | 2011-07-29 19:06:42 -0700 | [diff] [blame] | 19 | self._dns = miniFakeDns.DNSServer(fake_ip=fake_ip, port=local_port) |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 20 | self._stopper = threading.Event() |
| 21 | self._thread = threading.Thread(target=self._dns.run, |
| 22 | args=(self._stopper,)) |
| 23 | |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 24 | def __get_host_by_name(self, hostname): |
| 25 | """Resolve the dotted-quad IPv4 address of |hostname| |
| 26 | |
| 27 | This used to use suave python code, like this: |
| 28 | hosts = socket.getaddrinfo(hostname, 80, socket.AF_INET) |
| 29 | (fam, socktype, proto, canonname, (host, port)) = hosts[0] |
| 30 | return host |
| 31 | |
| 32 | But that hangs sometimes, and we don't understand why. So, use |
| 33 | a subprocess with a timeout. |
| 34 | """ |
| 35 | try: |
| 36 | host = utils.system_output('%s -c "import socket; ' |
| 37 | 'print socket.gethostbyname(\'%s\')"' % ( |
| 38 | sys.executable, hostname), |
| 39 | ignore_status=True, timeout=2) |
| 40 | except Exception as e: |
| 41 | logging.warning(e) |
| 42 | return None |
| 43 | return host or None |
| 44 | |
| 45 | def __attempt_resolve(self, hostname, ip, expected=True): |
| 46 | logging.debug('Attempting to resolve %s to %s' % (hostname, ip)) |
| 47 | host = self.__get_host_by_name(hostname) |
| 48 | logging.debug('Resolve attempt for %s got %s' % (hostname, host)) |
| 49 | return host and (host == ip) == expected |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 50 | |
| 51 | def run(self): |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 52 | """Start the mock DNS server and redirect all queries to it.""" |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 53 | self._thread.start() |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 54 | # Redirect all DNS queries to the mock DNS server. |
| 55 | try: |
| 56 | # Follow resolv.conf symlink. |
| 57 | resolv = os.path.realpath(constants.RESOLV_CONF_FILE) |
| 58 | # Grab path to the real file, do following work in that directory. |
| 59 | resolv_dir = os.path.dirname(resolv) |
| 60 | resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file) |
| 61 | resolv_contents = 'nameserver 127.0.0.1' |
| 62 | # Test to make sure the current resolv.conf isn't already our |
| 63 | # specially modified version. If this is the case, we have |
| 64 | # probably been interrupted while in the middle of this test |
| 65 | # in a previous run. The last thing we want to do at this point |
| 66 | # is to overwrite a legitimate backup. |
| 67 | if (utils.read_one_line(resolv) == resolv_contents and |
| 68 | os.path.exists(resolv_bak)): |
| 69 | logging.error('Current resolv.conf is setup for our local ' |
| 70 | 'server, and a backup already exists! ' |
| 71 | 'Skipping the backup step.') |
| 72 | else: |
| 73 | # Back up the current resolv.conf. |
| 74 | os.rename(resolv, resolv_bak) |
| 75 | # To stop flimflam from editing resolv.conf while we're working |
| 76 | # with it, we want to make the directory -r-xr-xr-x. Open an |
| 77 | # fd to the file first, so that we'll retain the ability to |
| 78 | # alter it. |
| 79 | resolv_fd = open(resolv, 'w') |
| 80 | self._resolv_dir_mode = os.stat(resolv_dir).st_mode |
| 81 | os.chmod(resolv_dir, (stat.S_IRUSR | stat.S_IXUSR | |
| 82 | stat.S_IRGRP | stat.S_IXGRP | |
| 83 | stat.S_IROTH | stat.S_IXOTH)) |
| 84 | resolv_fd.write(resolv_contents) |
| 85 | resolv_fd.close() |
| 86 | assert utils.read_one_line(resolv) == resolv_contents |
| 87 | except Exception as e: |
| 88 | logging.error(str(e)) |
| 89 | raise e |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 90 | |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 91 | utils.poll_for_condition( |
| 92 | lambda: self.__attempt_resolve('www.google.com.', '127.0.0.1'), |
| 93 | utils.TimeoutError('Timed out waiting for DNS changes.'), |
| 94 | timeout=10) |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 95 | |
| 96 | def stop(self): |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 97 | """Restore the backed-up DNS settings and stop the mock DNS server.""" |
| 98 | try: |
| 99 | # Follow resolv.conf symlink. |
| 100 | resolv = os.path.realpath(constants.RESOLV_CONF_FILE) |
| 101 | # Grab path to the real file, do following work in that directory. |
| 102 | resolv_dir = os.path.dirname(resolv) |
| 103 | resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file) |
| 104 | os.chmod(resolv_dir, self._resolv_dir_mode) |
Paul Stewart | 0b76ab3 | 2012-11-07 12:04:04 -0800 | [diff] [blame^] | 105 | if os.path.exists(resolv_bak): |
| 106 | os.rename(resolv_bak, resolv) |
| 107 | else: |
| 108 | # This probably means shill restarted during the execution |
| 109 | # of our test, and has cleaned up the .bak file we created. |
| 110 | raise error.TestError('Backup file %s no longer exists! ' |
| 111 | 'Connection manager probably crashed ' |
| 112 | 'during the test run.' % |
| 113 | resolv_bak) |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 114 | |
| 115 | utils.poll_for_condition( |
| 116 | lambda: self.__attempt_resolve('www.google.com.', |
| 117 | '127.0.0.1', |
| 118 | expected=False), |
| 119 | utils.TimeoutError('Timed out waiting to revert DNS. ' |
| 120 | 'resolv.conf contents are: ' + |
| 121 | utils.read_one_line(resolv)), |
| 122 | timeout=10) |
| 123 | finally: |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame] | 124 | # Stop the DNS server. |
| 125 | self._stopper.set() |
| 126 | self._thread.join() |