1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2008 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Lifrom __future__ import absolute_import 7*9c5db199SXin Lifrom __future__ import division 8*9c5db199SXin Lifrom __future__ import print_function 9*9c5db199SXin Li 10*9c5db199SXin Liimport os, time, socket, shutil, glob, logging, tempfile, re 11*9c5db199SXin Liimport shlex 12*9c5db199SXin Liimport subprocess 13*9c5db199SXin Li 14*9c5db199SXin Lifrom autotest_lib.client.bin.result_tools import runner as result_tools_runner 15*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 16*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 17*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import ping_runner 18*9c5db199SXin Lifrom autotest_lib.client.common_lib.global_config import global_config 19*9c5db199SXin Lifrom autotest_lib.server import autoserv_parser 20*9c5db199SXin Lifrom autotest_lib.server import utils, autotest 21*9c5db199SXin Lifrom autotest_lib.server.hosts import host_info 22*9c5db199SXin Lifrom autotest_lib.server.hosts import remote 23*9c5db199SXin Lifrom autotest_lib.server.hosts import rpc_server_tracker 24*9c5db199SXin Lifrom autotest_lib.server.hosts import ssh_multiplex 25*9c5db199SXin Lifrom autotest_lib.server.hosts.tls_client import exec_dut_command 26*9c5db199SXin Li 27*9c5db199SXin Liimport six 28*9c5db199SXin Lifrom six.moves import filter 29*9c5db199SXin Li 30*9c5db199SXin Litry: 31*9c5db199SXin Li from autotest_lib.utils.frozen_chromite.lib import metrics 32*9c5db199SXin Liexcept ImportError: 33*9c5db199SXin Li metrics = utils.metrics_mock 34*9c5db199SXin Li 35*9c5db199SXin Li# pylint: disable=C0111 36*9c5db199SXin Li 37*9c5db199SXin Liget_value = global_config.get_config_value 38*9c5db199SXin Lienable_main_ssh = get_value('AUTOSERV', 39*9c5db199SXin Li 'enable_main_ssh', 40*9c5db199SXin Li type=bool, 41*9c5db199SXin Li default=False) 42*9c5db199SXin Li 43*9c5db199SXin LiENABLE_EXEC_DUT_COMMAND = get_value('AUTOSERV', 44*9c5db199SXin Li 'enable_tls', 45*9c5db199SXin Li type=bool, 46*9c5db199SXin Li default=False) 47*9c5db199SXin Li 48*9c5db199SXin Li# Number of seconds to use the cached up status. 49*9c5db199SXin Li_DEFAULT_UP_STATUS_EXPIRATION_SECONDS = 300 50*9c5db199SXin Li_DEFAULT_SSH_PORT = None 51*9c5db199SXin Li 52*9c5db199SXin Li# Number of seconds to wait for the host to shut down in wait_down(). 53*9c5db199SXin Li_DEFAULT_WAIT_DOWN_TIME_SECONDS = 120 54*9c5db199SXin Li 55*9c5db199SXin Li# Number of seconds to wait for the host to boot up in wait_up(). 56*9c5db199SXin Li_DEFAULT_WAIT_UP_TIME_SECONDS = 120 57*9c5db199SXin Li 58*9c5db199SXin Li# Timeout in seconds for a single call of get_boot_id() in wait_down() 59*9c5db199SXin Li# and a single ssh ping in wait_up(). 60*9c5db199SXin Li_DEFAULT_MAX_PING_TIMEOUT = 10 61*9c5db199SXin Li 62*9c5db199SXin Li# The client symlink directory. 63*9c5db199SXin LiAUTOTEST_CLIENT_SYMLINK_END = 'client/autotest_lib' 64*9c5db199SXin Li 65*9c5db199SXin Li 66*9c5db199SXin Liclass AbstractSSHHost(remote.RemoteHost): 67*9c5db199SXin Li """ 68*9c5db199SXin Li This class represents a generic implementation of most of the 69*9c5db199SXin Li framework necessary for controlling a host via ssh. It implements 70*9c5db199SXin Li almost all of the abstract Host methods, except for the core 71*9c5db199SXin Li Host.run method. 72*9c5db199SXin Li """ 73*9c5db199SXin Li VERSION_PREFIX = '' 74*9c5db199SXin Li # Timeout for main ssh connection setup, in seconds. 75*9c5db199SXin Li DEFAULT_START_MAIN_SSH_TIMEOUT_S = 5 76*9c5db199SXin Li 77*9c5db199SXin Li def _initialize(self, 78*9c5db199SXin Li hostname, 79*9c5db199SXin Li user="root", 80*9c5db199SXin Li port=_DEFAULT_SSH_PORT, 81*9c5db199SXin Li password="", 82*9c5db199SXin Li is_client_install_supported=True, 83*9c5db199SXin Li afe_host=None, 84*9c5db199SXin Li host_info_store=None, 85*9c5db199SXin Li connection_pool=None, 86*9c5db199SXin Li *args, 87*9c5db199SXin Li **dargs): 88*9c5db199SXin Li super(AbstractSSHHost, self)._initialize(hostname=hostname, 89*9c5db199SXin Li *args, **dargs) 90*9c5db199SXin Li """ 91*9c5db199SXin Li @param hostname: The hostname of the host. 92*9c5db199SXin Li @param user: The username to use when ssh'ing into the host. 93*9c5db199SXin Li @param password: The password to use when ssh'ing into the host. 94*9c5db199SXin Li @param port: The port to use for ssh. 95*9c5db199SXin Li @param is_client_install_supported: Boolean to indicate if we can 96*9c5db199SXin Li install autotest on the host. 97*9c5db199SXin Li @param afe_host: The host object attained from the AFE (get_hosts). 98*9c5db199SXin Li @param host_info_store: Optional host_info.CachingHostInfoStore object 99*9c5db199SXin Li to obtain / update host information. 100*9c5db199SXin Li @param connection_pool: ssh_multiplex.ConnectionPool instance to share 101*9c5db199SXin Li the main ssh connection across control scripts. 102*9c5db199SXin Li """ 103*9c5db199SXin Li self._track_class_usage() 104*9c5db199SXin Li # IP address is retrieved only on demand. Otherwise the host 105*9c5db199SXin Li # initialization will fail for host is not online. 106*9c5db199SXin Li self._ip = None 107*9c5db199SXin Li self.user = user 108*9c5db199SXin Li self.port = port 109*9c5db199SXin Li self.password = password 110*9c5db199SXin Li self._is_client_install_supported = is_client_install_supported 111*9c5db199SXin Li self._use_rsync = None 112*9c5db199SXin Li self.known_hosts_file = tempfile.mkstemp()[1] 113*9c5db199SXin Li self._rpc_server_tracker = rpc_server_tracker.RpcServerTracker(self); 114*9c5db199SXin Li self._tls_exec_dut_command_client = None 115*9c5db199SXin Li self._tls_unstable = False 116*9c5db199SXin Li 117*9c5db199SXin Li # Read the value of the use_icmp flag, setting to true if missing. 118*9c5db199SXin Li args_string = autoserv_parser.autoserv_parser.options.args 119*9c5db199SXin Li args_dict = utils.args_to_dict( 120*9c5db199SXin Li args_string.split() if args_string is not None else '') 121*9c5db199SXin Li value = args_dict.get('use_icmp', 'true').lower() 122*9c5db199SXin Li if value == 'true': 123*9c5db199SXin Li self._use_icmp = True 124*9c5db199SXin Li elif value == 'false': 125*9c5db199SXin Li self._use_icmp = False 126*9c5db199SXin Li else: 127*9c5db199SXin Li raise ValueError( 128*9c5db199SXin Li 'use_icmp must be true or false: {}'.format(value)) 129*9c5db199SXin Li """ 130*9c5db199SXin Li Main SSH connection background job, socket temp directory and socket 131*9c5db199SXin Li control path option. If main-SSH is enabled, these fields will be 132*9c5db199SXin Li initialized by start_main_ssh when a new SSH connection is initiated. 133*9c5db199SXin Li """ 134*9c5db199SXin Li self._connection_pool = connection_pool 135*9c5db199SXin Li if connection_pool: 136*9c5db199SXin Li self._main_ssh = connection_pool.get(hostname, user, port) 137*9c5db199SXin Li else: 138*9c5db199SXin Li self._main_ssh = ssh_multiplex.MainSsh(hostname, user, port) 139*9c5db199SXin Li 140*9c5db199SXin Li self._afe_host = afe_host or utils.EmptyAFEHost() 141*9c5db199SXin Li self.host_info_store = (host_info_store or 142*9c5db199SXin Li host_info.InMemoryHostInfoStore()) 143*9c5db199SXin Li 144*9c5db199SXin Li # The cached status of whether the DUT responded to ping. 145*9c5db199SXin Li self._cached_up_status = None 146*9c5db199SXin Li # The timestamp when the value of _cached_up_status is set. 147*9c5db199SXin Li self._cached_up_status_updated = None 148*9c5db199SXin Li 149*9c5db199SXin Li 150*9c5db199SXin Li @property 151*9c5db199SXin Li def ip(self): 152*9c5db199SXin Li """@return IP address of the host. 153*9c5db199SXin Li """ 154*9c5db199SXin Li if not self._ip: 155*9c5db199SXin Li self._ip = socket.getaddrinfo(self.hostname, None)[0][4][0] 156*9c5db199SXin Li return self._ip 157*9c5db199SXin Li 158*9c5db199SXin Li 159*9c5db199SXin Li @property 160*9c5db199SXin Li def is_client_install_supported(self): 161*9c5db199SXin Li """" 162*9c5db199SXin Li Returns True if the host supports autotest client installs, False 163*9c5db199SXin Li otherwise. 164*9c5db199SXin Li """ 165*9c5db199SXin Li return self._is_client_install_supported 166*9c5db199SXin Li 167*9c5db199SXin Li def is_satlab(self): 168*9c5db199SXin Li """Determine if the host is part of satlab 169*9c5db199SXin Li 170*9c5db199SXin Li TODO(otabek@): Remove or update to better logic to determime Satlab. 171*9c5db199SXin Li 172*9c5db199SXin Li @returns True if ths host is running under satlab otherwise False. 173*9c5db199SXin Li """ 174*9c5db199SXin Li if not hasattr(self, '_is_satlab'): 175*9c5db199SXin Li self._is_satlab = self.hostname.startswith('satlab-') 176*9c5db199SXin Li return self._is_satlab 177*9c5db199SXin Li 178*9c5db199SXin Li @property 179*9c5db199SXin Li def rpc_server_tracker(self): 180*9c5db199SXin Li """" 181*9c5db199SXin Li @return The RPC server tracker associated with this host. 182*9c5db199SXin Li """ 183*9c5db199SXin Li return self._rpc_server_tracker 184*9c5db199SXin Li 185*9c5db199SXin Li 186*9c5db199SXin Li @property 187*9c5db199SXin Li def is_default_port(self): 188*9c5db199SXin Li """Returns True if its port is default SSH port.""" 189*9c5db199SXin Li return self.port == _DEFAULT_SSH_PORT or self.port is None 190*9c5db199SXin Li 191*9c5db199SXin Li @property 192*9c5db199SXin Li def host_port(self): 193*9c5db199SXin Li """Returns hostname if port is default. Otherwise, hostname:port. 194*9c5db199SXin Li """ 195*9c5db199SXin Li if self.is_default_port: 196*9c5db199SXin Li return self.hostname 197*9c5db199SXin Li else: 198*9c5db199SXin Li return '%s:%d' % (self.hostname, self.port) 199*9c5db199SXin Li 200*9c5db199SXin Li @property 201*9c5db199SXin Li def use_icmp(self): 202*9c5db199SXin Li """Returns True if icmp pings are allowed.""" 203*9c5db199SXin Li return self._use_icmp 204*9c5db199SXin Li 205*9c5db199SXin Li 206*9c5db199SXin Li # Though it doesn't use self here, it is not declared as staticmethod 207*9c5db199SXin Li # because its subclass may use self to access member variables. 208*9c5db199SXin Li def make_ssh_command(self, user="root", port=_DEFAULT_SSH_PORT, opts='', 209*9c5db199SXin Li hosts_file='/dev/null', connect_timeout=30, 210*9c5db199SXin Li alive_interval=300, alive_count_max=3, 211*9c5db199SXin Li connection_attempts=1): 212*9c5db199SXin Li ssh_options = " ".join([ 213*9c5db199SXin Li opts, 214*9c5db199SXin Li self.make_ssh_options( 215*9c5db199SXin Li hosts_file=hosts_file, connect_timeout=connect_timeout, 216*9c5db199SXin Li alive_interval=alive_interval, alive_count_max=alive_count_max, 217*9c5db199SXin Li connection_attempts=connection_attempts)]) 218*9c5db199SXin Li return ("/usr/bin/ssh -a -x %s -l %s %s" % 219*9c5db199SXin Li (ssh_options, user, "-p %d " % port if port else "")) 220*9c5db199SXin Li 221*9c5db199SXin Li 222*9c5db199SXin Li @staticmethod 223*9c5db199SXin Li def make_ssh_options(hosts_file='/dev/null', connect_timeout=30, 224*9c5db199SXin Li alive_interval=300, alive_count_max=3, 225*9c5db199SXin Li connection_attempts=1): 226*9c5db199SXin Li """Composes SSH -o options.""" 227*9c5db199SXin Li assert isinstance(connect_timeout, six.integer_types) 228*9c5db199SXin Li assert connect_timeout > 0 # can't disable the timeout 229*9c5db199SXin Li 230*9c5db199SXin Li options = [("StrictHostKeyChecking", "no"), 231*9c5db199SXin Li ("UserKnownHostsFile", hosts_file), 232*9c5db199SXin Li ("BatchMode", "yes"), 233*9c5db199SXin Li ("ConnectTimeout", str(connect_timeout)), 234*9c5db199SXin Li ("ServerAliveInterval", str(alive_interval)), 235*9c5db199SXin Li ("ServerAliveCountMax", str(alive_count_max)), 236*9c5db199SXin Li ("ConnectionAttempts", str(connection_attempts))] 237*9c5db199SXin Li return " ".join("-o %s=%s" % kv for kv in options) 238*9c5db199SXin Li 239*9c5db199SXin Li 240*9c5db199SXin Li def use_rsync(self): 241*9c5db199SXin Li if self._use_rsync is not None: 242*9c5db199SXin Li return self._use_rsync 243*9c5db199SXin Li 244*9c5db199SXin Li # Check if rsync is available on the remote host. If it's not, 245*9c5db199SXin Li # don't try to use it for any future file transfers. 246*9c5db199SXin Li self._use_rsync = self.check_rsync() 247*9c5db199SXin Li if not self._use_rsync: 248*9c5db199SXin Li logging.warning("rsync not available on remote host %s -- disabled", 249*9c5db199SXin Li self.host_port) 250*9c5db199SXin Li return self._use_rsync 251*9c5db199SXin Li 252*9c5db199SXin Li 253*9c5db199SXin Li def check_rsync(self): 254*9c5db199SXin Li """ 255*9c5db199SXin Li Check if rsync is available on the remote host. 256*9c5db199SXin Li """ 257*9c5db199SXin Li try: 258*9c5db199SXin Li self.run("rsync --version", stdout_tee=None, stderr_tee=None) 259*9c5db199SXin Li except error.AutoservRunError: 260*9c5db199SXin Li return False 261*9c5db199SXin Li return True 262*9c5db199SXin Li 263*9c5db199SXin Li 264*9c5db199SXin Li def _encode_remote_paths(self, paths, escape=True, use_scp=False): 265*9c5db199SXin Li """ 266*9c5db199SXin Li Given a list of file paths, encodes it as a single remote path, in 267*9c5db199SXin Li the style used by rsync and scp. 268*9c5db199SXin Li escape: add \\ to protect special characters. 269*9c5db199SXin Li use_scp: encode for scp if true, rsync if false. 270*9c5db199SXin Li """ 271*9c5db199SXin Li if escape: 272*9c5db199SXin Li paths = [utils.scp_remote_escape(path) for path in paths] 273*9c5db199SXin Li 274*9c5db199SXin Li remote = self.hostname 275*9c5db199SXin Li 276*9c5db199SXin Li # rsync and scp require IPv6 brackets, even when there isn't any 277*9c5db199SXin Li # trailing port number (ssh doesn't support IPv6 brackets). 278*9c5db199SXin Li # In the Python >= 3.3 future, 'import ipaddress' will parse addresses. 279*9c5db199SXin Li if re.search(r':.*:', remote): 280*9c5db199SXin Li remote = '[%s]' % remote 281*9c5db199SXin Li 282*9c5db199SXin Li if use_scp: 283*9c5db199SXin Li return '%s@%s:"%s"' % (self.user, remote, " ".join(paths)) 284*9c5db199SXin Li else: 285*9c5db199SXin Li return '%s@%s:%s' % ( 286*9c5db199SXin Li self.user, remote, 287*9c5db199SXin Li " :".join('"%s"' % p for p in paths)) 288*9c5db199SXin Li 289*9c5db199SXin Li def _encode_local_paths(self, paths, escape=True): 290*9c5db199SXin Li """ 291*9c5db199SXin Li Given a list of file paths, encodes it as a single local path. 292*9c5db199SXin Li escape: add \\ to protect special characters. 293*9c5db199SXin Li """ 294*9c5db199SXin Li if escape: 295*9c5db199SXin Li paths = [utils.sh_escape(path) for path in paths] 296*9c5db199SXin Li 297*9c5db199SXin Li return " ".join('"%s"' % p for p in paths) 298*9c5db199SXin Li 299*9c5db199SXin Li 300*9c5db199SXin Li def rsync_options(self, delete_dest=False, preserve_symlinks=False, 301*9c5db199SXin Li safe_symlinks=False, excludes=None): 302*9c5db199SXin Li """Obtains rsync options for the remote.""" 303*9c5db199SXin Li ssh_cmd = self.make_ssh_command(user=self.user, port=self.port, 304*9c5db199SXin Li opts=self._main_ssh.ssh_option, 305*9c5db199SXin Li hosts_file=self.known_hosts_file) 306*9c5db199SXin Li if delete_dest: 307*9c5db199SXin Li delete_flag = "--delete" 308*9c5db199SXin Li else: 309*9c5db199SXin Li delete_flag = "" 310*9c5db199SXin Li if safe_symlinks: 311*9c5db199SXin Li symlink_flag = "-l --safe-links" 312*9c5db199SXin Li elif preserve_symlinks: 313*9c5db199SXin Li symlink_flag = "-l" 314*9c5db199SXin Li else: 315*9c5db199SXin Li symlink_flag = "-L" 316*9c5db199SXin Li exclude_args = '' 317*9c5db199SXin Li if excludes: 318*9c5db199SXin Li exclude_args = ' '.join( 319*9c5db199SXin Li ["--exclude '%s'" % exclude for exclude in excludes]) 320*9c5db199SXin Li return "%s %s --timeout=1800 --rsh='%s' -az --no-o --no-g %s" % ( 321*9c5db199SXin Li symlink_flag, delete_flag, ssh_cmd, exclude_args) 322*9c5db199SXin Li 323*9c5db199SXin Li 324*9c5db199SXin Li def _make_rsync_cmd(self, sources, dest, delete_dest, 325*9c5db199SXin Li preserve_symlinks, safe_symlinks, excludes=None): 326*9c5db199SXin Li """ 327*9c5db199SXin Li Given a string of source paths and a destination path, produces the 328*9c5db199SXin Li appropriate rsync command for copying them. Remote paths must be 329*9c5db199SXin Li pre-encoded. 330*9c5db199SXin Li """ 331*9c5db199SXin Li rsync_options = self.rsync_options( 332*9c5db199SXin Li delete_dest=delete_dest, preserve_symlinks=preserve_symlinks, 333*9c5db199SXin Li safe_symlinks=safe_symlinks, excludes=excludes) 334*9c5db199SXin Li return 'rsync %s %s "%s"' % (rsync_options, sources, dest) 335*9c5db199SXin Li 336*9c5db199SXin Li 337*9c5db199SXin Li def _make_ssh_cmd(self, cmd): 338*9c5db199SXin Li """ 339*9c5db199SXin Li Create a base ssh command string for the host which can be used 340*9c5db199SXin Li to run commands directly on the machine 341*9c5db199SXin Li """ 342*9c5db199SXin Li base_cmd = self.make_ssh_command(user=self.user, port=self.port, 343*9c5db199SXin Li opts=self._main_ssh.ssh_option, 344*9c5db199SXin Li hosts_file=self.known_hosts_file) 345*9c5db199SXin Li 346*9c5db199SXin Li return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd)) 347*9c5db199SXin Li 348*9c5db199SXin Li def _make_scp_cmd(self, sources, dest): 349*9c5db199SXin Li """ 350*9c5db199SXin Li Given a string of source paths and a destination path, produces the 351*9c5db199SXin Li appropriate scp command for encoding it. Remote paths must be 352*9c5db199SXin Li pre-encoded. 353*9c5db199SXin Li """ 354*9c5db199SXin Li command = ("scp -rq %s -o StrictHostKeyChecking=no " 355*9c5db199SXin Li "-o UserKnownHostsFile=%s %s%s '%s'") 356*9c5db199SXin Li return command % (self._main_ssh.ssh_option, self.known_hosts_file, 357*9c5db199SXin Li "-P %d " % self.port if self.port else '', sources, 358*9c5db199SXin Li dest) 359*9c5db199SXin Li 360*9c5db199SXin Li 361*9c5db199SXin Li def _make_rsync_compatible_globs(self, path, is_local): 362*9c5db199SXin Li """ 363*9c5db199SXin Li Given an rsync-style path, returns a list of globbed paths 364*9c5db199SXin Li that will hopefully provide equivalent behaviour for scp. Does not 365*9c5db199SXin Li support the full range of rsync pattern matching behaviour, only that 366*9c5db199SXin Li exposed in the get/send_file interface (trailing slashes). 367*9c5db199SXin Li 368*9c5db199SXin Li The is_local param is flag indicating if the paths should be 369*9c5db199SXin Li interpreted as local or remote paths. 370*9c5db199SXin Li """ 371*9c5db199SXin Li 372*9c5db199SXin Li # non-trailing slash paths should just work 373*9c5db199SXin Li if len(path) == 0 or path[-1] != "/": 374*9c5db199SXin Li return [path] 375*9c5db199SXin Li 376*9c5db199SXin Li # make a function to test if a pattern matches any files 377*9c5db199SXin Li if is_local: 378*9c5db199SXin Li def glob_matches_files(path, pattern): 379*9c5db199SXin Li return len(glob.glob(path + pattern)) > 0 380*9c5db199SXin Li else: 381*9c5db199SXin Li def glob_matches_files(path, pattern): 382*9c5db199SXin Li result = self.run("ls \"%s\"%s" % (utils.sh_escape(path), 383*9c5db199SXin Li pattern), 384*9c5db199SXin Li stdout_tee=None, ignore_status=True) 385*9c5db199SXin Li return result.exit_status == 0 386*9c5db199SXin Li 387*9c5db199SXin Li # take a set of globs that cover all files, and see which are needed 388*9c5db199SXin Li patterns = ["*", ".[!.]*"] 389*9c5db199SXin Li patterns = [p for p in patterns if glob_matches_files(path, p)] 390*9c5db199SXin Li 391*9c5db199SXin Li # convert them into a set of paths suitable for the commandline 392*9c5db199SXin Li if is_local: 393*9c5db199SXin Li return ["\"%s\"%s" % (utils.sh_escape(path), pattern) 394*9c5db199SXin Li for pattern in patterns] 395*9c5db199SXin Li else: 396*9c5db199SXin Li return [utils.scp_remote_escape(path) + pattern 397*9c5db199SXin Li for pattern in patterns] 398*9c5db199SXin Li 399*9c5db199SXin Li 400*9c5db199SXin Li def _make_rsync_compatible_source(self, source, is_local): 401*9c5db199SXin Li """ 402*9c5db199SXin Li Applies the same logic as _make_rsync_compatible_globs, but 403*9c5db199SXin Li applies it to an entire list of sources, producing a new list of 404*9c5db199SXin Li sources, properly quoted. 405*9c5db199SXin Li """ 406*9c5db199SXin Li return sum((self._make_rsync_compatible_globs(path, is_local) 407*9c5db199SXin Li for path in source), []) 408*9c5db199SXin Li 409*9c5db199SXin Li 410*9c5db199SXin Li def _set_umask_perms(self, dest): 411*9c5db199SXin Li """ 412*9c5db199SXin Li Given a destination file/dir (recursively) set the permissions on 413*9c5db199SXin Li all the files and directories to the max allowed by running umask. 414*9c5db199SXin Li """ 415*9c5db199SXin Li 416*9c5db199SXin Li # now this looks strange but I haven't found a way in Python to _just_ 417*9c5db199SXin Li # get the umask, apparently the only option is to try to set it 418*9c5db199SXin Li umask = os.umask(0) 419*9c5db199SXin Li os.umask(umask) 420*9c5db199SXin Li 421*9c5db199SXin Li max_privs = 0o777 & ~umask 422*9c5db199SXin Li 423*9c5db199SXin Li def set_file_privs(filename): 424*9c5db199SXin Li """Sets mode of |filename|. Assumes |filename| exists.""" 425*9c5db199SXin Li file_stat = os.stat(filename) 426*9c5db199SXin Li 427*9c5db199SXin Li file_privs = max_privs 428*9c5db199SXin Li # if the original file permissions do not have at least one 429*9c5db199SXin Li # executable bit then do not set it anywhere 430*9c5db199SXin Li if not file_stat.st_mode & 0o111: 431*9c5db199SXin Li file_privs &= ~0o111 432*9c5db199SXin Li 433*9c5db199SXin Li os.chmod(filename, file_privs) 434*9c5db199SXin Li 435*9c5db199SXin Li # try a bottom-up walk so changes on directory permissions won't cut 436*9c5db199SXin Li # our access to the files/directories inside it 437*9c5db199SXin Li for root, dirs, files in os.walk(dest, topdown=False): 438*9c5db199SXin Li # when setting the privileges we emulate the chmod "X" behaviour 439*9c5db199SXin Li # that sets to execute only if it is a directory or any of the 440*9c5db199SXin Li # owner/group/other already has execute right 441*9c5db199SXin Li for dirname in dirs: 442*9c5db199SXin Li os.chmod(os.path.join(root, dirname), max_privs) 443*9c5db199SXin Li 444*9c5db199SXin Li # Filter out broken symlinks as we go. 445*9c5db199SXin Li for filename in filter(os.path.exists, files): 446*9c5db199SXin Li set_file_privs(os.path.join(root, filename)) 447*9c5db199SXin Li 448*9c5db199SXin Li 449*9c5db199SXin Li # now set privs for the dest itself 450*9c5db199SXin Li if os.path.isdir(dest): 451*9c5db199SXin Li os.chmod(dest, max_privs) 452*9c5db199SXin Li else: 453*9c5db199SXin Li set_file_privs(dest) 454*9c5db199SXin Li 455*9c5db199SXin Li 456*9c5db199SXin Li def get_file(self, source, dest, delete_dest=False, preserve_perm=True, 457*9c5db199SXin Li preserve_symlinks=False, retry=True, safe_symlinks=False, 458*9c5db199SXin Li try_rsync=True): 459*9c5db199SXin Li """ 460*9c5db199SXin Li Copy files from the remote host to a local path. 461*9c5db199SXin Li 462*9c5db199SXin Li Directories will be copied recursively. 463*9c5db199SXin Li If a source component is a directory with a trailing slash, 464*9c5db199SXin Li the content of the directory will be copied, otherwise, the 465*9c5db199SXin Li directory itself and its content will be copied. This 466*9c5db199SXin Li behavior is similar to that of the program 'rsync'. 467*9c5db199SXin Li 468*9c5db199SXin Li Args: 469*9c5db199SXin Li source: either 470*9c5db199SXin Li 1) a single file or directory, as a string 471*9c5db199SXin Li 2) a list of one or more (possibly mixed) 472*9c5db199SXin Li files or directories 473*9c5db199SXin Li dest: a file or a directory (if source contains a 474*9c5db199SXin Li directory or more than one element, you must 475*9c5db199SXin Li supply a directory dest) 476*9c5db199SXin Li delete_dest: if this is true, the command will also clear 477*9c5db199SXin Li out any old files at dest that are not in the 478*9c5db199SXin Li source 479*9c5db199SXin Li preserve_perm: tells get_file() to try to preserve the sources 480*9c5db199SXin Li permissions on files and dirs 481*9c5db199SXin Li preserve_symlinks: try to preserve symlinks instead of 482*9c5db199SXin Li transforming them into files/dirs on copy 483*9c5db199SXin Li safe_symlinks: same as preserve_symlinks, but discard links 484*9c5db199SXin Li that may point outside the copied tree 485*9c5db199SXin Li try_rsync: set to False to skip directly to using scp 486*9c5db199SXin Li Raises: 487*9c5db199SXin Li AutoservRunError: the scp command failed 488*9c5db199SXin Li """ 489*9c5db199SXin Li logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,' 490*9c5db199SXin Li 'preserve_perm: %s, preserve_symlinks:%s', source, dest, 491*9c5db199SXin Li delete_dest, preserve_perm, preserve_symlinks) 492*9c5db199SXin Li 493*9c5db199SXin Li # Start a main SSH connection if necessary. 494*9c5db199SXin Li self.start_main_ssh() 495*9c5db199SXin Li 496*9c5db199SXin Li if isinstance(source, six.string_types): 497*9c5db199SXin Li source = [source] 498*9c5db199SXin Li dest = os.path.abspath(dest) 499*9c5db199SXin Li 500*9c5db199SXin Li # If rsync is disabled or fails, try scp. 501*9c5db199SXin Li try_scp = True 502*9c5db199SXin Li if try_rsync and self.use_rsync(): 503*9c5db199SXin Li logging.debug('Using Rsync.') 504*9c5db199SXin Li try: 505*9c5db199SXin Li remote_source = self._encode_remote_paths(source) 506*9c5db199SXin Li local_dest = utils.sh_escape(dest) 507*9c5db199SXin Li rsync = self._make_rsync_cmd(remote_source, local_dest, 508*9c5db199SXin Li delete_dest, preserve_symlinks, 509*9c5db199SXin Li safe_symlinks) 510*9c5db199SXin Li utils.run(rsync) 511*9c5db199SXin Li try_scp = False 512*9c5db199SXin Li except error.CmdError as e: 513*9c5db199SXin Li # retry on rsync exit values which may be caused by transient 514*9c5db199SXin Li # network problems: 515*9c5db199SXin Li # 516*9c5db199SXin Li # rc 10: Error in socket I/O 517*9c5db199SXin Li # rc 12: Error in rsync protocol data stream 518*9c5db199SXin Li # rc 23: Partial transfer due to error 519*9c5db199SXin Li # rc 255: Ssh error 520*9c5db199SXin Li # 521*9c5db199SXin Li # Note that rc 23 includes dangling symlinks. In this case 522*9c5db199SXin Li # retrying is useless, but not very damaging since rsync checks 523*9c5db199SXin Li # for those before starting the transfer (scp does not). 524*9c5db199SXin Li status = e.result_obj.exit_status 525*9c5db199SXin Li if status in [10, 12, 23, 255] and retry: 526*9c5db199SXin Li logging.warning('rsync status %d, retrying', status) 527*9c5db199SXin Li self.get_file(source, dest, delete_dest, preserve_perm, 528*9c5db199SXin Li preserve_symlinks, retry=False) 529*9c5db199SXin Li # The nested get_file() does all that's needed. 530*9c5db199SXin Li return 531*9c5db199SXin Li else: 532*9c5db199SXin Li logging.warning("trying scp, rsync failed: %s (%d)", 533*9c5db199SXin Li e, status) 534*9c5db199SXin Li 535*9c5db199SXin Li if try_scp: 536*9c5db199SXin Li logging.debug('Trying scp.') 537*9c5db199SXin Li # scp has no equivalent to --delete, just drop the entire dest dir 538*9c5db199SXin Li if delete_dest and os.path.isdir(dest): 539*9c5db199SXin Li shutil.rmtree(dest) 540*9c5db199SXin Li os.mkdir(dest) 541*9c5db199SXin Li 542*9c5db199SXin Li remote_source = self._make_rsync_compatible_source(source, False) 543*9c5db199SXin Li if remote_source: 544*9c5db199SXin Li # _make_rsync_compatible_source() already did the escaping 545*9c5db199SXin Li remote_source = self._encode_remote_paths( 546*9c5db199SXin Li remote_source, escape=False, use_scp=True) 547*9c5db199SXin Li local_dest = utils.sh_escape(dest) 548*9c5db199SXin Li scp = self._make_scp_cmd(remote_source, local_dest) 549*9c5db199SXin Li try: 550*9c5db199SXin Li utils.run(scp) 551*9c5db199SXin Li except error.CmdError as e: 552*9c5db199SXin Li logging.debug('scp failed: %s', e) 553*9c5db199SXin Li raise error.AutoservRunError(e.args[0], e.args[1]) 554*9c5db199SXin Li 555*9c5db199SXin Li if not preserve_perm: 556*9c5db199SXin Li # we have no way to tell scp to not try to preserve the 557*9c5db199SXin Li # permissions so set them after copy instead. 558*9c5db199SXin Li # for rsync we could use "--no-p --chmod=ugo=rwX" but those 559*9c5db199SXin Li # options are only in very recent rsync versions 560*9c5db199SXin Li self._set_umask_perms(dest) 561*9c5db199SXin Li 562*9c5db199SXin Li 563*9c5db199SXin Li def send_file(self, source, dest, delete_dest=False, 564*9c5db199SXin Li preserve_symlinks=False, excludes=None): 565*9c5db199SXin Li """ 566*9c5db199SXin Li Copy files from a local path to the remote host. 567*9c5db199SXin Li 568*9c5db199SXin Li Directories will be copied recursively. 569*9c5db199SXin Li If a source component is a directory with a trailing slash, 570*9c5db199SXin Li the content of the directory will be copied, otherwise, the 571*9c5db199SXin Li directory itself and its content will be copied. This 572*9c5db199SXin Li behavior is similar to that of the program 'rsync'. 573*9c5db199SXin Li 574*9c5db199SXin Li Args: 575*9c5db199SXin Li source: either 576*9c5db199SXin Li 1) a single file or directory, as a string 577*9c5db199SXin Li 2) a list of one or more (possibly mixed) 578*9c5db199SXin Li files or directories 579*9c5db199SXin Li dest: a file or a directory (if source contains a 580*9c5db199SXin Li directory or more than one element, you must 581*9c5db199SXin Li supply a directory dest) 582*9c5db199SXin Li delete_dest: if this is true, the command will also clear 583*9c5db199SXin Li out any old files at dest that are not in the 584*9c5db199SXin Li source 585*9c5db199SXin Li preserve_symlinks: controls if symlinks on the source will be 586*9c5db199SXin Li copied as such on the destination or transformed into the 587*9c5db199SXin Li referenced file/directory 588*9c5db199SXin Li excludes: A list of file pattern that matches files not to be 589*9c5db199SXin Li sent. `send_file` will fail if exclude is set, since 590*9c5db199SXin Li local copy does not support --exclude, e.g., when 591*9c5db199SXin Li using scp to copy file. 592*9c5db199SXin Li 593*9c5db199SXin Li Raises: 594*9c5db199SXin Li AutoservRunError: the scp command failed 595*9c5db199SXin Li """ 596*9c5db199SXin Li logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,' 597*9c5db199SXin Li 'preserve_symlinks:%s', source, dest, 598*9c5db199SXin Li delete_dest, preserve_symlinks) 599*9c5db199SXin Li # Start a main SSH connection if necessary. 600*9c5db199SXin Li self.start_main_ssh() 601*9c5db199SXin Li 602*9c5db199SXin Li if isinstance(source, six.string_types): 603*9c5db199SXin Li source = [source] 604*9c5db199SXin Li 605*9c5db199SXin Li client_symlink = _client_symlink(source) 606*9c5db199SXin Li # The client symlink *must* be preserved, and should not be sent with 607*9c5db199SXin Li # the main send_file in case scp is used, which does not support symlink 608*9c5db199SXin Li if client_symlink: 609*9c5db199SXin Li source.remove(client_symlink) 610*9c5db199SXin Li 611*9c5db199SXin Li local_sources = self._encode_local_paths(source) 612*9c5db199SXin Li if not local_sources: 613*9c5db199SXin Li raise error.TestError('source |%s| yielded an empty string' % ( 614*9c5db199SXin Li source)) 615*9c5db199SXin Li if local_sources.find('\x00') != -1: 616*9c5db199SXin Li raise error.TestError('one or more sources include NUL char') 617*9c5db199SXin Li 618*9c5db199SXin Li self._send_file( 619*9c5db199SXin Li dest=dest, 620*9c5db199SXin Li source=source, 621*9c5db199SXin Li local_sources=local_sources, 622*9c5db199SXin Li delete_dest=delete_dest, 623*9c5db199SXin Li excludes=excludes, 624*9c5db199SXin Li preserve_symlinks=preserve_symlinks) 625*9c5db199SXin Li 626*9c5db199SXin Li # Send the client symlink after the rest of the autotest repo has been 627*9c5db199SXin Li # sent. 628*9c5db199SXin Li if client_symlink: 629*9c5db199SXin Li self._send_client_symlink(dest=dest, 630*9c5db199SXin Li source=[client_symlink], 631*9c5db199SXin Li local_sources=client_symlink, 632*9c5db199SXin Li delete_dest=delete_dest, 633*9c5db199SXin Li excludes=excludes, 634*9c5db199SXin Li preserve_symlinks=True) 635*9c5db199SXin Li 636*9c5db199SXin Li def _send_client_symlink(self, dest, source, local_sources, delete_dest, 637*9c5db199SXin Li excludes, preserve_symlinks): 638*9c5db199SXin Li if self.use_rsync(): 639*9c5db199SXin Li if self._send_using_rsync(dest=dest, 640*9c5db199SXin Li local_sources=local_sources, 641*9c5db199SXin Li delete_dest=delete_dest, 642*9c5db199SXin Li preserve_symlinks=preserve_symlinks, 643*9c5db199SXin Li excludes=excludes): 644*9c5db199SXin Li return 645*9c5db199SXin Li # Manually create the symlink if rsync is not available, or fails. 646*9c5db199SXin Li try: 647*9c5db199SXin Li self.run('mkdir {f} && touch {f}/__init__.py && cd {f} && ' 648*9c5db199SXin Li 'ln -s ../ client'.format( 649*9c5db199SXin Li f=os.path.join(dest, 'autotest_lib'))) 650*9c5db199SXin Li except Exception as e: 651*9c5db199SXin Li raise error.AutotestHostRunError( 652*9c5db199SXin Li "Could not create client symlink on host: %s" % e) 653*9c5db199SXin Li 654*9c5db199SXin Li def _send_file(self, dest, source, local_sources, delete_dest, excludes, 655*9c5db199SXin Li preserve_symlinks): 656*9c5db199SXin Li """Send file(s), trying rsync first, then scp.""" 657*9c5db199SXin Li if self.use_rsync(): 658*9c5db199SXin Li rsync_success = self._send_using_rsync( 659*9c5db199SXin Li dest=dest, 660*9c5db199SXin Li local_sources=local_sources, 661*9c5db199SXin Li delete_dest=delete_dest, 662*9c5db199SXin Li preserve_symlinks=preserve_symlinks, 663*9c5db199SXin Li excludes=excludes) 664*9c5db199SXin Li if rsync_success: 665*9c5db199SXin Li return 666*9c5db199SXin Li 667*9c5db199SXin Li # Send using scp if you cannot via rsync, or rsync fails. 668*9c5db199SXin Li self._send_using_scp(dest=dest, 669*9c5db199SXin Li source=source, 670*9c5db199SXin Li delete_dest=delete_dest, 671*9c5db199SXin Li excludes=excludes) 672*9c5db199SXin Li 673*9c5db199SXin Li def _send_using_rsync(self, dest, local_sources, delete_dest, 674*9c5db199SXin Li preserve_symlinks, excludes): 675*9c5db199SXin Li """Send using rsync. 676*9c5db199SXin Li 677*9c5db199SXin Li Args: 678*9c5db199SXin Li dest: a file or a directory (if source contains a 679*9c5db199SXin Li directory or more than one element, you must 680*9c5db199SXin Li supply a directory dest) 681*9c5db199SXin Li local_sources: a string of files/dirs to send separated with spaces 682*9c5db199SXin Li delete_dest: if this is true, the command will also clear 683*9c5db199SXin Li out any old files at dest that are not in the 684*9c5db199SXin Li source 685*9c5db199SXin Li preserve_symlinks: controls if symlinks on the source will be 686*9c5db199SXin Li copied as such on the destination or transformed into the 687*9c5db199SXin Li referenced file/directory 688*9c5db199SXin Li excludes: A list of file pattern that matches files not to be 689*9c5db199SXin Li sent. `send_file` will fail if exclude is set, since 690*9c5db199SXin Li local copy does not support --exclude, e.g., when 691*9c5db199SXin Li using scp to copy file. 692*9c5db199SXin Li Returns: 693*9c5db199SXin Li bool: True if the cmd succeeded, else False 694*9c5db199SXin Li 695*9c5db199SXin Li """ 696*9c5db199SXin Li logging.debug('Using Rsync.') 697*9c5db199SXin Li remote_dest = self._encode_remote_paths([dest]) 698*9c5db199SXin Li try: 699*9c5db199SXin Li rsync = self._make_rsync_cmd(local_sources, 700*9c5db199SXin Li remote_dest, 701*9c5db199SXin Li delete_dest, 702*9c5db199SXin Li preserve_symlinks, 703*9c5db199SXin Li False, 704*9c5db199SXin Li excludes=excludes) 705*9c5db199SXin Li utils.run(rsync) 706*9c5db199SXin Li return True 707*9c5db199SXin Li except error.CmdError as e: 708*9c5db199SXin Li logging.warning("trying scp, rsync failed: %s", e) 709*9c5db199SXin Li return False 710*9c5db199SXin Li 711*9c5db199SXin Li def _send_using_scp(self, dest, source, delete_dest, excludes): 712*9c5db199SXin Li """Send using scp. 713*9c5db199SXin Li 714*9c5db199SXin Li Args: 715*9c5db199SXin Li source: either 716*9c5db199SXin Li 1) a single file or directory, as a string 717*9c5db199SXin Li 2) a list of one or more (possibly mixed) 718*9c5db199SXin Li files or directories 719*9c5db199SXin Li dest: a file or a directory (if source contains a 720*9c5db199SXin Li directory or more than one element, you must 721*9c5db199SXin Li supply a directory dest) 722*9c5db199SXin Li delete_dest: if this is true, the command will also clear 723*9c5db199SXin Li out any old files at dest that are not in the 724*9c5db199SXin Li source 725*9c5db199SXin Li excludes: A list of file pattern that matches files not to be 726*9c5db199SXin Li sent. `send_file` will fail if exclude is set, since 727*9c5db199SXin Li local copy does not support --exclude, e.g., when 728*9c5db199SXin Li using scp to copy file. 729*9c5db199SXin Li 730*9c5db199SXin Li Raises: 731*9c5db199SXin Li AutoservRunError: the scp command failed 732*9c5db199SXin Li """ 733*9c5db199SXin Li logging.debug('Trying scp.') 734*9c5db199SXin Li if excludes: 735*9c5db199SXin Li raise error.AutotestHostRunError( 736*9c5db199SXin Li '--exclude is not supported in scp, try to use rsync. ' 737*9c5db199SXin Li 'excludes: %s' % ','.join(excludes), None) 738*9c5db199SXin Li 739*9c5db199SXin Li # scp has no equivalent to --delete, just drop the entire dest dir 740*9c5db199SXin Li if delete_dest: 741*9c5db199SXin Li is_dir = self.run("ls -d %s/" % dest, 742*9c5db199SXin Li ignore_status=True).exit_status == 0 743*9c5db199SXin Li if is_dir: 744*9c5db199SXin Li cmd = "rm -rf %s && mkdir %s" 745*9c5db199SXin Li cmd %= (dest, dest) 746*9c5db199SXin Li self.run(cmd) 747*9c5db199SXin Li 748*9c5db199SXin Li remote_dest = self._encode_remote_paths([dest], use_scp=True) 749*9c5db199SXin Li local_sources = self._make_rsync_compatible_source(source, True) 750*9c5db199SXin Li if local_sources: 751*9c5db199SXin Li sources = self._encode_local_paths(local_sources, escape=False) 752*9c5db199SXin Li scp = self._make_scp_cmd(sources, remote_dest) 753*9c5db199SXin Li try: 754*9c5db199SXin Li utils.run(scp) 755*9c5db199SXin Li except error.CmdError as e: 756*9c5db199SXin Li logging.debug('scp failed: %s', e) 757*9c5db199SXin Li raise error.AutoservRunError(e.args[0], e.args[1]) 758*9c5db199SXin Li else: 759*9c5db199SXin Li logging.debug('skipping scp for empty source list') 760*9c5db199SXin Li 761*9c5db199SXin Li def verify_ssh_user_access(self): 762*9c5db199SXin Li """Verify ssh access to this host. 763*9c5db199SXin Li 764*9c5db199SXin Li @returns False if ssh_ping fails due to Permissions error, True 765*9c5db199SXin Li otherwise. 766*9c5db199SXin Li """ 767*9c5db199SXin Li try: 768*9c5db199SXin Li self.ssh_ping() 769*9c5db199SXin Li except (error.AutoservSshPermissionDeniedError, 770*9c5db199SXin Li error.AutoservSshPingHostError): 771*9c5db199SXin Li return False 772*9c5db199SXin Li return True 773*9c5db199SXin Li 774*9c5db199SXin Li 775*9c5db199SXin Li def ssh_ping(self, timeout=60, connect_timeout=None, base_cmd='true'): 776*9c5db199SXin Li """ 777*9c5db199SXin Li Pings remote host via ssh. 778*9c5db199SXin Li 779*9c5db199SXin Li @param timeout: Command execution timeout in seconds. 780*9c5db199SXin Li Defaults to 60 seconds. 781*9c5db199SXin Li @param connect_timeout: ssh connection timeout in seconds. 782*9c5db199SXin Li @param base_cmd: The base command to run with the ssh ping. 783*9c5db199SXin Li Defaults to true. 784*9c5db199SXin Li @raise AutoservSSHTimeout: If the ssh ping times out. 785*9c5db199SXin Li @raise AutoservSshPermissionDeniedError: If ssh ping fails due to 786*9c5db199SXin Li permissions. 787*9c5db199SXin Li @raise AutoservSshPingHostError: For other AutoservRunErrors. 788*9c5db199SXin Li """ 789*9c5db199SXin Li ctimeout = min(timeout, connect_timeout or timeout) 790*9c5db199SXin Li try: 791*9c5db199SXin Li self.run(base_cmd, timeout=timeout, connect_timeout=ctimeout, 792*9c5db199SXin Li ssh_failure_retry_ok=True) 793*9c5db199SXin Li except error.AutoservSSHTimeout: 794*9c5db199SXin Li msg = "Host (ssh) verify timed out (timeout = %d)" % timeout 795*9c5db199SXin Li raise error.AutoservSSHTimeout(msg) 796*9c5db199SXin Li except error.AutoservSshPermissionDeniedError: 797*9c5db199SXin Li #let AutoservSshPermissionDeniedError be visible to the callers 798*9c5db199SXin Li raise 799*9c5db199SXin Li except error.AutoservRunError as e: 800*9c5db199SXin Li # convert the generic AutoservRunError into something more 801*9c5db199SXin Li # specific for this context 802*9c5db199SXin Li raise error.AutoservSshPingHostError(e.description + '\n' + 803*9c5db199SXin Li repr(e.result_obj)) 804*9c5db199SXin Li 805*9c5db199SXin Li 806*9c5db199SXin Li def is_up(self, timeout=60, connect_timeout=None, base_cmd='true'): 807*9c5db199SXin Li """ 808*9c5db199SXin Li Check if the remote host is up by ssh-ing and running a base command. 809*9c5db199SXin Li 810*9c5db199SXin Li @param timeout: command execution timeout in seconds. 811*9c5db199SXin Li @param connect_timeout: ssh connection timeout in seconds. 812*9c5db199SXin Li @param base_cmd: a base command to run with ssh. The default is 'true'. 813*9c5db199SXin Li @returns True if the remote host is up before the timeout expires, 814*9c5db199SXin Li False otherwise. 815*9c5db199SXin Li """ 816*9c5db199SXin Li try: 817*9c5db199SXin Li self.ssh_ping(timeout=timeout, 818*9c5db199SXin Li connect_timeout=connect_timeout, 819*9c5db199SXin Li base_cmd=base_cmd) 820*9c5db199SXin Li except error.AutoservError: 821*9c5db199SXin Li return False 822*9c5db199SXin Li else: 823*9c5db199SXin Li return True 824*9c5db199SXin Li 825*9c5db199SXin Li 826*9c5db199SXin Li def is_up_fast(self, count=1): 827*9c5db199SXin Li """Return True if the host can be pinged. 828*9c5db199SXin Li 829*9c5db199SXin Li @param count How many time try to ping before decide that host is not 830*9c5db199SXin Li reachable by ping. 831*9c5db199SXin Li """ 832*9c5db199SXin Li if not self._use_icmp: 833*9c5db199SXin Li stack = self._get_server_stack_state(lowest_frames=1, 834*9c5db199SXin Li highest_frames=7) 835*9c5db199SXin Li logging.warning("is_up_fast called with icmp disabled from %s!", 836*9c5db199SXin Li stack) 837*9c5db199SXin Li return True 838*9c5db199SXin Li ping_config = ping_runner.PingConfig(self.hostname, 839*9c5db199SXin Li count=1, 840*9c5db199SXin Li ignore_result=True, 841*9c5db199SXin Li ignore_status=True) 842*9c5db199SXin Li 843*9c5db199SXin Li # Run up to the amount specified, but also exit as soon as the first 844*9c5db199SXin Li # reply is found. 845*9c5db199SXin Li loops_remaining = count 846*9c5db199SXin Li while loops_remaining > 0: 847*9c5db199SXin Li loops_remaining -= 1 848*9c5db199SXin Li if ping_runner.PingRunner().ping(ping_config).received > 0: 849*9c5db199SXin Li return True 850*9c5db199SXin Li return False 851*9c5db199SXin Li 852*9c5db199SXin Li 853*9c5db199SXin Li def wait_up(self, 854*9c5db199SXin Li timeout=_DEFAULT_WAIT_UP_TIME_SECONDS, 855*9c5db199SXin Li host_is_down=False): 856*9c5db199SXin Li """ 857*9c5db199SXin Li Wait until the remote host is up or the timeout expires. 858*9c5db199SXin Li 859*9c5db199SXin Li In fact, it will wait until an ssh connection to the remote 860*9c5db199SXin Li host can be established, and getty is running. 861*9c5db199SXin Li 862*9c5db199SXin Li @param timeout time limit in seconds before returning even 863*9c5db199SXin Li if the host is not up. 864*9c5db199SXin Li @param host_is_down set to True if the host is known to be down before 865*9c5db199SXin Li wait_up. 866*9c5db199SXin Li 867*9c5db199SXin Li @returns True if the host was found to be up before the timeout expires, 868*9c5db199SXin Li False otherwise 869*9c5db199SXin Li """ 870*9c5db199SXin Li if host_is_down: 871*9c5db199SXin Li # Since we expect the host to be down when this is called, if there is 872*9c5db199SXin Li # an existing ssh main connection close it. 873*9c5db199SXin Li self.close_main_ssh() 874*9c5db199SXin Li current_time = int(time.time()) 875*9c5db199SXin Li end_time = current_time + timeout 876*9c5db199SXin Li 877*9c5db199SXin Li ssh_success_logged = False 878*9c5db199SXin Li autoserv_error_logged = False 879*9c5db199SXin Li while current_time < end_time: 880*9c5db199SXin Li ping_timeout = min(_DEFAULT_MAX_PING_TIMEOUT, 881*9c5db199SXin Li end_time - current_time) 882*9c5db199SXin Li if self.is_up(timeout=ping_timeout, connect_timeout=ping_timeout): 883*9c5db199SXin Li if not ssh_success_logged: 884*9c5db199SXin Li logging.debug('Successfully pinged host %s', 885*9c5db199SXin Li self.host_port) 886*9c5db199SXin Li wait_procs = self.get_wait_up_processes() 887*9c5db199SXin Li if wait_procs: 888*9c5db199SXin Li logging.debug('Waiting for processes: %s', wait_procs) 889*9c5db199SXin Li else: 890*9c5db199SXin Li logging.debug('No wait_up processes to wait for') 891*9c5db199SXin Li ssh_success_logged = True 892*9c5db199SXin Li try: 893*9c5db199SXin Li if self.are_wait_up_processes_up(): 894*9c5db199SXin Li logging.debug('Host %s is now up', self.host_port) 895*9c5db199SXin Li return True 896*9c5db199SXin Li except error.AutoservError as e: 897*9c5db199SXin Li if not autoserv_error_logged: 898*9c5db199SXin Li logging.debug('Ignoring failure to reach %s: %s %s', 899*9c5db199SXin Li self.host_port, e, 900*9c5db199SXin Li '(and further similar failures)') 901*9c5db199SXin Li autoserv_error_logged = True 902*9c5db199SXin Li time.sleep(1) 903*9c5db199SXin Li current_time = int(time.time()) 904*9c5db199SXin Li 905*9c5db199SXin Li logging.debug('Host %s is still down after waiting %d seconds', 906*9c5db199SXin Li self.host_port, int(timeout + time.time() - end_time)) 907*9c5db199SXin Li return False 908*9c5db199SXin Li 909*9c5db199SXin Li 910*9c5db199SXin Li def wait_down(self, timeout=_DEFAULT_WAIT_DOWN_TIME_SECONDS, 911*9c5db199SXin Li warning_timer=None, old_boot_id=None, 912*9c5db199SXin Li max_ping_timeout=_DEFAULT_MAX_PING_TIMEOUT): 913*9c5db199SXin Li """ 914*9c5db199SXin Li Wait until the remote host is down or the timeout expires. 915*9c5db199SXin Li 916*9c5db199SXin Li If old_boot_id is provided, waits until either the machine is 917*9c5db199SXin Li unpingable or self.get_boot_id() returns a value different from 918*9c5db199SXin Li old_boot_id. If the boot_id value has changed then the function 919*9c5db199SXin Li returns True under the assumption that the machine has shut down 920*9c5db199SXin Li and has now already come back up. 921*9c5db199SXin Li 922*9c5db199SXin Li If old_boot_id is None then until the machine becomes unreachable the 923*9c5db199SXin Li method assumes the machine has not yet shut down. 924*9c5db199SXin Li 925*9c5db199SXin Li @param timeout Time limit in seconds before returning even if the host 926*9c5db199SXin Li is still up. 927*9c5db199SXin Li @param warning_timer Time limit in seconds that will generate a warning 928*9c5db199SXin Li if the host is not down yet. Can be None for no warning. 929*9c5db199SXin Li @param old_boot_id A string containing the result of self.get_boot_id() 930*9c5db199SXin Li prior to the host being told to shut down. Can be None if this is 931*9c5db199SXin Li not available. 932*9c5db199SXin Li @param max_ping_timeout Maximum timeout in seconds for each 933*9c5db199SXin Li self.get_boot_id() call. If this timeout is hit, it is assumed that 934*9c5db199SXin Li the host went down and became unreachable. 935*9c5db199SXin Li 936*9c5db199SXin Li @returns True if the host was found to be down (max_ping_timeout timeout 937*9c5db199SXin Li expired or boot_id changed if provided) and False if timeout 938*9c5db199SXin Li expired. 939*9c5db199SXin Li """ 940*9c5db199SXin Li #TODO: there is currently no way to distinguish between knowing 941*9c5db199SXin Li #TODO: boot_id was unsupported and not knowing the boot_id. 942*9c5db199SXin Li current_time = int(time.time()) 943*9c5db199SXin Li end_time = current_time + timeout 944*9c5db199SXin Li 945*9c5db199SXin Li if warning_timer: 946*9c5db199SXin Li warn_time = current_time + warning_timer 947*9c5db199SXin Li 948*9c5db199SXin Li if old_boot_id is not None: 949*9c5db199SXin Li logging.debug('Host %s pre-shutdown boot_id is %s', 950*9c5db199SXin Li self.host_port, old_boot_id) 951*9c5db199SXin Li 952*9c5db199SXin Li # Impose semi real-time deadline constraints, since some clients 953*9c5db199SXin Li # (eg: watchdog timer tests) expect strict checking of time elapsed. 954*9c5db199SXin Li # Each iteration of this loop is treated as though it atomically 955*9c5db199SXin Li # completes within current_time, this is needed because if we used 956*9c5db199SXin Li # inline time.time() calls instead then the following could happen: 957*9c5db199SXin Li # 958*9c5db199SXin Li # while time.time() < end_time: [23 < 30] 959*9c5db199SXin Li # some code. [takes 10 secs] 960*9c5db199SXin Li # try: 961*9c5db199SXin Li # new_boot_id = self.get_boot_id(timeout=end_time - time.time()) 962*9c5db199SXin Li # [30 - 33] 963*9c5db199SXin Li # The last step will lead to a return True, when in fact the machine 964*9c5db199SXin Li # went down at 32 seconds (>30). Hence we need to pass get_boot_id 965*9c5db199SXin Li # the same time that allowed us into that iteration of the loop. 966*9c5db199SXin Li while current_time < end_time: 967*9c5db199SXin Li ping_timeout = min(end_time - current_time, max_ping_timeout) 968*9c5db199SXin Li try: 969*9c5db199SXin Li new_boot_id = self.get_boot_id(timeout=ping_timeout) 970*9c5db199SXin Li except error.AutoservError: 971*9c5db199SXin Li logging.debug('Host %s is now unreachable over ssh, is down', 972*9c5db199SXin Li self.host_port) 973*9c5db199SXin Li return True 974*9c5db199SXin Li else: 975*9c5db199SXin Li # if the machine is up but the boot_id value has changed from 976*9c5db199SXin Li # old boot id, then we can assume the machine has gone down 977*9c5db199SXin Li # and then already come back up 978*9c5db199SXin Li if old_boot_id is not None and old_boot_id != new_boot_id: 979*9c5db199SXin Li logging.debug('Host %s now has boot_id %s and so must ' 980*9c5db199SXin Li 'have rebooted', self.host_port, new_boot_id) 981*9c5db199SXin Li return True 982*9c5db199SXin Li 983*9c5db199SXin Li if warning_timer and current_time > warn_time: 984*9c5db199SXin Li self.record("INFO", None, "shutdown", 985*9c5db199SXin Li "Shutdown took longer than %ds" % warning_timer) 986*9c5db199SXin Li # Print the warning only once. 987*9c5db199SXin Li warning_timer = None 988*9c5db199SXin Li # If a machine is stuck switching runlevels 989*9c5db199SXin Li # This may cause the machine to reboot. 990*9c5db199SXin Li self.run('kill -HUP 1', ignore_status=True) 991*9c5db199SXin Li 992*9c5db199SXin Li time.sleep(1) 993*9c5db199SXin Li current_time = int(time.time()) 994*9c5db199SXin Li 995*9c5db199SXin Li return False 996*9c5db199SXin Li 997*9c5db199SXin Li 998*9c5db199SXin Li # tunable constants for the verify & repair code 999*9c5db199SXin Li AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER", 1000*9c5db199SXin Li "gb_diskspace_required", 1001*9c5db199SXin Li type=float, 1002*9c5db199SXin Li default=20.0) 1003*9c5db199SXin Li 1004*9c5db199SXin Li 1005*9c5db199SXin Li def verify_connectivity(self): 1006*9c5db199SXin Li super(AbstractSSHHost, self).verify_connectivity() 1007*9c5db199SXin Li 1008*9c5db199SXin Li logging.info('Pinging host %s', self.host_port) 1009*9c5db199SXin Li self.ssh_ping() 1010*9c5db199SXin Li logging.info("Host (ssh) %s is alive", self.host_port) 1011*9c5db199SXin Li 1012*9c5db199SXin Li if self.is_shutting_down(): 1013*9c5db199SXin Li raise error.AutoservHostIsShuttingDownError("Host is shutting down") 1014*9c5db199SXin Li 1015*9c5db199SXin Li 1016*9c5db199SXin Li def verify_software(self): 1017*9c5db199SXin Li super(AbstractSSHHost, self).verify_software() 1018*9c5db199SXin Li try: 1019*9c5db199SXin Li self.check_diskspace(autotest.Autotest.get_install_dir(self), 1020*9c5db199SXin Li self.AUTOTEST_GB_DISKSPACE_REQUIRED) 1021*9c5db199SXin Li except error.AutoservDiskFullHostError: 1022*9c5db199SXin Li # only want to raise if it's a space issue 1023*9c5db199SXin Li raise 1024*9c5db199SXin Li except (error.AutoservHostError, autotest.AutodirNotFoundError): 1025*9c5db199SXin Li logging.exception('autodir space check exception, this is probably ' 1026*9c5db199SXin Li 'safe to ignore\n') 1027*9c5db199SXin Li 1028*9c5db199SXin Li def close(self): 1029*9c5db199SXin Li super(AbstractSSHHost, self).close() 1030*9c5db199SXin Li self.rpc_server_tracker.disconnect_all() 1031*9c5db199SXin Li if not self._connection_pool: 1032*9c5db199SXin Li self._main_ssh.close() 1033*9c5db199SXin Li if os.path.exists(self.known_hosts_file): 1034*9c5db199SXin Li os.remove(self.known_hosts_file) 1035*9c5db199SXin Li self.tls_exec_dut_command = None 1036*9c5db199SXin Li 1037*9c5db199SXin Li def close_main_ssh(self): 1038*9c5db199SXin Li """Stop the ssh main connection. 1039*9c5db199SXin Li 1040*9c5db199SXin Li Intended for situations when the host is known to be down and we don't 1041*9c5db199SXin Li need a ssh timeout to tell us it is down. For example, if you just 1042*9c5db199SXin Li instructed the host to shutdown or hibernate. 1043*9c5db199SXin Li """ 1044*9c5db199SXin Li logging.debug("Stopping main ssh connection") 1045*9c5db199SXin Li self._main_ssh.close() 1046*9c5db199SXin Li 1047*9c5db199SXin Li def restart_main_ssh(self): 1048*9c5db199SXin Li """ 1049*9c5db199SXin Li Stop and restart the ssh main connection. This is meant as a last 1050*9c5db199SXin Li resort when ssh commands fail and we don't understand why. 1051*9c5db199SXin Li """ 1052*9c5db199SXin Li logging.debug("Restarting main ssh connection") 1053*9c5db199SXin Li self._main_ssh.close() 1054*9c5db199SXin Li self._main_ssh.maybe_start(timeout=30) 1055*9c5db199SXin Li 1056*9c5db199SXin Li def start_main_ssh(self, timeout=DEFAULT_START_MAIN_SSH_TIMEOUT_S): 1057*9c5db199SXin Li """ 1058*9c5db199SXin Li Called whenever a non-main SSH connection needs to be initiated (e.g., 1059*9c5db199SXin Li by run, rsync, scp). If main SSH support is enabled and a main SSH 1060*9c5db199SXin Li connection is not active already, start a new one in the background. 1061*9c5db199SXin Li Also, cleanup any zombie main SSH connections (e.g., dead due to 1062*9c5db199SXin Li reboot). 1063*9c5db199SXin Li 1064*9c5db199SXin Li timeout: timeout in seconds (default 5) to wait for main ssh 1065*9c5db199SXin Li connection to be established. If timeout is reached, a 1066*9c5db199SXin Li warning message is logged, but no other action is taken. 1067*9c5db199SXin Li """ 1068*9c5db199SXin Li if not enable_main_ssh: 1069*9c5db199SXin Li return 1070*9c5db199SXin Li self._main_ssh.maybe_start(timeout=timeout) 1071*9c5db199SXin Li 1072*9c5db199SXin Li @property 1073*9c5db199SXin Li def tls_unstable(self): 1074*9c5db199SXin Li # A single test will rebuild remote many times. Its safe to assume if 1075*9c5db199SXin Li # TLS unstable for one try, it will be for others. If we check each, 1076*9c5db199SXin Li # it adds ~60 seconds per test (if its dead). 1077*9c5db199SXin Li if os.getenv('TLS_UNSTABLE'): 1078*9c5db199SXin Li return bool(os.getenv('TLS_UNSTABLE')) 1079*9c5db199SXin Li if self._tls_unstable is not None: 1080*9c5db199SXin Li return self._tls_unstable 1081*9c5db199SXin Li 1082*9c5db199SXin Li @tls_unstable.setter 1083*9c5db199SXin Li def tls_unstable(self, v): 1084*9c5db199SXin Li if not isinstance(v, bool): 1085*9c5db199SXin Li raise error.AutoservError('tls_stable setting must be bool, got %s' 1086*9c5db199SXin Li % (type(v))) 1087*9c5db199SXin Li os.environ['TLS_UNSTABLE'] = str(v) 1088*9c5db199SXin Li self._tls_unstable = v 1089*9c5db199SXin Li 1090*9c5db199SXin Li @property 1091*9c5db199SXin Li def tls_exec_dut_command_client(self): 1092*9c5db199SXin Li # If client is already initialized, return that. 1093*9c5db199SXin Li if not ENABLE_EXEC_DUT_COMMAND: 1094*9c5db199SXin Li return None 1095*9c5db199SXin Li if self.tls_unstable: 1096*9c5db199SXin Li return None 1097*9c5db199SXin Li if self._tls_exec_dut_command_client is not None: 1098*9c5db199SXin Li return self._tls_exec_dut_command_client 1099*9c5db199SXin Li # If the TLS connection is alive, create a new client. 1100*9c5db199SXin Li if self.tls_connection is None: 1101*9c5db199SXin Li return None 1102*9c5db199SXin Li return exec_dut_command.TLSExecDutCommandClient( 1103*9c5db199SXin Li tlsconnection=self.tls_connection, 1104*9c5db199SXin Li hostname=self.hostname) 1105*9c5db199SXin Li 1106*9c5db199SXin Li def clear_known_hosts(self): 1107*9c5db199SXin Li """Clears out the temporary ssh known_hosts file. 1108*9c5db199SXin Li 1109*9c5db199SXin Li This is useful if the test SSHes to the machine, then reinstalls it, 1110*9c5db199SXin Li then SSHes to it again. It can be called after the reinstall to 1111*9c5db199SXin Li reduce the spam in the logs. 1112*9c5db199SXin Li """ 1113*9c5db199SXin Li logging.info("Clearing known hosts for host '%s', file '%s'.", 1114*9c5db199SXin Li self.host_port, self.known_hosts_file) 1115*9c5db199SXin Li # Clear out the file by opening it for writing and then closing. 1116*9c5db199SXin Li fh = open(self.known_hosts_file, "w") 1117*9c5db199SXin Li fh.close() 1118*9c5db199SXin Li 1119*9c5db199SXin Li 1120*9c5db199SXin Li def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True): 1121*9c5db199SXin Li """Copy log directories from a host to a local directory. 1122*9c5db199SXin Li 1123*9c5db199SXin Li @param remote_src_dir: A destination directory on the host. 1124*9c5db199SXin Li @param local_dest_dir: A path to a local destination directory. 1125*9c5db199SXin Li If it doesn't exist it will be created. 1126*9c5db199SXin Li @param ignore_errors: If True, ignore exceptions. 1127*9c5db199SXin Li 1128*9c5db199SXin Li @raises OSError: If there were problems creating the local_dest_dir and 1129*9c5db199SXin Li ignore_errors is False. 1130*9c5db199SXin Li @raises AutoservRunError, AutotestRunError: If something goes wrong 1131*9c5db199SXin Li while copying the directories and ignore_errors is False. 1132*9c5db199SXin Li """ 1133*9c5db199SXin Li if not self.check_cached_up_status(): 1134*9c5db199SXin Li logging.warning('Host %s did not answer to ping, skip collecting ' 1135*9c5db199SXin Li 'logs.', self.host_port) 1136*9c5db199SXin Li return 1137*9c5db199SXin Li 1138*9c5db199SXin Li locally_created_dest = False 1139*9c5db199SXin Li if (not os.path.exists(local_dest_dir) 1140*9c5db199SXin Li or not os.path.isdir(local_dest_dir)): 1141*9c5db199SXin Li try: 1142*9c5db199SXin Li os.makedirs(local_dest_dir) 1143*9c5db199SXin Li locally_created_dest = True 1144*9c5db199SXin Li except OSError as e: 1145*9c5db199SXin Li logging.warning('Unable to collect logs from host ' 1146*9c5db199SXin Li '%s: %s', self.host_port, e) 1147*9c5db199SXin Li if not ignore_errors: 1148*9c5db199SXin Li raise 1149*9c5db199SXin Li return 1150*9c5db199SXin Li 1151*9c5db199SXin Li # Build test result directory summary 1152*9c5db199SXin Li try: 1153*9c5db199SXin Li result_tools_runner.run_on_client(self, remote_src_dir) 1154*9c5db199SXin Li except (error.AutotestRunError, error.AutoservRunError, 1155*9c5db199SXin Li error.AutoservSSHTimeout) as e: 1156*9c5db199SXin Li logging.exception( 1157*9c5db199SXin Li 'Non-critical failure: Failed to collect and throttle ' 1158*9c5db199SXin Li 'results at %s from host %s', remote_src_dir, 1159*9c5db199SXin Li self.host_port) 1160*9c5db199SXin Li 1161*9c5db199SXin Li try: 1162*9c5db199SXin Li self.get_file(remote_src_dir, local_dest_dir, safe_symlinks=True) 1163*9c5db199SXin Li except (error.AutotestRunError, error.AutoservRunError, 1164*9c5db199SXin Li error.AutoservSSHTimeout) as e: 1165*9c5db199SXin Li logging.warning('Collection of %s to local dir %s from host %s ' 1166*9c5db199SXin Li 'failed: %s', remote_src_dir, local_dest_dir, 1167*9c5db199SXin Li self.host_port, e) 1168*9c5db199SXin Li if locally_created_dest: 1169*9c5db199SXin Li shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors) 1170*9c5db199SXin Li if not ignore_errors: 1171*9c5db199SXin Li raise 1172*9c5db199SXin Li 1173*9c5db199SXin Li # Clean up directory summary file on the client side. 1174*9c5db199SXin Li try: 1175*9c5db199SXin Li result_tools_runner.run_on_client(self, remote_src_dir, 1176*9c5db199SXin Li cleanup_only=True) 1177*9c5db199SXin Li except (error.AutotestRunError, error.AutoservRunError, 1178*9c5db199SXin Li error.AutoservSSHTimeout) as e: 1179*9c5db199SXin Li logging.exception( 1180*9c5db199SXin Li 'Non-critical failure: Failed to cleanup result summary ' 1181*9c5db199SXin Li 'files at %s in host %s', remote_src_dir, self.hostname) 1182*9c5db199SXin Li 1183*9c5db199SXin Li 1184*9c5db199SXin Li def create_ssh_tunnel(self, port, local_port): 1185*9c5db199SXin Li """Create an ssh tunnel from local_port to port. 1186*9c5db199SXin Li 1187*9c5db199SXin Li This is used to forward a port securely through a tunnel process from 1188*9c5db199SXin Li the server to the DUT for RPC server connection. 1189*9c5db199SXin Li 1190*9c5db199SXin Li @param port: remote port on the host. 1191*9c5db199SXin Li @param local_port: local forwarding port. 1192*9c5db199SXin Li 1193*9c5db199SXin Li @return: the tunnel process. 1194*9c5db199SXin Li """ 1195*9c5db199SXin Li tunnel_options = '-n -N -q -L %d:localhost:%d' % (local_port, port) 1196*9c5db199SXin Li ssh_cmd = self.make_ssh_command(opts=tunnel_options, port=self.port) 1197*9c5db199SXin Li tunnel_cmd = '%s %s' % (ssh_cmd, self.hostname) 1198*9c5db199SXin Li logging.debug('Full tunnel command: %s', tunnel_cmd) 1199*9c5db199SXin Li # Exec the ssh process directly here rather than using a shell. 1200*9c5db199SXin Li # Using a shell leaves a dangling ssh process, because we deliver 1201*9c5db199SXin Li # signals to the shell wrapping ssh, not the ssh process itself. 1202*9c5db199SXin Li args = shlex.split(tunnel_cmd) 1203*9c5db199SXin Li with open('/dev/null', 'w') as devnull: 1204*9c5db199SXin Li tunnel_proc = subprocess.Popen(args, stdout=devnull, stderr=devnull, 1205*9c5db199SXin Li close_fds=True) 1206*9c5db199SXin Li logging.debug('Started ssh tunnel, local = %d' 1207*9c5db199SXin Li ' remote = %d, pid = %d', 1208*9c5db199SXin Li local_port, port, tunnel_proc.pid) 1209*9c5db199SXin Li return tunnel_proc 1210*9c5db199SXin Li 1211*9c5db199SXin Li 1212*9c5db199SXin Li def disconnect_ssh_tunnel(self, tunnel_proc): 1213*9c5db199SXin Li """ 1214*9c5db199SXin Li Disconnects a previously forwarded port from the server to the DUT for 1215*9c5db199SXin Li RPC server connection. 1216*9c5db199SXin Li 1217*9c5db199SXin Li @param tunnel_proc: a tunnel process returned from |create_ssh_tunnel|. 1218*9c5db199SXin Li """ 1219*9c5db199SXin Li if tunnel_proc.poll() is None: 1220*9c5db199SXin Li tunnel_proc.terminate() 1221*9c5db199SXin Li logging.debug('Terminated tunnel, pid %d', tunnel_proc.pid) 1222*9c5db199SXin Li else: 1223*9c5db199SXin Li logging.debug('Tunnel pid %d terminated early, status %d', 1224*9c5db199SXin Li tunnel_proc.pid, tunnel_proc.returncode) 1225*9c5db199SXin Li 1226*9c5db199SXin Li 1227*9c5db199SXin Li def get_os_type(self): 1228*9c5db199SXin Li """Returns the host OS descriptor (to be implemented in subclasses). 1229*9c5db199SXin Li 1230*9c5db199SXin Li @return A string describing the OS type. 1231*9c5db199SXin Li """ 1232*9c5db199SXin Li raise NotImplementedError 1233*9c5db199SXin Li 1234*9c5db199SXin Li 1235*9c5db199SXin Li def check_cached_up_status( 1236*9c5db199SXin Li self, expiration_seconds=_DEFAULT_UP_STATUS_EXPIRATION_SECONDS): 1237*9c5db199SXin Li """Check if the DUT responded to ping in the past `expiration_seconds`. 1238*9c5db199SXin Li 1239*9c5db199SXin Li @param expiration_seconds: The number of seconds to keep the cached 1240*9c5db199SXin Li status of whether the DUT responded to ping. 1241*9c5db199SXin Li @return: True if the DUT has responded to ping during the past 1242*9c5db199SXin Li `expiration_seconds`. 1243*9c5db199SXin Li """ 1244*9c5db199SXin Li # Refresh the up status if any of following conditions is true: 1245*9c5db199SXin Li # * cached status is never set 1246*9c5db199SXin Li # * cached status is False, so the method can check if the host is up 1247*9c5db199SXin Li # again. 1248*9c5db199SXin Li # * If the cached status is older than `expiration_seconds` 1249*9c5db199SXin Li # If we have icmp disabled, treat that as a cached ping. 1250*9c5db199SXin Li if not self._use_icmp: 1251*9c5db199SXin Li return True 1252*9c5db199SXin Li expire_time = time.time() - expiration_seconds 1253*9c5db199SXin Li if (self._cached_up_status_updated is None or 1254*9c5db199SXin Li not self._cached_up_status or 1255*9c5db199SXin Li self._cached_up_status_updated < expire_time): 1256*9c5db199SXin Li self._cached_up_status = self.is_up_fast() 1257*9c5db199SXin Li self._cached_up_status_updated = time.time() 1258*9c5db199SXin Li return self._cached_up_status 1259*9c5db199SXin Li 1260*9c5db199SXin Li 1261*9c5db199SXin Li def _track_class_usage(self): 1262*9c5db199SXin Li """Tracking which class was used. 1263*9c5db199SXin Li 1264*9c5db199SXin Li The idea to identify unused classes to be able clean them up. 1265*9c5db199SXin Li We skip names with dynamic created classes where the name is 1266*9c5db199SXin Li hostname of the device. 1267*9c5db199SXin Li """ 1268*9c5db199SXin Li class_name = None 1269*9c5db199SXin Li if 'chrome' not in self.__class__.__name__: 1270*9c5db199SXin Li class_name = self.__class__.__name__ 1271*9c5db199SXin Li else: 1272*9c5db199SXin Li for base in self.__class__.__bases__: 1273*9c5db199SXin Li if 'chrome' not in base.__name__: 1274*9c5db199SXin Li class_name = base.__name__ 1275*9c5db199SXin Li break 1276*9c5db199SXin Li if class_name: 1277*9c5db199SXin Li data = {'host_class': class_name} 1278*9c5db199SXin Li metrics.Counter( 1279*9c5db199SXin Li 'chromeos/autotest/used_hosts').increment(fields=data) 1280*9c5db199SXin Li 1281*9c5db199SXin Li def is_file_exists(self, file_path): 1282*9c5db199SXin Li """Check whether a given file is exist on the host. 1283*9c5db199SXin Li """ 1284*9c5db199SXin Li result = self.run('test -f ' + file_path, 1285*9c5db199SXin Li timeout=30, 1286*9c5db199SXin Li ignore_status=True) 1287*9c5db199SXin Li return result.exit_status == 0 1288*9c5db199SXin Li 1289*9c5db199SXin Li 1290*9c5db199SXin Lidef _client_symlink(sources): 1291*9c5db199SXin Li """Return the client symlink if in sources.""" 1292*9c5db199SXin Li for source in sources: 1293*9c5db199SXin Li if source.endswith(AUTOTEST_CLIENT_SYMLINK_END): 1294*9c5db199SXin Li return source 1295*9c5db199SXin Li return None 1296