blob: be281e85c63bc7bceb45555879bd16c8f1c726bf [file] [log] [blame]
Marc Herbert21eb6492015-11-13 15:48:53 -08001import os, time, socket, shutil, glob, logging, traceback, tempfile, re
xixuan6cf6d2f2016-01-29 15:29:00 -08002import shlex
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +08003import subprocess
4
Simran Basi3b858a22015-03-17 16:23:24 -07005from multiprocessing import Lock
Aviv Keshet53a216a2013-08-27 13:58:46 -07006from autotest_lib.client.common_lib import autotemp, error
jadmanski31c49b72008-10-27 20:44:48 +00007from autotest_lib.server import utils, autotest
Prathmesh Prabhu8b5065d2017-01-10 17:13:01 -08008from autotest_lib.server.hosts import host_info
mblighe8b93af2009-01-30 00:45:53 +00009from autotest_lib.server.hosts import remote
Roshan Pius58e5dd32015-10-16 15:16:42 -070010from autotest_lib.server.hosts import rpc_server_tracker
mblighefccc1b2010-01-11 19:08:42 +000011from autotest_lib.client.common_lib.global_config import global_config
jadmanskica7da372008-10-21 16:26:52 +000012
Gwendal Grignou36b61702016-02-10 11:57:53 -080013# pylint: disable=C0111
jadmanskica7da372008-10-21 16:26:52 +000014
mblighb86bfa12010-02-12 20:22:21 +000015get_value = global_config.get_config_value
16enable_master_ssh = get_value('AUTOSERV', 'enable_master_ssh', type=bool,
17 default=False)
mblighefccc1b2010-01-11 19:08:42 +000018
19
Fang Deng96667ca2013-08-01 17:46:18 -070020class AbstractSSHHost(remote.RemoteHost):
mblighbc9402b2009-12-29 01:15:34 +000021 """
22 This class represents a generic implementation of most of the
jadmanskica7da372008-10-21 16:26:52 +000023 framework necessary for controlling a host via ssh. It implements
24 almost all of the abstract Host methods, except for the core
mblighbc9402b2009-12-29 01:15:34 +000025 Host.run method.
26 """
Simran Basi5ace6f22016-01-06 17:30:44 -080027 VERSION_PREFIX = ''
jadmanskica7da372008-10-21 16:26:52 +000028
jadmanskif6562912008-10-21 17:59:01 +000029 def _initialize(self, hostname, user="root", port=22, password="",
Kevin Cheng05ae2a42016-06-06 10:12:48 -070030 is_client_install_supported=True, afe_host=None,
Prathmesh Prabhu8b5065d2017-01-10 17:13:01 -080031 host_info_store=None, *args, **dargs):
jadmanskif6562912008-10-21 17:59:01 +000032 super(AbstractSSHHost, self)._initialize(hostname=hostname,
33 *args, **dargs)
Kevin Cheng05ae2a42016-06-06 10:12:48 -070034 """
35 @param hostname: The hostname of the host.
36 @param user: The username to use when ssh'ing into the host.
37 @param password: The password to use when ssh'ing into the host.
38 @param port: The port to use for ssh.
39 @param is_client_install_supported: Boolean to indicate if we can
40 install autotest on the host.
41 @param afe_host: The host object attained from the AFE (get_hosts).
Prathmesh Prabhu8b5065d2017-01-10 17:13:01 -080042 @param host_info_store: Optional host_info.CachingHostInfoStore object
43 to obtain / update host information.
Kevin Cheng05ae2a42016-06-06 10:12:48 -070044 """
Dan Shic07b8932014-12-11 15:22:30 -080045 # IP address is retrieved only on demand. Otherwise the host
46 # initialization will fail for host is not online.
47 self._ip = None
jadmanskica7da372008-10-21 16:26:52 +000048 self.user = user
49 self.port = port
50 self.password = password
Roshan Piusa58163a2015-10-14 13:36:29 -070051 self._is_client_install_supported = is_client_install_supported
showard6eafb492010-01-15 20:29:06 +000052 self._use_rsync = None
Fang Deng3af66202013-08-16 15:19:25 -070053 self.known_hosts_file = tempfile.mkstemp()[1]
Roshan Pius58e5dd32015-10-16 15:16:42 -070054 self._rpc_server_tracker = rpc_server_tracker.RpcServerTracker(self);
jadmanskica7da372008-10-21 16:26:52 +000055
mblighefccc1b2010-01-11 19:08:42 +000056 """
57 Master SSH connection background job, socket temp directory and socket
58 control path option. If master-SSH is enabled, these fields will be
59 initialized by start_master_ssh when a new SSH connection is initiated.
60 """
61 self.master_ssh_job = None
62 self.master_ssh_tempdir = None
63 self.master_ssh_option = ''
64
Simran Basi3b858a22015-03-17 16:23:24 -070065 # Create a Lock to protect against race conditions.
66 self._lock = Lock()
67
Kevin Cheng05ae2a42016-06-06 10:12:48 -070068 self._afe_host = afe_host or utils.EmptyAFEHost()
Prathmesh Prabhu8b5065d2017-01-10 17:13:01 -080069 self.host_info_store = (host_info_store or
70 host_info.InMemoryHostInfoStore())
showard6eafb492010-01-15 20:29:06 +000071
Dan Shic07b8932014-12-11 15:22:30 -080072 @property
73 def ip(self):
74 """@return IP address of the host.
75 """
76 if not self._ip:
77 self._ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
78 return self._ip
79
80
Roshan Piusa58163a2015-10-14 13:36:29 -070081 @property
82 def is_client_install_supported(self):
83 """"
84 Returns True if the host supports autotest client installs, False
85 otherwise.
86 """
87 return self._is_client_install_supported
88
89
Roshan Pius58e5dd32015-10-16 15:16:42 -070090 @property
91 def rpc_server_tracker(self):
92 """"
93 @return The RPC server tracker associated with this host.
94 """
95 return self._rpc_server_tracker
96
97
Fang Deng96667ca2013-08-01 17:46:18 -070098 def make_ssh_command(self, user="root", port=22, opts='',
99 hosts_file='/dev/null',
100 connect_timeout=30, alive_interval=300):
101 base_command = ("/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no "
102 "-o UserKnownHostsFile=%s -o BatchMode=yes "
103 "-o ConnectTimeout=%d -o ServerAliveInterval=%d "
104 "-l %s -p %d")
105 assert isinstance(connect_timeout, (int, long))
106 assert connect_timeout > 0 # can't disable the timeout
107 return base_command % (opts, hosts_file, connect_timeout,
108 alive_interval, user, port)
109
110
showard6eafb492010-01-15 20:29:06 +0000111 def use_rsync(self):
112 if self._use_rsync is not None:
113 return self._use_rsync
114
mblighc9892c02010-01-06 19:02:16 +0000115 # Check if rsync is available on the remote host. If it's not,
116 # don't try to use it for any future file transfers.
Gwendal Grignou03286f02017-03-24 10:50:59 -0700117 self._use_rsync = self.check_rsync()
showard6eafb492010-01-15 20:29:06 +0000118 if not self._use_rsync:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700119 logging.warning("rsync not available on remote host %s -- disabled",
mblighc9892c02010-01-06 19:02:16 +0000120 self.hostname)
Eric Lie0493a42010-11-15 13:05:43 -0800121 return self._use_rsync
mblighc9892c02010-01-06 19:02:16 +0000122
123
Gwendal Grignou03286f02017-03-24 10:50:59 -0700124 def check_rsync(self):
mblighc9892c02010-01-06 19:02:16 +0000125 """
126 Check if rsync is available on the remote host.
127 """
128 try:
129 self.run("rsync --version", stdout_tee=None, stderr_tee=None)
130 except error.AutoservRunError:
131 return False
132 return True
133
jadmanskica7da372008-10-21 16:26:52 +0000134
Gwendal Grignou36b61702016-02-10 11:57:53 -0800135 def _encode_remote_paths(self, paths, escape=True, use_scp=False):
mblighbc9402b2009-12-29 01:15:34 +0000136 """
137 Given a list of file paths, encodes it as a single remote path, in
138 the style used by rsync and scp.
Gwendal Grignou36b61702016-02-10 11:57:53 -0800139 escape: add \\ to protect special characters.
140 use_scp: encode for scp if true, rsync if false.
mblighbc9402b2009-12-29 01:15:34 +0000141 """
showard56176ec2009-10-28 19:52:30 +0000142 if escape:
143 paths = [utils.scp_remote_escape(path) for path in paths]
Marc Herbert21eb6492015-11-13 15:48:53 -0800144
145 remote = self.hostname
146
147 # rsync and scp require IPv6 brackets, even when there isn't any
148 # trailing port number (ssh doesn't support IPv6 brackets).
149 # In the Python >= 3.3 future, 'import ipaddress' will parse addresses.
150 if re.search(r':.*:', remote):
151 remote = '[%s]' % remote
152
Gwendal Grignou36b61702016-02-10 11:57:53 -0800153 if use_scp:
154 return '%s@%s:"%s"' % (self.user, remote, " ".join(paths))
155 else:
156 return '%s@%s:%s' % (
157 self.user, remote,
158 " :".join('"%s"' % p for p in paths))
jadmanskica7da372008-10-21 16:26:52 +0000159
Gwendal Grignou36b61702016-02-10 11:57:53 -0800160 def _encode_local_paths(self, paths, escape=True):
161 """
162 Given a list of file paths, encodes it as a single local path.
163 escape: add \\ to protect special characters.
164 """
165 if escape:
166 paths = [utils.sh_escape(path) for path in paths]
167
168 return " ".join('"%s"' % p for p in paths)
jadmanskica7da372008-10-21 16:26:52 +0000169
Luigi Semenzato9b083072016-12-19 16:50:40 -0800170 def _make_rsync_cmd(self, sources, dest, delete_dest,
171 preserve_symlinks, safe_symlinks):
mblighbc9402b2009-12-29 01:15:34 +0000172 """
Gwendal Grignou36b61702016-02-10 11:57:53 -0800173 Given a string of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +0000174 appropriate rsync command for copying them. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +0000175 pre-encoded.
176 """
Fang Deng96667ca2013-08-01 17:46:18 -0700177 ssh_cmd = self.make_ssh_command(user=self.user, port=self.port,
178 opts=self.master_ssh_option,
179 hosts_file=self.known_hosts_file)
jadmanskid7b79ed2009-01-07 17:19:48 +0000180 if delete_dest:
181 delete_flag = "--delete"
182 else:
183 delete_flag = ""
Luigi Semenzato9b083072016-12-19 16:50:40 -0800184 if safe_symlinks:
185 symlink_flag = "-l --safe-links"
186 elif preserve_symlinks:
187 symlink_flag = "-l"
mbligh45561782009-05-11 21:14:34 +0000188 else:
189 symlink_flag = "-L"
Dan Shi06d7fbf2014-02-12 12:34:41 -0800190 command = ("rsync %s %s --timeout=1800 --rsh='%s' -az --no-o --no-g "
David Hendricksb8904182014-06-02 15:22:49 -0700191 "%s \"%s\"")
Gwendal Grignou36b61702016-02-10 11:57:53 -0800192 return command % (symlink_flag, delete_flag, ssh_cmd, sources, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000193
194
Eric Li861b2d52011-02-04 14:50:35 -0800195 def _make_ssh_cmd(self, cmd):
196 """
197 Create a base ssh command string for the host which can be used
198 to run commands directly on the machine
199 """
Fang Deng96667ca2013-08-01 17:46:18 -0700200 base_cmd = self.make_ssh_command(user=self.user, port=self.port,
201 opts=self.master_ssh_option,
202 hosts_file=self.known_hosts_file)
Eric Li861b2d52011-02-04 14:50:35 -0800203
204 return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd))
205
jadmanskid7b79ed2009-01-07 17:19:48 +0000206 def _make_scp_cmd(self, sources, dest):
mblighbc9402b2009-12-29 01:15:34 +0000207 """
Gwendal Grignou36b61702016-02-10 11:57:53 -0800208 Given a string of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +0000209 appropriate scp command for encoding it. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +0000210 pre-encoded.
211 """
mblighc0649d62010-01-15 18:15:58 +0000212 command = ("scp -rq %s -o StrictHostKeyChecking=no "
lmraf676f32010-02-04 03:36:26 +0000213 "-o UserKnownHostsFile=%s -P %d %s '%s'")
Fang Deng3af66202013-08-16 15:19:25 -0700214 return command % (self.master_ssh_option, self.known_hosts_file,
Gwendal Grignou36b61702016-02-10 11:57:53 -0800215 self.port, sources, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000216
217
218 def _make_rsync_compatible_globs(self, path, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000219 """
220 Given an rsync-style path, returns a list of globbed paths
jadmanskid7b79ed2009-01-07 17:19:48 +0000221 that will hopefully provide equivalent behaviour for scp. Does not
222 support the full range of rsync pattern matching behaviour, only that
223 exposed in the get/send_file interface (trailing slashes).
224
225 The is_local param is flag indicating if the paths should be
mblighbc9402b2009-12-29 01:15:34 +0000226 interpreted as local or remote paths.
227 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000228
229 # non-trailing slash paths should just work
230 if len(path) == 0 or path[-1] != "/":
231 return [path]
232
233 # make a function to test if a pattern matches any files
234 if is_local:
showard56176ec2009-10-28 19:52:30 +0000235 def glob_matches_files(path, pattern):
236 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +0000237 else:
showard56176ec2009-10-28 19:52:30 +0000238 def glob_matches_files(path, pattern):
239 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
240 pattern),
241 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +0000242 return result.exit_status == 0
243
244 # take a set of globs that cover all files, and see which are needed
245 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +0000246 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +0000247
248 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000249 if is_local:
showard56176ec2009-10-28 19:52:30 +0000250 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
251 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000252 else:
showard56176ec2009-10-28 19:52:30 +0000253 return [utils.scp_remote_escape(path) + pattern
254 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000255
256
257 def _make_rsync_compatible_source(self, source, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000258 """
259 Applies the same logic as _make_rsync_compatible_globs, but
jadmanskid7b79ed2009-01-07 17:19:48 +0000260 applies it to an entire list of sources, producing a new list of
mblighbc9402b2009-12-29 01:15:34 +0000261 sources, properly quoted.
262 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000263 return sum((self._make_rsync_compatible_globs(path, is_local)
264 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000265
266
mblighfeac0102009-04-28 18:31:12 +0000267 def _set_umask_perms(self, dest):
mblighbc9402b2009-12-29 01:15:34 +0000268 """
269 Given a destination file/dir (recursively) set the permissions on
270 all the files and directories to the max allowed by running umask.
271 """
mblighfeac0102009-04-28 18:31:12 +0000272
273 # now this looks strange but I haven't found a way in Python to _just_
274 # get the umask, apparently the only option is to try to set it
275 umask = os.umask(0)
276 os.umask(umask)
277
278 max_privs = 0777 & ~umask
279
280 def set_file_privs(filename):
Chris Masone567d0d92011-12-19 09:38:30 -0800281 """Sets mode of |filename|. Assumes |filename| exists."""
282 file_stat = os.stat(filename)
mblighfeac0102009-04-28 18:31:12 +0000283
284 file_privs = max_privs
285 # if the original file permissions do not have at least one
286 # executable bit then do not set it anywhere
287 if not file_stat.st_mode & 0111:
288 file_privs &= ~0111
289
290 os.chmod(filename, file_privs)
291
292 # try a bottom-up walk so changes on directory permissions won't cut
293 # our access to the files/directories inside it
294 for root, dirs, files in os.walk(dest, topdown=False):
295 # when setting the privileges we emulate the chmod "X" behaviour
296 # that sets to execute only if it is a directory or any of the
297 # owner/group/other already has execute right
298 for dirname in dirs:
299 os.chmod(os.path.join(root, dirname), max_privs)
300
Chris Masone567d0d92011-12-19 09:38:30 -0800301 # Filter out broken symlinks as we go.
302 for filename in filter(os.path.exists, files):
mblighfeac0102009-04-28 18:31:12 +0000303 set_file_privs(os.path.join(root, filename))
304
305
306 # now set privs for the dest itself
307 if os.path.isdir(dest):
308 os.chmod(dest, max_privs)
309 else:
310 set_file_privs(dest)
311
312
mbligh45561782009-05-11 21:14:34 +0000313 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
Luigi Semenzato9b083072016-12-19 16:50:40 -0800314 preserve_symlinks=False, retry=True, safe_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000315 """
316 Copy files from the remote host to a local path.
317
318 Directories will be copied recursively.
319 If a source component is a directory with a trailing slash,
320 the content of the directory will be copied, otherwise, the
321 directory itself and its content will be copied. This
322 behavior is similar to that of the program 'rsync'.
323
324 Args:
325 source: either
326 1) a single file or directory, as a string
327 2) a list of one or more (possibly mixed)
328 files or directories
329 dest: a file or a directory (if source contains a
330 directory or more than one element, you must
331 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000332 delete_dest: if this is true, the command will also clear
333 out any old files at dest that are not in the
334 source
mblighfeac0102009-04-28 18:31:12 +0000335 preserve_perm: tells get_file() to try to preserve the sources
336 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000337 preserve_symlinks: try to preserve symlinks instead of
338 transforming them into files/dirs on copy
Luigi Semenzato9b083072016-12-19 16:50:40 -0800339 safe_symlinks: same as preserve_symlinks, but discard links
340 that may point outside the copied tree
jadmanskica7da372008-10-21 16:26:52 +0000341 Raises:
342 AutoservRunError: the scp command failed
343 """
Simran Basi882f15b2013-10-29 14:59:34 -0700344 logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,'
345 'preserve_perm: %s, preserve_symlinks:%s', source, dest,
346 delete_dest, preserve_perm, preserve_symlinks)
mblighefccc1b2010-01-11 19:08:42 +0000347 # Start a master SSH connection if necessary.
348 self.start_master_ssh()
349
jadmanskica7da372008-10-21 16:26:52 +0000350 if isinstance(source, basestring):
351 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000352 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000353
mblighc9892c02010-01-06 19:02:16 +0000354 # If rsync is disabled or fails, try scp.
showard6eafb492010-01-15 20:29:06 +0000355 try_scp = True
356 if self.use_rsync():
Simran Basi882f15b2013-10-29 14:59:34 -0700357 logging.debug('Using Rsync.')
mblighc9892c02010-01-06 19:02:16 +0000358 try:
359 remote_source = self._encode_remote_paths(source)
360 local_dest = utils.sh_escape(dest)
Gwendal Grignou36b61702016-02-10 11:57:53 -0800361 rsync = self._make_rsync_cmd(remote_source, local_dest,
Luigi Semenzato9b083072016-12-19 16:50:40 -0800362 delete_dest, preserve_symlinks,
363 safe_symlinks)
mblighc9892c02010-01-06 19:02:16 +0000364 utils.run(rsync)
showard6eafb492010-01-15 20:29:06 +0000365 try_scp = False
mblighc9892c02010-01-06 19:02:16 +0000366 except error.CmdError, e:
Luigi Semenzato7f9dff12016-11-21 14:01:20 -0800367 # retry on rsync exit values which may be caused by transient
368 # network problems:
369 #
370 # rc 10: Error in socket I/O
371 # rc 12: Error in rsync protocol data stream
372 # rc 23: Partial transfer due to error
373 # rc 255: Ssh error
374 #
375 # Note that rc 23 includes dangling symlinks. In this case
376 # retrying is useless, but not very damaging since rsync checks
377 # for those before starting the transfer (scp does not).
378 status = e.result_obj.exit_status
379 if status in [10, 12, 23, 255] and retry:
380 logging.warning('rsync status %d, retrying', status)
381 self.get_file(source, dest, delete_dest, preserve_perm,
382 preserve_symlinks, retry=False)
383 # The nested get_file() does all that's needed.
384 return
385 else:
386 logging.warning("trying scp, rsync failed: %s (%d)",
387 e, status)
mblighc9892c02010-01-06 19:02:16 +0000388
389 if try_scp:
Simran Basi882f15b2013-10-29 14:59:34 -0700390 logging.debug('Trying scp.')
jadmanskid7b79ed2009-01-07 17:19:48 +0000391 # scp has no equivalent to --delete, just drop the entire dest dir
392 if delete_dest and os.path.isdir(dest):
393 shutil.rmtree(dest)
394 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000395
jadmanskid7b79ed2009-01-07 17:19:48 +0000396 remote_source = self._make_rsync_compatible_source(source, False)
397 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000398 # _make_rsync_compatible_source() already did the escaping
Gwendal Grignou36b61702016-02-10 11:57:53 -0800399 remote_source = self._encode_remote_paths(
400 remote_source, escape=False, use_scp=True)
jadmanskid7b79ed2009-01-07 17:19:48 +0000401 local_dest = utils.sh_escape(dest)
Gwendal Grignou36b61702016-02-10 11:57:53 -0800402 scp = self._make_scp_cmd(remote_source, local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000403 try:
404 utils.run(scp)
405 except error.CmdError, e:
Simran Basi882f15b2013-10-29 14:59:34 -0700406 logging.debug('scp failed: %s', e)
jadmanskid7b79ed2009-01-07 17:19:48 +0000407 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000408
mblighfeac0102009-04-28 18:31:12 +0000409 if not preserve_perm:
410 # we have no way to tell scp to not try to preserve the
411 # permissions so set them after copy instead.
412 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
413 # options are only in very recent rsync versions
414 self._set_umask_perms(dest)
415
jadmanskica7da372008-10-21 16:26:52 +0000416
mbligh45561782009-05-11 21:14:34 +0000417 def send_file(self, source, dest, delete_dest=False,
418 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000419 """
420 Copy files from a local path to the remote host.
421
422 Directories will be copied recursively.
423 If a source component is a directory with a trailing slash,
424 the content of the directory will be copied, otherwise, the
425 directory itself and its content will be copied. This
426 behavior is similar to that of the program 'rsync'.
427
428 Args:
429 source: either
430 1) a single file or directory, as a string
431 2) a list of one or more (possibly mixed)
432 files or directories
433 dest: a file or a directory (if source contains a
434 directory or more than one element, you must
435 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000436 delete_dest: if this is true, the command will also clear
437 out any old files at dest that are not in the
438 source
mbligh45561782009-05-11 21:14:34 +0000439 preserve_symlinks: controls if symlinks on the source will be
440 copied as such on the destination or transformed into the
441 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000442
443 Raises:
444 AutoservRunError: the scp command failed
445 """
Simran Basi882f15b2013-10-29 14:59:34 -0700446 logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,'
447 'preserve_symlinks:%s', source, dest,
448 delete_dest, preserve_symlinks)
mblighefccc1b2010-01-11 19:08:42 +0000449 # Start a master SSH connection if necessary.
450 self.start_master_ssh()
451
jadmanskica7da372008-10-21 16:26:52 +0000452 if isinstance(source, basestring):
453 source = [source]
454
Gwendal Grignou36b61702016-02-10 11:57:53 -0800455 local_sources = self._encode_local_paths(source)
mukesh agrawal0d3616c2015-07-17 15:47:36 -0700456 if not local_sources:
Gwendal Grignou36b61702016-02-10 11:57:53 -0800457 raise error.TestError('source |%s| yielded an empty string' % (
mukesh agrawal0d3616c2015-07-17 15:47:36 -0700458 source))
Gwendal Grignou36b61702016-02-10 11:57:53 -0800459 if local_sources.find('\x00') != -1:
mukesh agrawal0d3616c2015-07-17 15:47:36 -0700460 raise error.TestError('one or more sources include NUL char')
461
mblighc9892c02010-01-06 19:02:16 +0000462 # If rsync is disabled or fails, try scp.
showard6eafb492010-01-15 20:29:06 +0000463 try_scp = True
464 if self.use_rsync():
Simran Basi882f15b2013-10-29 14:59:34 -0700465 logging.debug('Using Rsync.')
Gwendal Grignou36b61702016-02-10 11:57:53 -0800466 remote_dest = self._encode_remote_paths([dest])
mblighc9892c02010-01-06 19:02:16 +0000467 try:
mblighc9892c02010-01-06 19:02:16 +0000468 rsync = self._make_rsync_cmd(local_sources, remote_dest,
Luigi Semenzato9b083072016-12-19 16:50:40 -0800469 delete_dest, preserve_symlinks,
470 False)
mblighc9892c02010-01-06 19:02:16 +0000471 utils.run(rsync)
showard6eafb492010-01-15 20:29:06 +0000472 try_scp = False
mblighc9892c02010-01-06 19:02:16 +0000473 except error.CmdError, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700474 logging.warning("trying scp, rsync failed: %s", e)
mblighc9892c02010-01-06 19:02:16 +0000475
476 if try_scp:
Simran Basi882f15b2013-10-29 14:59:34 -0700477 logging.debug('Trying scp.')
jadmanskid7b79ed2009-01-07 17:19:48 +0000478 # scp has no equivalent to --delete, just drop the entire dest dir
479 if delete_dest:
showard27160152009-07-15 14:28:42 +0000480 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000481 ignore_status=True).exit_status == 0
482 if is_dir:
483 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000484 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000485 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000486
Gwendal Grignou36b61702016-02-10 11:57:53 -0800487 remote_dest = self._encode_remote_paths([dest], use_scp=True)
jadmanski2583a432009-02-10 23:59:11 +0000488 local_sources = self._make_rsync_compatible_source(source, True)
489 if local_sources:
Cheng-Yi Chiang9b2812d2016-02-29 17:01:44 +0800490 sources = self._encode_local_paths(local_sources, escape=False)
491 scp = self._make_scp_cmd(sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000492 try:
493 utils.run(scp)
494 except error.CmdError, e:
Simran Basi882f15b2013-10-29 14:59:34 -0700495 logging.debug('scp failed: %s', e)
jadmanskid7b79ed2009-01-07 17:19:48 +0000496 raise error.AutoservRunError(e.args[0], e.args[1])
mukesh agrawal0d3616c2015-07-17 15:47:36 -0700497 else:
498 logging.debug('skipping scp for empty source list')
jadmanskid7b79ed2009-01-07 17:19:48 +0000499
jadmanskica7da372008-10-21 16:26:52 +0000500
Simran Basi1621c632015-10-14 12:22:23 -0700501 def verify_ssh_user_access(self):
502 """Verify ssh access to this host.
503
504 @returns False if ssh_ping fails due to Permissions error, True
505 otherwise.
506 """
507 try:
508 self.ssh_ping()
509 except (error.AutoservSshPermissionDeniedError,
510 error.AutoservSshPingHostError):
511 return False
512 return True
513
514
Luigi Semenzato135574c2016-08-31 17:25:08 -0700515 def ssh_ping(self, timeout=60, connect_timeout=None, base_cmd='true'):
beepsadd66d32013-03-04 17:21:51 -0800516 """
517 Pings remote host via ssh.
518
519 @param timeout: Time in seconds before giving up.
520 Defaults to 60 seconds.
beeps46dadc92013-11-07 14:07:10 -0800521 @param base_cmd: The base command to run with the ssh ping.
522 Defaults to true.
beepsadd66d32013-03-04 17:21:51 -0800523 @raise AutoservSSHTimeout: If the ssh ping times out.
524 @raise AutoservSshPermissionDeniedError: If ssh ping fails due to
525 permissions.
526 @raise AutoservSshPingHostError: For other AutoservRunErrors.
527 """
Luigi Semenzato135574c2016-08-31 17:25:08 -0700528 ctimeout = min(timeout, connect_timeout or timeout)
jadmanskica7da372008-10-21 16:26:52 +0000529 try:
Luigi Semenzatobfbd1f32017-01-06 10:41:18 -0800530 self.run(base_cmd, timeout=timeout, connect_timeout=ctimeout,
531 ssh_failure_retry_ok=True)
jadmanskica7da372008-10-21 16:26:52 +0000532 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000533 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000534 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000535 except error.AutoservSshPermissionDeniedError:
536 #let AutoservSshPermissionDeniedError be visible to the callers
537 raise
jadmanskica7da372008-10-21 16:26:52 +0000538 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000539 # convert the generic AutoservRunError into something more
540 # specific for this context
541 raise error.AutoservSshPingHostError(e.description + '\n' +
542 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000543
544
Luigi Semenzato135574c2016-08-31 17:25:08 -0700545 def is_up(self, timeout=60, connect_timeout=None, base_cmd='true'):
jadmanskica7da372008-10-21 16:26:52 +0000546 """
beeps46dadc92013-11-07 14:07:10 -0800547 Check if the remote host is up by ssh-ing and running a base command.
jadmanskica7da372008-10-21 16:26:52 +0000548
beepsadd66d32013-03-04 17:21:51 -0800549 @param timeout: timeout in seconds.
beeps46dadc92013-11-07 14:07:10 -0800550 @param base_cmd: a base command to run with ssh. The default is 'true'.
beepsadd66d32013-03-04 17:21:51 -0800551 @returns True if the remote host is up before the timeout expires,
552 False otherwise.
jadmanskica7da372008-10-21 16:26:52 +0000553 """
554 try:
Luigi Semenzato135574c2016-08-31 17:25:08 -0700555 self.ssh_ping(timeout=timeout,
556 connect_timeout=connect_timeout,
557 base_cmd=base_cmd)
jadmanskica7da372008-10-21 16:26:52 +0000558 except error.AutoservError:
559 return False
560 else:
561 return True
562
563
564 def wait_up(self, timeout=None):
565 """
566 Wait until the remote host is up or the timeout expires.
567
568 In fact, it will wait until an ssh connection to the remote
569 host can be established, and getty is running.
570
jadmanskic0354912010-01-12 15:57:29 +0000571 @param timeout time limit in seconds before returning even
572 if the host is not up.
jadmanskica7da372008-10-21 16:26:52 +0000573
beepsadd66d32013-03-04 17:21:51 -0800574 @returns True if the host was found to be up before the timeout expires,
575 False otherwise
jadmanskica7da372008-10-21 16:26:52 +0000576 """
577 if timeout:
beeps46dadc92013-11-07 14:07:10 -0800578 current_time = int(time.time())
579 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000580
Luigi Semenzato135574c2016-08-31 17:25:08 -0700581 autoserv_error_logged = False
beepsadd66d32013-03-04 17:21:51 -0800582 while not timeout or current_time < end_time:
Luigi Semenzato135574c2016-08-31 17:25:08 -0700583 if self.is_up(timeout=end_time - current_time,
584 connect_timeout=20):
jadmanskica7da372008-10-21 16:26:52 +0000585 try:
586 if self.are_wait_up_processes_up():
jadmanski7ebac3d2010-06-17 16:06:31 +0000587 logging.debug('Host %s is now up', self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000588 return True
Luigi Semenzato135574c2016-08-31 17:25:08 -0700589 except error.AutoservError as e:
590 if not autoserv_error_logged:
591 logging.debug('Ignoring failure to reach %s: %s %s',
592 self.hostname, e,
593 '(and further similar failures)')
594 autoserv_error_logged = True
jadmanskica7da372008-10-21 16:26:52 +0000595 time.sleep(1)
beeps46dadc92013-11-07 14:07:10 -0800596 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000597
jadmanski7ebac3d2010-06-17 16:06:31 +0000598 logging.debug('Host %s is still down after waiting %d seconds',
599 self.hostname, int(timeout + time.time() - end_time))
jadmanskica7da372008-10-21 16:26:52 +0000600 return False
601
602
jadmanskic0354912010-01-12 15:57:29 +0000603 def wait_down(self, timeout=None, warning_timer=None, old_boot_id=None):
jadmanskica7da372008-10-21 16:26:52 +0000604 """
605 Wait until the remote host is down or the timeout expires.
606
jadmanskic0354912010-01-12 15:57:29 +0000607 If old_boot_id is provided, this will wait until either the machine
608 is unpingable or self.get_boot_id() returns a value different from
609 old_boot_id. If the boot_id value has changed then the function
610 returns true under the assumption that the machine has shut down
611 and has now already come back up.
jadmanskica7da372008-10-21 16:26:52 +0000612
jadmanskic0354912010-01-12 15:57:29 +0000613 If old_boot_id is None then until the machine becomes unreachable the
614 method assumes the machine has not yet shut down.
jadmanskica7da372008-10-21 16:26:52 +0000615
beepsadd66d32013-03-04 17:21:51 -0800616 Based on this definition, the 4 possible permutations of timeout
617 and old_boot_id are:
618 1. timeout and old_boot_id: wait timeout seconds for either the
619 host to become unpingable, or the boot id
620 to change. In the latter case we've rebooted
621 and in the former case we've only shutdown,
622 but both cases return True.
623 2. only timeout: wait timeout seconds for the host to become unpingable.
624 If the host remains pingable throughout timeout seconds
625 we return False.
626 3. only old_boot_id: wait forever until either the host becomes
627 unpingable or the boot_id changes. Return true
628 when either of those conditions are met.
629 4. not timeout, not old_boot_id: wait forever till the host becomes
630 unpingable.
631
jadmanskic0354912010-01-12 15:57:29 +0000632 @param timeout Time limit in seconds before returning even
633 if the host is still up.
634 @param warning_timer Time limit in seconds that will generate
635 a warning if the host is not down yet.
636 @param old_boot_id A string containing the result of self.get_boot_id()
637 prior to the host being told to shut down. Can be None if this is
638 not available.
639
640 @returns True if the host was found to be down, False otherwise
jadmanskica7da372008-10-21 16:26:52 +0000641 """
mblighe5e3cf22010-05-27 23:33:14 +0000642 #TODO: there is currently no way to distinguish between knowing
643 #TODO: boot_id was unsupported and not knowing the boot_id.
beeps46dadc92013-11-07 14:07:10 -0800644 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000645 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000646 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000647
mbligh2ed998f2009-04-08 21:03:47 +0000648 if warning_timer:
649 warn_time = current_time + warning_timer
650
jadmanskic0354912010-01-12 15:57:29 +0000651 if old_boot_id is not None:
652 logging.debug('Host %s pre-shutdown boot_id is %s',
653 self.hostname, old_boot_id)
654
beepsadd66d32013-03-04 17:21:51 -0800655 # Impose semi real-time deadline constraints, since some clients
656 # (eg: watchdog timer tests) expect strict checking of time elapsed.
657 # Each iteration of this loop is treated as though it atomically
658 # completes within current_time, this is needed because if we used
659 # inline time.time() calls instead then the following could happen:
660 #
661 # while not timeout or time.time() < end_time: [23 < 30]
662 # some code. [takes 10 secs]
663 # try:
664 # new_boot_id = self.get_boot_id(timeout=end_time - time.time())
665 # [30 - 33]
666 # The last step will lead to a return True, when in fact the machine
667 # went down at 32 seconds (>30). Hence we need to pass get_boot_id
668 # the same time that allowed us into that iteration of the loop.
mbligh2ed998f2009-04-08 21:03:47 +0000669 while not timeout or current_time < end_time:
jadmanskic0354912010-01-12 15:57:29 +0000670 try:
beeps46dadc92013-11-07 14:07:10 -0800671 new_boot_id = self.get_boot_id(timeout=end_time-current_time)
mblighdbc7e4a2010-01-15 20:34:20 +0000672 except error.AutoservError:
jadmanskic0354912010-01-12 15:57:29 +0000673 logging.debug('Host %s is now unreachable over ssh, is down',
674 self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000675 return True
jadmanskic0354912010-01-12 15:57:29 +0000676 else:
677 # if the machine is up but the boot_id value has changed from
678 # old boot id, then we can assume the machine has gone down
679 # and then already come back up
680 if old_boot_id is not None and old_boot_id != new_boot_id:
681 logging.debug('Host %s now has boot_id %s and so must '
682 'have rebooted', self.hostname, new_boot_id)
683 return True
mbligh2ed998f2009-04-08 21:03:47 +0000684
685 if warning_timer and current_time > warn_time:
Scott Zawalskic86fdeb2013-10-23 10:24:04 -0400686 self.record("INFO", None, "shutdown",
mbligh2ed998f2009-04-08 21:03:47 +0000687 "Shutdown took longer than %ds" % warning_timer)
688 # Print the warning only once.
689 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000690 # If a machine is stuck switching runlevels
691 # This may cause the machine to reboot.
692 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000693
jadmanskica7da372008-10-21 16:26:52 +0000694 time.sleep(1)
beeps46dadc92013-11-07 14:07:10 -0800695 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000696
697 return False
jadmanskif6562912008-10-21 17:59:01 +0000698
mbligha0a27592009-01-24 01:41:36 +0000699
jadmanskif6562912008-10-21 17:59:01 +0000700 # tunable constants for the verify & repair code
mblighb86bfa12010-02-12 20:22:21 +0000701 AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER",
702 "gb_diskspace_required",
Fang Deng6b05f5b2013-03-20 13:42:11 -0700703 type=float,
704 default=20.0)
mbligha0a27592009-01-24 01:41:36 +0000705
jadmanskif6562912008-10-21 17:59:01 +0000706
showardca572982009-09-18 21:20:01 +0000707 def verify_connectivity(self):
708 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000709
showardb18134f2009-03-20 20:52:18 +0000710 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000711 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000712 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000713
jadmanski80deb752009-01-21 17:14:16 +0000714 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000715 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000716
mblighb49b5232009-02-12 21:54:49 +0000717
showardca572982009-09-18 21:20:01 +0000718 def verify_software(self):
719 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000720 try:
showardad812bf2009-10-20 23:49:56 +0000721 self.check_diskspace(autotest.Autotest.get_install_dir(self),
722 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000723 except error.AutoservHostError:
724 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000725 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000726 # autotest dir may not exist, etc. ignore
727 logging.debug('autodir space check exception, this is probably '
728 'safe to ignore\n' + traceback.format_exc())
mblighefccc1b2010-01-11 19:08:42 +0000729
730
731 def close(self):
732 super(AbstractSSHHost, self).close()
Godofredo Contreras773179e2016-05-24 10:17:48 -0700733 self.rpc_server_tracker.disconnect_all()
mblighefccc1b2010-01-11 19:08:42 +0000734 self._cleanup_master_ssh()
xixuand6011f12016-12-08 15:01:58 -0800735 if os.path.exists(self.known_hosts_file):
736 os.remove(self.known_hosts_file)
mblighefccc1b2010-01-11 19:08:42 +0000737
738
Luigi Semenzato3b95ede2016-12-09 11:51:01 -0800739 def restart_master_ssh(self):
740 """
741 Stop and restart the ssh master connection. This is meant as a last
742 resort when ssh commands fail and we don't understand why.
743 """
744 logging.debug('Restarting master ssh connection')
745 self._cleanup_master_ssh()
746 self.start_master_ssh(timeout=30)
747
748
mblighefccc1b2010-01-11 19:08:42 +0000749 def _cleanup_master_ssh(self):
750 """
751 Release all resources (process, temporary directory) used by an active
752 master SSH connection.
753 """
754 # If a master SSH connection is running, kill it.
755 if self.master_ssh_job is not None:
Aviv Keshet46250752013-08-27 15:52:06 -0700756 logging.debug('Nuking master_ssh_job.')
mblighefccc1b2010-01-11 19:08:42 +0000757 utils.nuke_subprocess(self.master_ssh_job.sp)
758 self.master_ssh_job = None
759
760 # Remove the temporary directory for the master SSH socket.
761 if self.master_ssh_tempdir is not None:
Aviv Keshet46250752013-08-27 15:52:06 -0700762 logging.debug('Cleaning master_ssh_tempdir.')
mblighefccc1b2010-01-11 19:08:42 +0000763 self.master_ssh_tempdir.clean()
764 self.master_ssh_tempdir = None
765 self.master_ssh_option = ''
766
767
Aviv Keshet0749a822013-10-17 09:53:26 -0700768 def start_master_ssh(self, timeout=5):
mblighefccc1b2010-01-11 19:08:42 +0000769 """
770 Called whenever a slave SSH connection needs to be initiated (e.g., by
771 run, rsync, scp). If master SSH support is enabled and a master SSH
772 connection is not active already, start a new one in the background.
773 Also, cleanup any zombie master SSH connections (e.g., dead due to
774 reboot).
Aviv Keshet0749a822013-10-17 09:53:26 -0700775
776 timeout: timeout in seconds (default 5) to wait for master ssh
777 connection to be established. If timeout is reached, a
778 warning message is logged, but no other action is taken.
mblighefccc1b2010-01-11 19:08:42 +0000779 """
780 if not enable_master_ssh:
781 return
782
Simran Basi3b858a22015-03-17 16:23:24 -0700783 # Multiple processes might try in parallel to clean up the old master
784 # ssh connection and create a new one, therefore use a lock to protect
785 # against race conditions.
786 with self._lock:
787 # If a previously started master SSH connection is not running
788 # anymore, it needs to be cleaned up and then restarted.
789 if self.master_ssh_job is not None:
790 socket_path = os.path.join(self.master_ssh_tempdir.name,
791 'socket')
792 if (not os.path.exists(socket_path) or
793 self.master_ssh_job.sp.poll() is not None):
794 logging.info("Master ssh connection to %s is down.",
795 self.hostname)
796 self._cleanup_master_ssh()
mblighefccc1b2010-01-11 19:08:42 +0000797
Simran Basi3b858a22015-03-17 16:23:24 -0700798 # Start a new master SSH connection.
799 if self.master_ssh_job is None:
800 # Create a shared socket in a temp location.
801 self.master_ssh_tempdir = autotemp.tempdir(
802 unique_id='ssh-master')
803 self.master_ssh_option = ("-o ControlPath=%s/socket" %
804 self.master_ssh_tempdir.name)
mblighefccc1b2010-01-11 19:08:42 +0000805
Simran Basi3b858a22015-03-17 16:23:24 -0700806 # Start the master SSH connection in the background.
807 master_cmd = self.ssh_command(
808 options="-N -o ControlMaster=yes")
809 logging.info("Starting master ssh connection '%s'", master_cmd)
810 self.master_ssh_job = utils.BgJob(master_cmd,
811 nickname='master-ssh',
812 no_pipes=True)
813 # To prevent a race between the the master ssh connection
814 # startup and its first attempted use, wait for socket file to
815 # exist before returning.
816 end_time = time.time() + timeout
817 socket_file_path = os.path.join(self.master_ssh_tempdir.name,
818 'socket')
819 while time.time() < end_time:
820 if os.path.exists(socket_file_path):
821 break
822 time.sleep(.2)
823 else:
824 logging.info('Timed out waiting for master-ssh connection '
825 'to be established.')
mbligh0a883702010-04-21 01:58:34 +0000826
827
828 def clear_known_hosts(self):
829 """Clears out the temporary ssh known_hosts file.
830
831 This is useful if the test SSHes to the machine, then reinstalls it,
832 then SSHes to it again. It can be called after the reinstall to
833 reduce the spam in the logs.
834 """
835 logging.info("Clearing known hosts for host '%s', file '%s'.",
Fang Deng3af66202013-08-16 15:19:25 -0700836 self.hostname, self.known_hosts_file)
mbligh0a883702010-04-21 01:58:34 +0000837 # Clear out the file by opening it for writing and then closing.
Fang Deng3af66202013-08-16 15:19:25 -0700838 fh = open(self.known_hosts_file, "w")
mbligh0a883702010-04-21 01:58:34 +0000839 fh.close()
Prashanth B98509c72014-04-04 16:01:34 -0700840
841
842 def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True):
843 """Copy log directories from a host to a local directory.
844
845 @param remote_src_dir: A destination directory on the host.
846 @param local_dest_dir: A path to a local destination directory.
847 If it doesn't exist it will be created.
848 @param ignore_errors: If True, ignore exceptions.
849
850 @raises OSError: If there were problems creating the local_dest_dir and
851 ignore_errors is False.
852 @raises AutoservRunError, AutotestRunError: If something goes wrong
853 while copying the directories and ignore_errors is False.
854 """
855 locally_created_dest = False
856 if (not os.path.exists(local_dest_dir)
857 or not os.path.isdir(local_dest_dir)):
858 try:
859 os.makedirs(local_dest_dir)
860 locally_created_dest = True
861 except OSError as e:
862 logging.warning('Unable to collect logs from host '
863 '%s: %s', self.hostname, e)
864 if not ignore_errors:
865 raise
866 return
867 try:
Luigi Semenzato9b083072016-12-19 16:50:40 -0800868 self.get_file(remote_src_dir, local_dest_dir, safe_symlinks=True)
Prashanth B98509c72014-04-04 16:01:34 -0700869 except (error.AutotestRunError, error.AutoservRunError,
870 error.AutoservSSHTimeout) as e:
871 logging.warning('Collection of %s to local dir %s from host %s '
872 'failed: %s', remote_src_dir, local_dest_dir,
873 self.hostname, e)
874 if locally_created_dest:
875 shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors)
876 if not ignore_errors:
877 raise
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +0800878
879
xixuan6cf6d2f2016-01-29 15:29:00 -0800880 def create_ssh_tunnel(self, port, local_port):
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +0800881 """Create an ssh tunnel from local_port to port.
882
xixuan6cf6d2f2016-01-29 15:29:00 -0800883 This is used to forward a port securely through a tunnel process from
884 the server to the DUT for RPC server connection.
885
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +0800886 @param port: remote port on the host.
887 @param local_port: local forwarding port.
888
889 @return: the tunnel process.
890 """
891 tunnel_options = '-n -N -q -L %d:localhost:%d' % (local_port, port)
892 ssh_cmd = self.make_ssh_command(opts=tunnel_options)
893 tunnel_cmd = '%s %s' % (ssh_cmd, self.hostname)
894 logging.debug('Full tunnel command: %s', tunnel_cmd)
xixuan6cf6d2f2016-01-29 15:29:00 -0800895 # Exec the ssh process directly here rather than using a shell.
896 # Using a shell leaves a dangling ssh process, because we deliver
897 # signals to the shell wrapping ssh, not the ssh process itself.
898 args = shlex.split(tunnel_cmd)
899 tunnel_proc = subprocess.Popen(args, close_fds=True)
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +0800900 logging.debug('Started ssh tunnel, local = %d'
901 ' remote = %d, pid = %d',
902 local_port, port, tunnel_proc.pid)
903 return tunnel_proc
Gilad Arnolda76bef02015-09-29 13:55:15 -0700904
905
xixuan6cf6d2f2016-01-29 15:29:00 -0800906 def disconnect_ssh_tunnel(self, tunnel_proc, port):
Roshan Pius58e5dd32015-10-16 15:16:42 -0700907 """
908 Disconnects a previously forwarded port from the server to the DUT for
909 RPC server connection.
910
xixuan6cf6d2f2016-01-29 15:29:00 -0800911 @param tunnel_proc: a tunnel process returned from |create_ssh_tunnel|.
912 @param port: remote port on the DUT, used in ADBHost.
Roshan Pius58e5dd32015-10-16 15:16:42 -0700913
914 """
915 if tunnel_proc.poll() is None:
916 tunnel_proc.terminate()
917 logging.debug('Terminated tunnel, pid %d', tunnel_proc.pid)
918 else:
919 logging.debug('Tunnel pid %d terminated early, status %d',
920 tunnel_proc.pid, tunnel_proc.returncode)
921
922
Gilad Arnolda76bef02015-09-29 13:55:15 -0700923 def get_os_type(self):
924 """Returns the host OS descriptor (to be implemented in subclasses).
925
926 @return A string describing the OS type.
927 """
Gwendal Grignou36b61702016-02-10 11:57:53 -0800928 raise NotImplementedError