blob: b31f9e51471737b8c6980cba458a9d0d4294df4e [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
Chris Masonef7ae5252010-04-21 08:21:25 -07008
9class LocalDns(object):
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020010 """A wrapper around miniFakeDns that runs the server in a separate thread
11 and redirects all DNS queries to it.
Chris Masonef7ae5252010-04-21 08:21:25 -070012 """
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020013 # This is a symlink. We look up the real path at runtime by following it.
14 _resolv_bak_file = 'resolv.conf.bak'
15
Chris Masone7d04f572010-05-30 18:07:11 -070016 def __init__(self, fake_ip="127.0.0.1", local_port=53):
Chris Masonef7ae5252010-04-21 08:21:25 -070017 import miniFakeDns # So we don't need to install it in the chroot.
Chris Masonee0d39132011-07-29 19:06:42 -070018 self._dns = miniFakeDns.DNSServer(fake_ip=fake_ip, port=local_port)
Chris Masonef7ae5252010-04-21 08:21:25 -070019 self._stopper = threading.Event()
20 self._thread = threading.Thread(target=self._dns.run,
21 args=(self._stopper,))
22
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020023 def __get_host_by_name(self, hostname):
24 """Resolve the dotted-quad IPv4 address of |hostname|
25
26 This used to use suave python code, like this:
27 hosts = socket.getaddrinfo(hostname, 80, socket.AF_INET)
28 (fam, socktype, proto, canonname, (host, port)) = hosts[0]
29 return host
30
31 But that hangs sometimes, and we don't understand why. So, use
32 a subprocess with a timeout.
33 """
34 try:
35 host = utils.system_output('%s -c "import socket; '
36 'print socket.gethostbyname(\'%s\')"' % (
37 sys.executable, hostname),
38 ignore_status=True, timeout=2)
39 except Exception as e:
40 logging.warning(e)
41 return None
42 return host or None
43
44 def __attempt_resolve(self, hostname, ip, expected=True):
45 logging.debug('Attempting to resolve %s to %s' % (hostname, ip))
46 host = self.__get_host_by_name(hostname)
47 logging.debug('Resolve attempt for %s got %s' % (hostname, host))
48 return host and (host == ip) == expected
Chris Masonef7ae5252010-04-21 08:21:25 -070049
50 def run(self):
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020051 """Start the mock DNS server and redirect all queries to it."""
Chris Masonef7ae5252010-04-21 08:21:25 -070052 self._thread.start()
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020053 # Redirect all DNS queries to the mock DNS server.
54 try:
55 # Follow resolv.conf symlink.
56 resolv = os.path.realpath(constants.RESOLV_CONF_FILE)
57 # Grab path to the real file, do following work in that directory.
58 resolv_dir = os.path.dirname(resolv)
59 resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file)
60 resolv_contents = 'nameserver 127.0.0.1'
61 # Test to make sure the current resolv.conf isn't already our
62 # specially modified version. If this is the case, we have
63 # probably been interrupted while in the middle of this test
64 # in a previous run. The last thing we want to do at this point
65 # is to overwrite a legitimate backup.
66 if (utils.read_one_line(resolv) == resolv_contents and
67 os.path.exists(resolv_bak)):
68 logging.error('Current resolv.conf is setup for our local '
69 'server, and a backup already exists! '
70 'Skipping the backup step.')
71 else:
72 # Back up the current resolv.conf.
73 os.rename(resolv, resolv_bak)
74 # To stop flimflam from editing resolv.conf while we're working
75 # with it, we want to make the directory -r-xr-xr-x. Open an
76 # fd to the file first, so that we'll retain the ability to
77 # alter it.
78 resolv_fd = open(resolv, 'w')
79 self._resolv_dir_mode = os.stat(resolv_dir).st_mode
80 os.chmod(resolv_dir, (stat.S_IRUSR | stat.S_IXUSR |
81 stat.S_IRGRP | stat.S_IXGRP |
82 stat.S_IROTH | stat.S_IXOTH))
83 resolv_fd.write(resolv_contents)
84 resolv_fd.close()
85 assert utils.read_one_line(resolv) == resolv_contents
86 except Exception as e:
87 logging.error(str(e))
88 raise e
Chris Masonef7ae5252010-04-21 08:21:25 -070089
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020090 utils.poll_for_condition(
91 lambda: self.__attempt_resolve('www.google.com.', '127.0.0.1'),
92 utils.TimeoutError('Timed out waiting for DNS changes.'),
93 timeout=10)
Chris Masonef7ae5252010-04-21 08:21:25 -070094
95 def stop(self):
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +020096 """Restore the backed-up DNS settings and stop the mock DNS server."""
97 try:
98 # Follow resolv.conf symlink.
99 resolv = os.path.realpath(constants.RESOLV_CONF_FILE)
100 # Grab path to the real file, do following work in that directory.
101 resolv_dir = os.path.dirname(resolv)
102 resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file)
103 os.chmod(resolv_dir, self._resolv_dir_mode)
104 os.rename(resolv_bak, resolv)
105
106 utils.poll_for_condition(
107 lambda: self.__attempt_resolve('www.google.com.',
108 '127.0.0.1',
109 expected=False),
110 utils.TimeoutError('Timed out waiting to revert DNS. '
111 'resolv.conf contents are: ' +
112 utils.read_one_line(resolv)),
113 timeout=10)
114 finally:
Bartosz Fabianowski6fe8dee2012-06-21 16:35:57 +0200115 # Stop the DNS server.
116 self._stopper.set()
117 self._thread.join()