Home | History | Annotate | Download | only in hosts
      1 import os, time, socket, shutil, glob, logging, traceback, tempfile, re
      2 import shlex
      3 import subprocess
      4 
      5 from autotest_lib.client.common_lib import error
      6 from autotest_lib.client.common_lib.global_config import global_config
      7 from autotest_lib.server import utils, autotest
      8 from autotest_lib.server.hosts import host_info
      9 from autotest_lib.server.hosts import remote
     10 from autotest_lib.server.hosts import rpc_server_tracker
     11 from autotest_lib.server.hosts import ssh_multiplex
     12 
     13 # pylint: disable=C0111
     14 
     15 get_value = global_config.get_config_value
     16 enable_master_ssh = get_value('AUTOSERV', 'enable_master_ssh', type=bool,
     17                               default=False)
     18 
     19 
     20 class AbstractSSHHost(remote.RemoteHost):
     21     """
     22     This class represents a generic implementation of most of the
     23     framework necessary for controlling a host via ssh. It implements
     24     almost all of the abstract Host methods, except for the core
     25     Host.run method.
     26     """
     27     VERSION_PREFIX = ''
     28 
     29     def _initialize(self, hostname, user="root", port=22, password="",
     30                     is_client_install_supported=True, afe_host=None,
     31                     host_info_store=None, connection_pool=None,
     32                     *args, **dargs):
     33         super(AbstractSSHHost, self)._initialize(hostname=hostname,
     34                                                  *args, **dargs)
     35         """
     36         @param hostname: The hostname of the host.
     37         @param user: The username to use when ssh'ing into the host.
     38         @param password: The password to use when ssh'ing into the host.
     39         @param port: The port to use for ssh.
     40         @param is_client_install_supported: Boolean to indicate if we can
     41                 install autotest on the host.
     42         @param afe_host: The host object attained from the AFE (get_hosts).
     43         @param host_info_store: Optional host_info.CachingHostInfoStore object
     44                 to obtain / update host information.
     45         @param connection_pool: ssh_multiplex.ConnectionPool instance to share
     46                 the master ssh connection across control scripts.
     47         """
     48         # IP address is retrieved only on demand. Otherwise the host
     49         # initialization will fail for host is not online.
     50         self._ip = None
     51         self.user = user
     52         self.port = port
     53         self.password = password
     54         self._is_client_install_supported = is_client_install_supported
     55         self._use_rsync = None
     56         self.known_hosts_file = tempfile.mkstemp()[1]
     57         self._rpc_server_tracker = rpc_server_tracker.RpcServerTracker(self);
     58 
     59         """
     60         Master SSH connection background job, socket temp directory and socket
     61         control path option. If master-SSH is enabled, these fields will be
     62         initialized by start_master_ssh when a new SSH connection is initiated.
     63         """
     64         self._connection_pool = connection_pool
     65         if connection_pool:
     66             self._master_ssh = connection_pool.get(hostname, user, port)
     67         else:
     68             self._master_ssh = ssh_multiplex.MasterSsh(hostname, user, port)
     69 
     70         self._afe_host = afe_host or utils.EmptyAFEHost()
     71         self.host_info_store = (host_info_store or
     72                                 host_info.InMemoryHostInfoStore())
     73 
     74     @property
     75     def ip(self):
     76         """@return IP address of the host.
     77         """
     78         if not self._ip:
     79             self._ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
     80         return self._ip
     81 
     82 
     83     @property
     84     def is_client_install_supported(self):
     85         """"
     86         Returns True if the host supports autotest client installs, False
     87         otherwise.
     88         """
     89         return self._is_client_install_supported
     90 
     91 
     92     @property
     93     def rpc_server_tracker(self):
     94         """"
     95         @return The RPC server tracker associated with this host.
     96         """
     97         return self._rpc_server_tracker
     98 
     99 
    100     def make_ssh_command(self, user="root", port=22, opts='',
    101                          hosts_file='/dev/null',
    102                          connect_timeout=30, alive_interval=300):
    103         base_command = ("/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no "
    104                         "-o UserKnownHostsFile=%s -o BatchMode=yes "
    105                         "-o ConnectTimeout=%d -o ServerAliveInterval=%d "
    106                         "-l %s -p %d")
    107         assert isinstance(connect_timeout, (int, long))
    108         assert connect_timeout > 0 # can't disable the timeout
    109         return base_command % (opts, hosts_file, connect_timeout,
    110                                alive_interval, user, port)
    111 
    112 
    113     def use_rsync(self):
    114         if self._use_rsync is not None:
    115             return self._use_rsync
    116 
    117         # Check if rsync is available on the remote host. If it's not,
    118         # don't try to use it for any future file transfers.
    119         self._use_rsync = self.check_rsync()
    120         if not self._use_rsync:
    121             logging.warning("rsync not available on remote host %s -- disabled",
    122                          self.hostname)
    123         return self._use_rsync
    124 
    125 
    126     def check_rsync(self):
    127         """
    128         Check if rsync is available on the remote host.
    129         """
    130         try:
    131             self.run("rsync --version", stdout_tee=None, stderr_tee=None)
    132         except error.AutoservRunError:
    133             return False
    134         return True
    135 
    136 
    137     def _encode_remote_paths(self, paths, escape=True, use_scp=False):
    138         """
    139         Given a list of file paths, encodes it as a single remote path, in
    140         the style used by rsync and scp.
    141         escape: add \\ to protect special characters.
    142         use_scp: encode for scp if true, rsync if false.
    143         """
    144         if escape:
    145             paths = [utils.scp_remote_escape(path) for path in paths]
    146 
    147         remote = self.hostname
    148 
    149         # rsync and scp require IPv6 brackets, even when there isn't any
    150         # trailing port number (ssh doesn't support IPv6 brackets).
    151         # In the Python >= 3.3 future, 'import ipaddress' will parse addresses.
    152         if re.search(r':.*:', remote):
    153             remote = '[%s]' % remote
    154 
    155         if use_scp:
    156             return '%s@%s:"%s"' % (self.user, remote, " ".join(paths))
    157         else:
    158             return '%s@%s:%s' % (
    159                     self.user, remote,
    160                     " :".join('"%s"' % p for p in paths))
    161 
    162     def _encode_local_paths(self, paths, escape=True):
    163         """
    164         Given a list of file paths, encodes it as a single local path.
    165         escape: add \\ to protect special characters.
    166         """
    167         if escape:
    168             paths = [utils.sh_escape(path) for path in paths]
    169 
    170         return " ".join('"%s"' % p for p in paths)
    171 
    172     def _make_rsync_cmd(self, sources, dest, delete_dest,
    173                         preserve_symlinks, safe_symlinks):
    174         """
    175         Given a string of source paths and a destination path, produces the
    176         appropriate rsync command for copying them. Remote paths must be
    177         pre-encoded.
    178         """
    179         ssh_cmd = self.make_ssh_command(user=self.user, port=self.port,
    180                                         opts=self._master_ssh.ssh_option,
    181                                         hosts_file=self.known_hosts_file)
    182         if delete_dest:
    183             delete_flag = "--delete"
    184         else:
    185             delete_flag = ""
    186         if safe_symlinks:
    187             symlink_flag = "-l --safe-links"
    188         elif preserve_symlinks:
    189             symlink_flag = "-l"
    190         else:
    191             symlink_flag = "-L"
    192         command = ("rsync %s %s --timeout=1800 --rsh='%s' -az --no-o --no-g "
    193                    "%s \"%s\"")
    194         return command % (symlink_flag, delete_flag, ssh_cmd, sources, dest)
    195 
    196 
    197     def _make_ssh_cmd(self, cmd):
    198         """
    199         Create a base ssh command string for the host which can be used
    200         to run commands directly on the machine
    201         """
    202         base_cmd = self.make_ssh_command(user=self.user, port=self.port,
    203                                          opts=self._master_ssh.ssh_option,
    204                                          hosts_file=self.known_hosts_file)
    205 
    206         return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd))
    207 
    208     def _make_scp_cmd(self, sources, dest):
    209         """
    210         Given a string of source paths and a destination path, produces the
    211         appropriate scp command for encoding it. Remote paths must be
    212         pre-encoded.
    213         """
    214         command = ("scp -rq %s -o StrictHostKeyChecking=no "
    215                    "-o UserKnownHostsFile=%s -P %d %s '%s'")
    216         return command % (self._master_ssh.ssh_option, self.known_hosts_file,
    217                           self.port, sources, dest)
    218 
    219 
    220     def _make_rsync_compatible_globs(self, path, is_local):
    221         """
    222         Given an rsync-style path, returns a list of globbed paths
    223         that will hopefully provide equivalent behaviour for scp. Does not
    224         support the full range of rsync pattern matching behaviour, only that
    225         exposed in the get/send_file interface (trailing slashes).
    226 
    227         The is_local param is flag indicating if the paths should be
    228         interpreted as local or remote paths.
    229         """
    230 
    231         # non-trailing slash paths should just work
    232         if len(path) == 0 or path[-1] != "/":
    233             return [path]
    234 
    235         # make a function to test if a pattern matches any files
    236         if is_local:
    237             def glob_matches_files(path, pattern):
    238                 return len(glob.glob(path + pattern)) > 0
    239         else:
    240             def glob_matches_files(path, pattern):
    241                 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
    242                                                    pattern),
    243                                   stdout_tee=None, ignore_status=True)
    244                 return result.exit_status == 0
    245 
    246         # take a set of globs that cover all files, and see which are needed
    247         patterns = ["*", ".[!.]*"]
    248         patterns = [p for p in patterns if glob_matches_files(path, p)]
    249 
    250         # convert them into a set of paths suitable for the commandline
    251         if is_local:
    252             return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
    253                     for pattern in patterns]
    254         else:
    255             return [utils.scp_remote_escape(path) + pattern
    256                     for pattern in patterns]
    257 
    258 
    259     def _make_rsync_compatible_source(self, source, is_local):
    260         """
    261         Applies the same logic as _make_rsync_compatible_globs, but
    262         applies it to an entire list of sources, producing a new list of
    263         sources, properly quoted.
    264         """
    265         return sum((self._make_rsync_compatible_globs(path, is_local)
    266                     for path in source), [])
    267 
    268 
    269     def _set_umask_perms(self, dest):
    270         """
    271         Given a destination file/dir (recursively) set the permissions on
    272         all the files and directories to the max allowed by running umask.
    273         """
    274 
    275         # now this looks strange but I haven't found a way in Python to _just_
    276         # get the umask, apparently the only option is to try to set it
    277         umask = os.umask(0)
    278         os.umask(umask)
    279 
    280         max_privs = 0777 & ~umask
    281 
    282         def set_file_privs(filename):
    283             """Sets mode of |filename|.  Assumes |filename| exists."""
    284             file_stat = os.stat(filename)
    285 
    286             file_privs = max_privs
    287             # if the original file permissions do not have at least one
    288             # executable bit then do not set it anywhere
    289             if not file_stat.st_mode & 0111:
    290                 file_privs &= ~0111
    291 
    292             os.chmod(filename, file_privs)
    293 
    294         # try a bottom-up walk so changes on directory permissions won't cut
    295         # our access to the files/directories inside it
    296         for root, dirs, files in os.walk(dest, topdown=False):
    297             # when setting the privileges we emulate the chmod "X" behaviour
    298             # that sets to execute only if it is a directory or any of the
    299             # owner/group/other already has execute right
    300             for dirname in dirs:
    301                 os.chmod(os.path.join(root, dirname), max_privs)
    302 
    303             # Filter out broken symlinks as we go.
    304             for filename in filter(os.path.exists, files):
    305                 set_file_privs(os.path.join(root, filename))
    306 
    307 
    308         # now set privs for the dest itself
    309         if os.path.isdir(dest):
    310             os.chmod(dest, max_privs)
    311         else:
    312             set_file_privs(dest)
    313 
    314 
    315     def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
    316                  preserve_symlinks=False, retry=True, safe_symlinks=False):
    317         """
    318         Copy files from the remote host to a local path.
    319 
    320         Directories will be copied recursively.
    321         If a source component is a directory with a trailing slash,
    322         the content of the directory will be copied, otherwise, the
    323         directory itself and its content will be copied. This
    324         behavior is similar to that of the program 'rsync'.
    325 
    326         Args:
    327                 source: either
    328                         1) a single file or directory, as a string
    329                         2) a list of one or more (possibly mixed)
    330                                 files or directories
    331                 dest: a file or a directory (if source contains a
    332                         directory or more than one element, you must
    333                         supply a directory dest)
    334                 delete_dest: if this is true, the command will also clear
    335                              out any old files at dest that are not in the
    336                              source
    337                 preserve_perm: tells get_file() to try to preserve the sources
    338                                permissions on files and dirs
    339                 preserve_symlinks: try to preserve symlinks instead of
    340                                    transforming them into files/dirs on copy
    341                 safe_symlinks: same as preserve_symlinks, but discard links
    342                                that may point outside the copied tree
    343         Raises:
    344                 AutoservRunError: the scp command failed
    345         """
    346         logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,'
    347                       'preserve_perm: %s, preserve_symlinks:%s', source, dest,
    348                       delete_dest, preserve_perm, preserve_symlinks)
    349         # Start a master SSH connection if necessary.
    350         self.start_master_ssh()
    351 
    352         if isinstance(source, basestring):
    353             source = [source]
    354         dest = os.path.abspath(dest)
    355 
    356         # If rsync is disabled or fails, try scp.
    357         try_scp = True
    358         if self.use_rsync():
    359             logging.debug('Using Rsync.')
    360             try:
    361                 remote_source = self._encode_remote_paths(source)
    362                 local_dest = utils.sh_escape(dest)
    363                 rsync = self._make_rsync_cmd(remote_source, local_dest,
    364                                              delete_dest, preserve_symlinks,
    365                                              safe_symlinks)
    366                 utils.run(rsync)
    367                 try_scp = False
    368             except error.CmdError, e:
    369                 # retry on rsync exit values which may be caused by transient
    370                 # network problems:
    371                 #
    372                 # rc 10: Error in socket I/O
    373                 # rc 12: Error in rsync protocol data stream
    374                 # rc 23: Partial transfer due to error
    375                 # rc 255: Ssh error
    376                 #
    377                 # Note that rc 23 includes dangling symlinks.  In this case
    378                 # retrying is useless, but not very damaging since rsync checks
    379                 # for those before starting the transfer (scp does not).
    380                 status = e.result_obj.exit_status
    381                 if status in [10, 12, 23, 255] and retry:
    382                     logging.warning('rsync status %d, retrying', status)
    383                     self.get_file(source, dest, delete_dest, preserve_perm,
    384                                   preserve_symlinks, retry=False)
    385                     # The nested get_file() does all that's needed.
    386                     return
    387                 else:
    388                     logging.warning("trying scp, rsync failed: %s (%d)",
    389                                      e, status)
    390 
    391         if try_scp:
    392             logging.debug('Trying scp.')
    393             # scp has no equivalent to --delete, just drop the entire dest dir
    394             if delete_dest and os.path.isdir(dest):
    395                 shutil.rmtree(dest)
    396                 os.mkdir(dest)
    397 
    398             remote_source = self._make_rsync_compatible_source(source, False)
    399             if remote_source:
    400                 # _make_rsync_compatible_source() already did the escaping
    401                 remote_source = self._encode_remote_paths(
    402                         remote_source, escape=False, use_scp=True)
    403                 local_dest = utils.sh_escape(dest)
    404                 scp = self._make_scp_cmd(remote_source, local_dest)
    405                 try:
    406                     utils.run(scp)
    407                 except error.CmdError, e:
    408                     logging.debug('scp failed: %s', e)
    409                     raise error.AutoservRunError(e.args[0], e.args[1])
    410 
    411         if not preserve_perm:
    412             # we have no way to tell scp to not try to preserve the
    413             # permissions so set them after copy instead.
    414             # for rsync we could use "--no-p --chmod=ugo=rwX" but those
    415             # options are only in very recent rsync versions
    416             self._set_umask_perms(dest)
    417 
    418 
    419     def send_file(self, source, dest, delete_dest=False,
    420                   preserve_symlinks=False):
    421         """
    422         Copy files from a local path to the remote host.
    423 
    424         Directories will be copied recursively.
    425         If a source component is a directory with a trailing slash,
    426         the content of the directory will be copied, otherwise, the
    427         directory itself and its content will be copied. This
    428         behavior is similar to that of the program 'rsync'.
    429 
    430         Args:
    431                 source: either
    432                         1) a single file or directory, as a string
    433                         2) a list of one or more (possibly mixed)
    434                                 files or directories
    435                 dest: a file or a directory (if source contains a
    436                         directory or more than one element, you must
    437                         supply a directory dest)
    438                 delete_dest: if this is true, the command will also clear
    439                              out any old files at dest that are not in the
    440                              source
    441                 preserve_symlinks: controls if symlinks on the source will be
    442                     copied as such on the destination or transformed into the
    443                     referenced file/directory
    444 
    445         Raises:
    446                 AutoservRunError: the scp command failed
    447         """
    448         logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,'
    449                       'preserve_symlinks:%s', source, dest,
    450                       delete_dest, preserve_symlinks)
    451         # Start a master SSH connection if necessary.
    452         self.start_master_ssh()
    453 
    454         if isinstance(source, basestring):
    455             source = [source]
    456 
    457         local_sources = self._encode_local_paths(source)
    458         if not local_sources:
    459             raise error.TestError('source |%s| yielded an empty string' % (
    460                 source))
    461         if local_sources.find('\x00') != -1:
    462             raise error.TestError('one or more sources include NUL char')
    463 
    464         # If rsync is disabled or fails, try scp.
    465         try_scp = True
    466         if self.use_rsync():
    467             logging.debug('Using Rsync.')
    468             remote_dest = self._encode_remote_paths([dest])
    469             try:
    470                 rsync = self._make_rsync_cmd(local_sources, remote_dest,
    471                                              delete_dest, preserve_symlinks,
    472                                              False)
    473                 utils.run(rsync)
    474                 try_scp = False
    475             except error.CmdError, e:
    476                 logging.warning("trying scp, rsync failed: %s", e)
    477 
    478         if try_scp:
    479             logging.debug('Trying scp.')
    480             # scp has no equivalent to --delete, just drop the entire dest dir
    481             if delete_dest:
    482                 is_dir = self.run("ls -d %s/" % dest,
    483                                   ignore_status=True).exit_status == 0
    484                 if is_dir:
    485                     cmd = "rm -rf %s && mkdir %s"
    486                     cmd %= (dest, dest)
    487                     self.run(cmd)
    488 
    489             remote_dest = self._encode_remote_paths([dest], use_scp=True)
    490             local_sources = self._make_rsync_compatible_source(source, True)
    491             if local_sources:
    492                 sources = self._encode_local_paths(local_sources, escape=False)
    493                 scp = self._make_scp_cmd(sources, remote_dest)
    494                 try:
    495                     utils.run(scp)
    496                 except error.CmdError, e:
    497                     logging.debug('scp failed: %s', e)
    498                     raise error.AutoservRunError(e.args[0], e.args[1])
    499             else:
    500                 logging.debug('skipping scp for empty source list')
    501 
    502 
    503     def verify_ssh_user_access(self):
    504         """Verify ssh access to this host.
    505 
    506         @returns False if ssh_ping fails due to Permissions error, True
    507                  otherwise.
    508         """
    509         try:
    510             self.ssh_ping()
    511         except (error.AutoservSshPermissionDeniedError,
    512                 error.AutoservSshPingHostError):
    513             return False
    514         return True
    515 
    516 
    517     def ssh_ping(self, timeout=60, connect_timeout=None, base_cmd='true'):
    518         """
    519         Pings remote host via ssh.
    520 
    521         @param timeout: Time in seconds before giving up.
    522                         Defaults to 60 seconds.
    523         @param base_cmd: The base command to run with the ssh ping.
    524                          Defaults to true.
    525         @raise AutoservSSHTimeout: If the ssh ping times out.
    526         @raise AutoservSshPermissionDeniedError: If ssh ping fails due to
    527                                                  permissions.
    528         @raise AutoservSshPingHostError: For other AutoservRunErrors.
    529         """
    530         ctimeout = min(timeout, connect_timeout or timeout)
    531         try:
    532             self.run(base_cmd, timeout=timeout, connect_timeout=ctimeout,
    533                      ssh_failure_retry_ok=True)
    534         except error.AutoservSSHTimeout:
    535             msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
    536             raise error.AutoservSSHTimeout(msg)
    537         except error.AutoservSshPermissionDeniedError:
    538             #let AutoservSshPermissionDeniedError be visible to the callers
    539             raise
    540         except error.AutoservRunError, e:
    541             # convert the generic AutoservRunError into something more
    542             # specific for this context
    543             raise error.AutoservSshPingHostError(e.description + '\n' +
    544                                                  repr(e.result_obj))
    545 
    546 
    547     def is_up(self, timeout=60, connect_timeout=None, base_cmd='true'):
    548         """
    549         Check if the remote host is up by ssh-ing and running a base command.
    550 
    551         @param timeout: timeout in seconds.
    552         @param base_cmd: a base command to run with ssh. The default is 'true'.
    553         @returns True if the remote host is up before the timeout expires,
    554                  False otherwise.
    555         """
    556         try:
    557             self.ssh_ping(timeout=timeout,
    558                           connect_timeout=connect_timeout,
    559                           base_cmd=base_cmd)
    560         except error.AutoservError:
    561             return False
    562         else:
    563             return True
    564 
    565 
    566     def wait_up(self, timeout=None):
    567         """
    568         Wait until the remote host is up or the timeout expires.
    569 
    570         In fact, it will wait until an ssh connection to the remote
    571         host can be established, and getty is running.
    572 
    573         @param timeout time limit in seconds before returning even
    574             if the host is not up.
    575 
    576         @returns True if the host was found to be up before the timeout expires,
    577                  False otherwise
    578         """
    579         if timeout:
    580             current_time = int(time.time())
    581             end_time = current_time + timeout
    582 
    583         autoserv_error_logged = False
    584         while not timeout or current_time < end_time:
    585             if self.is_up(timeout=end_time - current_time,
    586                           connect_timeout=20):
    587                 try:
    588                     if self.are_wait_up_processes_up():
    589                         logging.debug('Host %s is now up', self.hostname)
    590                         return True
    591                 except error.AutoservError as e:
    592                     if not autoserv_error_logged:
    593                         logging.debug('Ignoring failure to reach %s: %s %s',
    594                                       self.hostname, e,
    595                                       '(and further similar failures)')
    596                         autoserv_error_logged = True
    597             time.sleep(1)
    598             current_time = int(time.time())
    599 
    600         logging.debug('Host %s is still down after waiting %d seconds',
    601                       self.hostname, int(timeout + time.time() - end_time))
    602         return False
    603 
    604 
    605     def wait_down(self, timeout=None, warning_timer=None, old_boot_id=None):
    606         """
    607         Wait until the remote host is down or the timeout expires.
    608 
    609         If old_boot_id is provided, this will wait until either the machine
    610         is unpingable or self.get_boot_id() returns a value different from
    611         old_boot_id. If the boot_id value has changed then the function
    612         returns true under the assumption that the machine has shut down
    613         and has now already come back up.
    614 
    615         If old_boot_id is None then until the machine becomes unreachable the
    616         method assumes the machine has not yet shut down.
    617 
    618         Based on this definition, the 4 possible permutations of timeout
    619         and old_boot_id are:
    620         1. timeout and old_boot_id: wait timeout seconds for either the
    621                                     host to become unpingable, or the boot id
    622                                     to change. In the latter case we've rebooted
    623                                     and in the former case we've only shutdown,
    624                                     but both cases return True.
    625         2. only timeout: wait timeout seconds for the host to become unpingable.
    626                          If the host remains pingable throughout timeout seconds
    627                          we return False.
    628         3. only old_boot_id: wait forever until either the host becomes
    629                              unpingable or the boot_id changes. Return true
    630                              when either of those conditions are met.
    631         4. not timeout, not old_boot_id: wait forever till the host becomes
    632                                          unpingable.
    633 
    634         @param timeout Time limit in seconds before returning even
    635             if the host is still up.
    636         @param warning_timer Time limit in seconds that will generate
    637             a warning if the host is not down yet.
    638         @param old_boot_id A string containing the result of self.get_boot_id()
    639             prior to the host being told to shut down. Can be None if this is
    640             not available.
    641 
    642         @returns True if the host was found to be down, False otherwise
    643         """
    644         #TODO: there is currently no way to distinguish between knowing
    645         #TODO: boot_id was unsupported and not knowing the boot_id.
    646         current_time = int(time.time())
    647         if timeout:
    648             end_time = current_time + timeout
    649 
    650         if warning_timer:
    651             warn_time = current_time + warning_timer
    652 
    653         if old_boot_id is not None:
    654             logging.debug('Host %s pre-shutdown boot_id is %s',
    655                           self.hostname, old_boot_id)
    656 
    657         # Impose semi real-time deadline constraints, since some clients
    658         # (eg: watchdog timer tests) expect strict checking of time elapsed.
    659         # Each iteration of this loop is treated as though it atomically
    660         # completes within current_time, this is needed because if we used
    661         # inline time.time() calls instead then the following could happen:
    662         #
    663         # while not timeout or time.time() < end_time:      [23 < 30]
    664         #    some code.                                     [takes 10 secs]
    665         #    try:
    666         #        new_boot_id = self.get_boot_id(timeout=end_time - time.time())
    667         #                                                   [30 - 33]
    668         # The last step will lead to a return True, when in fact the machine
    669         # went down at 32 seconds (>30). Hence we need to pass get_boot_id
    670         # the same time that allowed us into that iteration of the loop.
    671         while not timeout or current_time < end_time:
    672             try:
    673                 new_boot_id = self.get_boot_id(timeout=end_time-current_time)
    674             except error.AutoservError:
    675                 logging.debug('Host %s is now unreachable over ssh, is down',
    676                               self.hostname)
    677                 return True
    678             else:
    679                 # if the machine is up but the boot_id value has changed from
    680                 # old boot id, then we can assume the machine has gone down
    681                 # and then already come back up
    682                 if old_boot_id is not None and old_boot_id != new_boot_id:
    683                     logging.debug('Host %s now has boot_id %s and so must '
    684                                   'have rebooted', self.hostname, new_boot_id)
    685                     return True
    686 
    687             if warning_timer and current_time > warn_time:
    688                 self.record("INFO", None, "shutdown",
    689                             "Shutdown took longer than %ds" % warning_timer)
    690                 # Print the warning only once.
    691                 warning_timer = None
    692                 # If a machine is stuck switching runlevels
    693                 # This may cause the machine to reboot.
    694                 self.run('kill -HUP 1', ignore_status=True)
    695 
    696             time.sleep(1)
    697             current_time = int(time.time())
    698 
    699         return False
    700 
    701 
    702     # tunable constants for the verify & repair code
    703     AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER",
    704                                                "gb_diskspace_required",
    705                                                type=float,
    706                                                default=20.0)
    707 
    708 
    709     def verify_connectivity(self):
    710         super(AbstractSSHHost, self).verify_connectivity()
    711 
    712         logging.info('Pinging host ' + self.hostname)
    713         self.ssh_ping()
    714         logging.info("Host (ssh) %s is alive", self.hostname)
    715 
    716         if self.is_shutting_down():
    717             raise error.AutoservHostIsShuttingDownError("Host is shutting down")
    718 
    719 
    720     def verify_software(self):
    721         super(AbstractSSHHost, self).verify_software()
    722         try:
    723             self.check_diskspace(autotest.Autotest.get_install_dir(self),
    724                                  self.AUTOTEST_GB_DISKSPACE_REQUIRED)
    725         except error.AutoservHostError:
    726             raise           # only want to raise if it's a space issue
    727         except autotest.AutodirNotFoundError:
    728             # autotest dir may not exist, etc. ignore
    729             logging.debug('autodir space check exception, this is probably '
    730                           'safe to ignore\n' + traceback.format_exc())
    731 
    732 
    733     def close(self):
    734         super(AbstractSSHHost, self).close()
    735         self.rpc_server_tracker.disconnect_all()
    736         if not self._connection_pool:
    737             self._master_ssh.close()
    738         if os.path.exists(self.known_hosts_file):
    739             os.remove(self.known_hosts_file)
    740 
    741 
    742     def restart_master_ssh(self):
    743         """
    744         Stop and restart the ssh master connection.  This is meant as a last
    745         resort when ssh commands fail and we don't understand why.
    746         """
    747         logging.debug('Restarting master ssh connection')
    748         self._master_ssh.close()
    749         self._master_ssh.maybe_start(timeout=30)
    750 
    751 
    752 
    753     def start_master_ssh(self, timeout=5):
    754         """
    755         Called whenever a slave SSH connection needs to be initiated (e.g., by
    756         run, rsync, scp). If master SSH support is enabled and a master SSH
    757         connection is not active already, start a new one in the background.
    758         Also, cleanup any zombie master SSH connections (e.g., dead due to
    759         reboot).
    760 
    761         timeout: timeout in seconds (default 5) to wait for master ssh
    762                  connection to be established. If timeout is reached, a
    763                  warning message is logged, but no other action is taken.
    764         """
    765         if not enable_master_ssh:
    766             return
    767         self._master_ssh.maybe_start(timeout=timeout)
    768 
    769 
    770     def clear_known_hosts(self):
    771         """Clears out the temporary ssh known_hosts file.
    772 
    773         This is useful if the test SSHes to the machine, then reinstalls it,
    774         then SSHes to it again.  It can be called after the reinstall to
    775         reduce the spam in the logs.
    776         """
    777         logging.info("Clearing known hosts for host '%s', file '%s'.",
    778                      self.hostname, self.known_hosts_file)
    779         # Clear out the file by opening it for writing and then closing.
    780         fh = open(self.known_hosts_file, "w")
    781         fh.close()
    782 
    783 
    784     def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True):
    785         """Copy log directories from a host to a local directory.
    786 
    787         @param remote_src_dir: A destination directory on the host.
    788         @param local_dest_dir: A path to a local destination directory.
    789             If it doesn't exist it will be created.
    790         @param ignore_errors: If True, ignore exceptions.
    791 
    792         @raises OSError: If there were problems creating the local_dest_dir and
    793             ignore_errors is False.
    794         @raises AutoservRunError, AutotestRunError: If something goes wrong
    795             while copying the directories and ignore_errors is False.
    796         """
    797         locally_created_dest = False
    798         if (not os.path.exists(local_dest_dir)
    799                 or not os.path.isdir(local_dest_dir)):
    800             try:
    801                 os.makedirs(local_dest_dir)
    802                 locally_created_dest = True
    803             except OSError as e:
    804                 logging.warning('Unable to collect logs from host '
    805                                 '%s: %s', self.hostname, e)
    806                 if not ignore_errors:
    807                     raise
    808                 return
    809         try:
    810             self.get_file(remote_src_dir, local_dest_dir, safe_symlinks=True)
    811         except (error.AutotestRunError, error.AutoservRunError,
    812                 error.AutoservSSHTimeout) as e:
    813             logging.warning('Collection of %s to local dir %s from host %s '
    814                             'failed: %s', remote_src_dir, local_dest_dir,
    815                             self.hostname, e)
    816             if locally_created_dest:
    817                 shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors)
    818             if not ignore_errors:
    819                 raise
    820 
    821 
    822     def create_ssh_tunnel(self, port, local_port):
    823         """Create an ssh tunnel from local_port to port.
    824 
    825         This is used to forward a port securely through a tunnel process from
    826         the server to the DUT for RPC server connection.
    827 
    828         @param port: remote port on the host.
    829         @param local_port: local forwarding port.
    830 
    831         @return: the tunnel process.
    832         """
    833         tunnel_options = '-n -N -q -L %d:localhost:%d' % (local_port, port)
    834         ssh_cmd = self.make_ssh_command(opts=tunnel_options)
    835         tunnel_cmd = '%s %s' % (ssh_cmd, self.hostname)
    836         logging.debug('Full tunnel command: %s', tunnel_cmd)
    837         # Exec the ssh process directly here rather than using a shell.
    838         # Using a shell leaves a dangling ssh process, because we deliver
    839         # signals to the shell wrapping ssh, not the ssh process itself.
    840         args = shlex.split(tunnel_cmd)
    841         tunnel_proc = subprocess.Popen(args, close_fds=True)
    842         logging.debug('Started ssh tunnel, local = %d'
    843                       ' remote = %d, pid = %d',
    844                       local_port, port, tunnel_proc.pid)
    845         return tunnel_proc
    846 
    847 
    848     def disconnect_ssh_tunnel(self, tunnel_proc, port):
    849         """
    850         Disconnects a previously forwarded port from the server to the DUT for
    851         RPC server connection.
    852 
    853         @param tunnel_proc: a tunnel process returned from |create_ssh_tunnel|.
    854         @param port: remote port on the DUT, used in ADBHost.
    855 
    856         """
    857         if tunnel_proc.poll() is None:
    858             tunnel_proc.terminate()
    859             logging.debug('Terminated tunnel, pid %d', tunnel_proc.pid)
    860         else:
    861             logging.debug('Tunnel pid %d terminated early, status %d',
    862                           tunnel_proc.pid, tunnel_proc.returncode)
    863 
    864 
    865     def get_os_type(self):
    866         """Returns the host OS descriptor (to be implemented in subclasses).
    867 
    868         @return A string describing the OS type.
    869         """
    870         raise NotImplementedError
    871