blob: 57571cb4f31758d2f677d920b2c753f76ee0175d [file] [log] [blame]
barfab@chromium.orgb6d29932012-04-11 09:46:43 +02001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Chris Masonef7ae5252010-04-21 08:21:25 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Chris Masone440b4a82012-06-25 15:00:59 -07005import common, constants, logging, os, socket, stat, sys, threading, time
barfab@chromium.orgb6d29932012-04-11 09:46:43 +02006
Eric Lif7b81922011-03-04 14:39:35 -08007from autotest_lib.client.bin import utils
Paul Stewart0b76ab32012-11-07 12:04:04 -08008from autotest_lib.client.common_lib import error
Chris Masonef7ae5252010-04-21 08:21:25 -07009
10class LocalDns(object):
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020011 """A wrapper around miniFakeDns that runs the server in a separate thread
12 and redirects all DNS queries to it.
Chris Masonef7ae5252010-04-21 08:21:25 -070013 """
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020014 # This is a symlink. We look up the real path at runtime by following it.
15 _resolv_bak_file = 'resolv.conf.bak'
16
Chris Masone7d04f572010-05-30 18:07:11 -070017 def __init__(self, fake_ip="127.0.0.1", local_port=53):
Chris Masonef7ae5252010-04-21 08:21:25 -070018 import miniFakeDns # So we don't need to install it in the chroot.
Chris Masonee0d39132011-07-29 19:06:42 -070019 self._dns = miniFakeDns.DNSServer(fake_ip=fake_ip, port=local_port)
Chris Masonef7ae5252010-04-21 08:21:25 -070020 self._stopper = threading.Event()
21 self._thread = threading.Thread(target=self._dns.run,
22 args=(self._stopper,))
23
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020024 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 Masonef7ae5252010-04-21 08:21:25 -070050
51 def run(self):
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020052 """Start the mock DNS server and redirect all queries to it."""
Chris Masonef7ae5252010-04-21 08:21:25 -070053 self._thread.start()
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020054 # 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 Masonef7ae5252010-04-21 08:21:25 -070090
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020091 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 Masonef7ae5252010-04-21 08:21:25 -070095
96 def stop(self):
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020097 """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 Stewart0b76ab32012-11-07 12:04:04 -0800105 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 Fabianowski6fe8dee2012-06-21 16:35:57 +0200114
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 Fabianowski6fe8dee2012-06-21 16:35:57 +0200124 # Stop the DNS server.
125 self._stopper.set()
126 self._thread.join()