# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import common, constants, logging, os, socket, stat, sys, threading, time from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import error class LocalDns(object): """A wrapper around miniFakeDns that runs the server in a separate thread and redirects all DNS queries to it. """ # This is a symlink. We look up the real path at runtime by following it. _resolv_bak_file = 'resolv.conf.bak' def __init__(self, fake_ip="127.0.0.1", local_port=53): import miniFakeDns # So we don't need to install it in the chroot. self._dns = miniFakeDns.DNSServer(fake_ip=fake_ip, port=local_port) self._stopper = threading.Event() self._thread = threading.Thread(target=self._dns.run, args=(self._stopper,)) def __get_host_by_name(self, hostname): """Resolve the dotted-quad IPv4 address of |hostname| This used to use suave python code, like this: hosts = socket.getaddrinfo(hostname, 80, socket.AF_INET) (fam, socktype, proto, canonname, (host, port)) = hosts[0] return host But that hangs sometimes, and we don't understand why. So, use a subprocess with a timeout. """ try: host = utils.system_output('%s -c "import socket; ' 'print socket.gethostbyname(\'%s\')"' % ( sys.executable, hostname), ignore_status=True, timeout=2) except Exception as e: logging.warning(e) return None return host or None def __attempt_resolve(self, hostname, ip, expected=True): logging.debug('Attempting to resolve %s to %s' % (hostname, ip)) host = self.__get_host_by_name(hostname) logging.debug('Resolve attempt for %s got %s' % (hostname, host)) return host and (host == ip) == expected def run(self): """Start the mock DNS server and redirect all queries to it.""" self._thread.start() # Redirect all DNS queries to the mock DNS server. try: # Follow resolv.conf symlink. resolv = os.path.realpath(constants.RESOLV_CONF_FILE) # Grab path to the real file, do following work in that directory. resolv_dir = os.path.dirname(resolv) resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file) resolv_contents = 'nameserver 127.0.0.1' # Test to make sure the current resolv.conf isn't already our # specially modified version. If this is the case, we have # probably been interrupted while in the middle of this test # in a previous run. The last thing we want to do at this point # is to overwrite a legitimate backup. if (utils.read_one_line(resolv) == resolv_contents and os.path.exists(resolv_bak)): logging.error('Current resolv.conf is setup for our local ' 'server, and a backup already exists! ' 'Skipping the backup step.') else: # Back up the current resolv.conf. os.rename(resolv, resolv_bak) # To stop flimflam from editing resolv.conf while we're working # with it, we want to make the directory -r-xr-xr-x. Open an # fd to the file first, so that we'll retain the ability to # alter it. resolv_fd = open(resolv, 'w') self._resolv_dir_mode = os.stat(resolv_dir).st_mode os.chmod(resolv_dir, (stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)) resolv_fd.write(resolv_contents) resolv_fd.close() assert utils.read_one_line(resolv) == resolv_contents except Exception as e: logging.error(str(e)) raise e utils.poll_for_condition( lambda: self.__attempt_resolve('www.google.com.', '127.0.0.1'), utils.TimeoutError('Timed out waiting for DNS changes.'), timeout=10) def stop(self): """Restore the backed-up DNS settings and stop the mock DNS server.""" try: # Follow resolv.conf symlink. resolv = os.path.realpath(constants.RESOLV_CONF_FILE) # Grab path to the real file, do following work in that directory. resolv_dir = os.path.dirname(resolv) resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file) os.chmod(resolv_dir, self._resolv_dir_mode) if os.path.exists(resolv_bak): os.rename(resolv_bak, resolv) else: # This probably means shill restarted during the execution # of our test, and has cleaned up the .bak file we created. raise error.TestError('Backup file %s no longer exists! ' 'Connection manager probably crashed ' 'during the test run.' % resolv_bak) utils.poll_for_condition( lambda: self.__attempt_resolve('www.google.com.', '127.0.0.1', expected=False), utils.TimeoutError('Timed out waiting to revert DNS. ' 'resolv.conf contents are: ' + utils.read_one_line(resolv)), timeout=10) finally: # Stop the DNS server. self._stopper.set() self._thread.join()