blob: d8f51b3a809d897a0cab00dce1f491dbaf5c777a [file] [log] [blame]
Marc Herbert21eb6492015-11-13 15:48:53 -08001import os, time, socket, shutil, glob, logging, traceback, tempfile, re
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +08002import subprocess
3
Simran Basi3b858a22015-03-17 16:23:24 -07004from multiprocessing import Lock
Aviv Keshet53a216a2013-08-27 13:58:46 -07005from autotest_lib.client.common_lib import autotemp, error
jadmanski31c49b72008-10-27 20:44:48 +00006from autotest_lib.server import utils, autotest
mblighe8b93af2009-01-30 00:45:53 +00007from autotest_lib.server.hosts import remote
Roshan Pius58e5dd32015-10-16 15:16:42 -07008from autotest_lib.server.hosts import rpc_server_tracker
mblighefccc1b2010-01-11 19:08:42 +00009from autotest_lib.client.common_lib.global_config import global_config
jadmanskica7da372008-10-21 16:26:52 +000010
Aviv Keshet53a216a2013-08-27 13:58:46 -070011# pylint: disable-msg=C0111
jadmanskica7da372008-10-21 16:26:52 +000012
mblighb86bfa12010-02-12 20:22:21 +000013get_value = global_config.get_config_value
14enable_master_ssh = get_value('AUTOSERV', 'enable_master_ssh', type=bool,
15 default=False)
mblighefccc1b2010-01-11 19:08:42 +000016
17
Fang Deng96667ca2013-08-01 17:46:18 -070018class AbstractSSHHost(remote.RemoteHost):
mblighbc9402b2009-12-29 01:15:34 +000019 """
20 This class represents a generic implementation of most of the
jadmanskica7da372008-10-21 16:26:52 +000021 framework necessary for controlling a host via ssh. It implements
22 almost all of the abstract Host methods, except for the core
mblighbc9402b2009-12-29 01:15:34 +000023 Host.run method.
24 """
jadmanskica7da372008-10-21 16:26:52 +000025
jadmanskif6562912008-10-21 17:59:01 +000026 def _initialize(self, hostname, user="root", port=22, password="",
Simran Basi1bf60eb2015-12-01 16:39:29 -080027 is_client_install_supported=True, host_attributes={},
28 *args, **dargs):
jadmanskif6562912008-10-21 17:59:01 +000029 super(AbstractSSHHost, self)._initialize(hostname=hostname,
30 *args, **dargs)
Dan Shic07b8932014-12-11 15:22:30 -080031 # IP address is retrieved only on demand. Otherwise the host
32 # initialization will fail for host is not online.
33 self._ip = None
jadmanskica7da372008-10-21 16:26:52 +000034 self.user = user
35 self.port = port
36 self.password = password
Roshan Piusa58163a2015-10-14 13:36:29 -070037 self._is_client_install_supported = is_client_install_supported
showard6eafb492010-01-15 20:29:06 +000038 self._use_rsync = None
Fang Deng3af66202013-08-16 15:19:25 -070039 self.known_hosts_file = tempfile.mkstemp()[1]
Roshan Pius58e5dd32015-10-16 15:16:42 -070040 self._rpc_server_tracker = rpc_server_tracker.RpcServerTracker(self);
jadmanskica7da372008-10-21 16:26:52 +000041
mblighefccc1b2010-01-11 19:08:42 +000042 """
43 Master SSH connection background job, socket temp directory and socket
44 control path option. If master-SSH is enabled, these fields will be
45 initialized by start_master_ssh when a new SSH connection is initiated.
46 """
47 self.master_ssh_job = None
48 self.master_ssh_tempdir = None
49 self.master_ssh_option = ''
50
Simran Basi3b858a22015-03-17 16:23:24 -070051 # Create a Lock to protect against race conditions.
52 self._lock = Lock()
53
Simran Basi1bf60eb2015-12-01 16:39:29 -080054 self.host_attributes = host_attributes
55
showard6eafb492010-01-15 20:29:06 +000056
Dan Shic07b8932014-12-11 15:22:30 -080057 @property
58 def ip(self):
59 """@return IP address of the host.
60 """
61 if not self._ip:
62 self._ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
63 return self._ip
64
65
Roshan Piusa58163a2015-10-14 13:36:29 -070066 @property
67 def is_client_install_supported(self):
68 """"
69 Returns True if the host supports autotest client installs, False
70 otherwise.
71 """
72 return self._is_client_install_supported
73
74
Roshan Pius58e5dd32015-10-16 15:16:42 -070075 @property
76 def rpc_server_tracker(self):
77 """"
78 @return The RPC server tracker associated with this host.
79 """
80 return self._rpc_server_tracker
81
82
Fang Deng96667ca2013-08-01 17:46:18 -070083 def make_ssh_command(self, user="root", port=22, opts='',
84 hosts_file='/dev/null',
85 connect_timeout=30, alive_interval=300):
86 base_command = ("/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no "
87 "-o UserKnownHostsFile=%s -o BatchMode=yes "
88 "-o ConnectTimeout=%d -o ServerAliveInterval=%d "
89 "-l %s -p %d")
90 assert isinstance(connect_timeout, (int, long))
91 assert connect_timeout > 0 # can't disable the timeout
92 return base_command % (opts, hosts_file, connect_timeout,
93 alive_interval, user, port)
94
95
showard6eafb492010-01-15 20:29:06 +000096 def use_rsync(self):
97 if self._use_rsync is not None:
98 return self._use_rsync
99
mblighc9892c02010-01-06 19:02:16 +0000100 # Check if rsync is available on the remote host. If it's not,
101 # don't try to use it for any future file transfers.
showard6eafb492010-01-15 20:29:06 +0000102 self._use_rsync = self._check_rsync()
103 if not self._use_rsync:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700104 logging.warning("rsync not available on remote host %s -- disabled",
mblighc9892c02010-01-06 19:02:16 +0000105 self.hostname)
Eric Lie0493a42010-11-15 13:05:43 -0800106 return self._use_rsync
mblighc9892c02010-01-06 19:02:16 +0000107
108
109 def _check_rsync(self):
110 """
111 Check if rsync is available on the remote host.
112 """
113 try:
114 self.run("rsync --version", stdout_tee=None, stderr_tee=None)
115 except error.AutoservRunError:
116 return False
117 return True
118
jadmanskica7da372008-10-21 16:26:52 +0000119
showard56176ec2009-10-28 19:52:30 +0000120 def _encode_remote_paths(self, paths, escape=True):
mblighbc9402b2009-12-29 01:15:34 +0000121 """
122 Given a list of file paths, encodes it as a single remote path, in
123 the style used by rsync and scp.
124 """
showard56176ec2009-10-28 19:52:30 +0000125 if escape:
126 paths = [utils.scp_remote_escape(path) for path in paths]
Marc Herbert21eb6492015-11-13 15:48:53 -0800127
128 remote = self.hostname
129
130 # rsync and scp require IPv6 brackets, even when there isn't any
131 # trailing port number (ssh doesn't support IPv6 brackets).
132 # In the Python >= 3.3 future, 'import ipaddress' will parse addresses.
133 if re.search(r':.*:', remote):
134 remote = '[%s]' % remote
135
136 return '%s@%s:"%s"' % (self.user, remote, " ".join(paths))
jadmanskica7da372008-10-21 16:26:52 +0000137
jadmanskica7da372008-10-21 16:26:52 +0000138
mbligh45561782009-05-11 21:14:34 +0000139 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks):
mblighbc9402b2009-12-29 01:15:34 +0000140 """
141 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +0000142 appropriate rsync command for copying them. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +0000143 pre-encoded.
144 """
Fang Deng96667ca2013-08-01 17:46:18 -0700145 ssh_cmd = self.make_ssh_command(user=self.user, port=self.port,
146 opts=self.master_ssh_option,
147 hosts_file=self.known_hosts_file)
jadmanskid7b79ed2009-01-07 17:19:48 +0000148 if delete_dest:
149 delete_flag = "--delete"
150 else:
151 delete_flag = ""
mbligh45561782009-05-11 21:14:34 +0000152 if preserve_symlinks:
153 symlink_flag = ""
154 else:
155 symlink_flag = "-L"
Dan Shi06d7fbf2014-02-12 12:34:41 -0800156 command = ("rsync %s %s --timeout=1800 --rsh='%s' -az --no-o --no-g "
David Hendricksb8904182014-06-02 15:22:49 -0700157 "%s \"%s\"")
mbligh45561782009-05-11 21:14:34 +0000158 return command % (symlink_flag, delete_flag, ssh_cmd,
David Hendricksb8904182014-06-02 15:22:49 -0700159 " ".join(['"%s"' % p for p in sources]), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000160
161
Eric Li861b2d52011-02-04 14:50:35 -0800162 def _make_ssh_cmd(self, cmd):
163 """
164 Create a base ssh command string for the host which can be used
165 to run commands directly on the machine
166 """
Fang Deng96667ca2013-08-01 17:46:18 -0700167 base_cmd = self.make_ssh_command(user=self.user, port=self.port,
168 opts=self.master_ssh_option,
169 hosts_file=self.known_hosts_file)
Eric Li861b2d52011-02-04 14:50:35 -0800170
171 return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd))
172
jadmanskid7b79ed2009-01-07 17:19:48 +0000173 def _make_scp_cmd(self, sources, dest):
mblighbc9402b2009-12-29 01:15:34 +0000174 """
175 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +0000176 appropriate scp command for encoding it. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +0000177 pre-encoded.
178 """
mblighc0649d62010-01-15 18:15:58 +0000179 command = ("scp -rq %s -o StrictHostKeyChecking=no "
lmraf676f32010-02-04 03:36:26 +0000180 "-o UserKnownHostsFile=%s -P %d %s '%s'")
Fang Deng3af66202013-08-16 15:19:25 -0700181 return command % (self.master_ssh_option, self.known_hosts_file,
mblighefccc1b2010-01-11 19:08:42 +0000182 self.port, " ".join(sources), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000183
184
185 def _make_rsync_compatible_globs(self, path, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000186 """
187 Given an rsync-style path, returns a list of globbed paths
jadmanskid7b79ed2009-01-07 17:19:48 +0000188 that will hopefully provide equivalent behaviour for scp. Does not
189 support the full range of rsync pattern matching behaviour, only that
190 exposed in the get/send_file interface (trailing slashes).
191
192 The is_local param is flag indicating if the paths should be
mblighbc9402b2009-12-29 01:15:34 +0000193 interpreted as local or remote paths.
194 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000195
196 # non-trailing slash paths should just work
197 if len(path) == 0 or path[-1] != "/":
198 return [path]
199
200 # make a function to test if a pattern matches any files
201 if is_local:
showard56176ec2009-10-28 19:52:30 +0000202 def glob_matches_files(path, pattern):
203 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +0000204 else:
showard56176ec2009-10-28 19:52:30 +0000205 def glob_matches_files(path, pattern):
206 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
207 pattern),
208 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +0000209 return result.exit_status == 0
210
211 # take a set of globs that cover all files, and see which are needed
212 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +0000213 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +0000214
215 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000216 if is_local:
showard56176ec2009-10-28 19:52:30 +0000217 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
218 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000219 else:
showard56176ec2009-10-28 19:52:30 +0000220 return [utils.scp_remote_escape(path) + pattern
221 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000222
223
224 def _make_rsync_compatible_source(self, source, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000225 """
226 Applies the same logic as _make_rsync_compatible_globs, but
jadmanskid7b79ed2009-01-07 17:19:48 +0000227 applies it to an entire list of sources, producing a new list of
mblighbc9402b2009-12-29 01:15:34 +0000228 sources, properly quoted.
229 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000230 return sum((self._make_rsync_compatible_globs(path, is_local)
231 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000232
233
mblighfeac0102009-04-28 18:31:12 +0000234 def _set_umask_perms(self, dest):
mblighbc9402b2009-12-29 01:15:34 +0000235 """
236 Given a destination file/dir (recursively) set the permissions on
237 all the files and directories to the max allowed by running umask.
238 """
mblighfeac0102009-04-28 18:31:12 +0000239
240 # now this looks strange but I haven't found a way in Python to _just_
241 # get the umask, apparently the only option is to try to set it
242 umask = os.umask(0)
243 os.umask(umask)
244
245 max_privs = 0777 & ~umask
246
247 def set_file_privs(filename):
Chris Masone567d0d92011-12-19 09:38:30 -0800248 """Sets mode of |filename|. Assumes |filename| exists."""
249 file_stat = os.stat(filename)
mblighfeac0102009-04-28 18:31:12 +0000250
251 file_privs = max_privs
252 # if the original file permissions do not have at least one
253 # executable bit then do not set it anywhere
254 if not file_stat.st_mode & 0111:
255 file_privs &= ~0111
256
257 os.chmod(filename, file_privs)
258
259 # try a bottom-up walk so changes on directory permissions won't cut
260 # our access to the files/directories inside it
261 for root, dirs, files in os.walk(dest, topdown=False):
262 # when setting the privileges we emulate the chmod "X" behaviour
263 # that sets to execute only if it is a directory or any of the
264 # owner/group/other already has execute right
265 for dirname in dirs:
266 os.chmod(os.path.join(root, dirname), max_privs)
267
Chris Masone567d0d92011-12-19 09:38:30 -0800268 # Filter out broken symlinks as we go.
269 for filename in filter(os.path.exists, files):
mblighfeac0102009-04-28 18:31:12 +0000270 set_file_privs(os.path.join(root, filename))
271
272
273 # now set privs for the dest itself
274 if os.path.isdir(dest):
275 os.chmod(dest, max_privs)
276 else:
277 set_file_privs(dest)
278
279
mbligh45561782009-05-11 21:14:34 +0000280 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
281 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000282 """
283 Copy files from the remote host to a local path.
284
285 Directories will be copied recursively.
286 If a source component is a directory with a trailing slash,
287 the content of the directory will be copied, otherwise, the
288 directory itself and its content will be copied. This
289 behavior is similar to that of the program 'rsync'.
290
291 Args:
292 source: either
293 1) a single file or directory, as a string
294 2) a list of one or more (possibly mixed)
295 files or directories
296 dest: a file or a directory (if source contains a
297 directory or more than one element, you must
298 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000299 delete_dest: if this is true, the command will also clear
300 out any old files at dest that are not in the
301 source
mblighfeac0102009-04-28 18:31:12 +0000302 preserve_perm: tells get_file() to try to preserve the sources
303 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000304 preserve_symlinks: try to preserve symlinks instead of
305 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000306
307 Raises:
308 AutoservRunError: the scp command failed
309 """
Simran Basi882f15b2013-10-29 14:59:34 -0700310 logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,'
311 'preserve_perm: %s, preserve_symlinks:%s', source, dest,
312 delete_dest, preserve_perm, preserve_symlinks)
mblighefccc1b2010-01-11 19:08:42 +0000313 # Start a master SSH connection if necessary.
314 self.start_master_ssh()
315
jadmanskica7da372008-10-21 16:26:52 +0000316 if isinstance(source, basestring):
317 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000318 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000319
mblighc9892c02010-01-06 19:02:16 +0000320 # If rsync is disabled or fails, try scp.
showard6eafb492010-01-15 20:29:06 +0000321 try_scp = True
322 if self.use_rsync():
Simran Basi882f15b2013-10-29 14:59:34 -0700323 logging.debug('Using Rsync.')
mblighc9892c02010-01-06 19:02:16 +0000324 try:
325 remote_source = self._encode_remote_paths(source)
326 local_dest = utils.sh_escape(dest)
327 rsync = self._make_rsync_cmd([remote_source], local_dest,
328 delete_dest, preserve_symlinks)
329 utils.run(rsync)
showard6eafb492010-01-15 20:29:06 +0000330 try_scp = False
mblighc9892c02010-01-06 19:02:16 +0000331 except error.CmdError, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700332 logging.warning("trying scp, rsync failed: %s", e)
mblighc9892c02010-01-06 19:02:16 +0000333
334 if try_scp:
Simran Basi882f15b2013-10-29 14:59:34 -0700335 logging.debug('Trying scp.')
jadmanskid7b79ed2009-01-07 17:19:48 +0000336 # scp has no equivalent to --delete, just drop the entire dest dir
337 if delete_dest and os.path.isdir(dest):
338 shutil.rmtree(dest)
339 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000340
jadmanskid7b79ed2009-01-07 17:19:48 +0000341 remote_source = self._make_rsync_compatible_source(source, False)
342 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000343 # _make_rsync_compatible_source() already did the escaping
344 remote_source = self._encode_remote_paths(remote_source,
345 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000346 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000347 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000348 try:
349 utils.run(scp)
350 except error.CmdError, e:
Simran Basi882f15b2013-10-29 14:59:34 -0700351 logging.debug('scp failed: %s', e)
jadmanskid7b79ed2009-01-07 17:19:48 +0000352 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000353
mblighfeac0102009-04-28 18:31:12 +0000354 if not preserve_perm:
355 # we have no way to tell scp to not try to preserve the
356 # permissions so set them after copy instead.
357 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
358 # options are only in very recent rsync versions
359 self._set_umask_perms(dest)
360
jadmanskica7da372008-10-21 16:26:52 +0000361
mbligh45561782009-05-11 21:14:34 +0000362 def send_file(self, source, dest, delete_dest=False,
363 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000364 """
365 Copy files from a local path to the remote host.
366
367 Directories will be copied recursively.
368 If a source component is a directory with a trailing slash,
369 the content of the directory will be copied, otherwise, the
370 directory itself and its content will be copied. This
371 behavior is similar to that of the program 'rsync'.
372
373 Args:
374 source: either
375 1) a single file or directory, as a string
376 2) a list of one or more (possibly mixed)
377 files or directories
378 dest: a file or a directory (if source contains a
379 directory or more than one element, you must
380 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000381 delete_dest: if this is true, the command will also clear
382 out any old files at dest that are not in the
383 source
mbligh45561782009-05-11 21:14:34 +0000384 preserve_symlinks: controls if symlinks on the source will be
385 copied as such on the destination or transformed into the
386 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000387
388 Raises:
389 AutoservRunError: the scp command failed
390 """
Simran Basi882f15b2013-10-29 14:59:34 -0700391 logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,'
392 'preserve_symlinks:%s', source, dest,
393 delete_dest, preserve_symlinks)
mblighefccc1b2010-01-11 19:08:42 +0000394 # Start a master SSH connection if necessary.
395 self.start_master_ssh()
396
jadmanskica7da372008-10-21 16:26:52 +0000397 if isinstance(source, basestring):
398 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000399 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000400
mukesh agrawal0d3616c2015-07-17 15:47:36 -0700401 local_sources = [utils.sh_escape(path) for path in source]
402 if not local_sources:
403 raise error.TestError('source |%s| yielded an empty list' % (
404 source))
405 if any([local_source.find('\x00') != -1 for
406 local_source in local_sources]):
407 raise error.TestError('one or more sources include NUL char')
408
mblighc9892c02010-01-06 19:02:16 +0000409 # If rsync is disabled or fails, try scp.
showard6eafb492010-01-15 20:29:06 +0000410 try_scp = True
411 if self.use_rsync():
Simran Basi882f15b2013-10-29 14:59:34 -0700412 logging.debug('Using Rsync.')
mblighc9892c02010-01-06 19:02:16 +0000413 try:
mblighc9892c02010-01-06 19:02:16 +0000414 rsync = self._make_rsync_cmd(local_sources, remote_dest,
415 delete_dest, preserve_symlinks)
416 utils.run(rsync)
showard6eafb492010-01-15 20:29:06 +0000417 try_scp = False
mblighc9892c02010-01-06 19:02:16 +0000418 except error.CmdError, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700419 logging.warning("trying scp, rsync failed: %s", e)
mblighc9892c02010-01-06 19:02:16 +0000420
421 if try_scp:
Simran Basi882f15b2013-10-29 14:59:34 -0700422 logging.debug('Trying scp.')
jadmanskid7b79ed2009-01-07 17:19:48 +0000423 # scp has no equivalent to --delete, just drop the entire dest dir
424 if delete_dest:
showard27160152009-07-15 14:28:42 +0000425 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000426 ignore_status=True).exit_status == 0
427 if is_dir:
428 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000429 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000430 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000431
jadmanski2583a432009-02-10 23:59:11 +0000432 local_sources = self._make_rsync_compatible_source(source, True)
433 if local_sources:
434 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000435 try:
436 utils.run(scp)
437 except error.CmdError, e:
Simran Basi882f15b2013-10-29 14:59:34 -0700438 logging.debug('scp failed: %s', e)
jadmanskid7b79ed2009-01-07 17:19:48 +0000439 raise error.AutoservRunError(e.args[0], e.args[1])
mukesh agrawal0d3616c2015-07-17 15:47:36 -0700440 else:
441 logging.debug('skipping scp for empty source list')
jadmanskid7b79ed2009-01-07 17:19:48 +0000442
jadmanskica7da372008-10-21 16:26:52 +0000443
Simran Basi1621c632015-10-14 12:22:23 -0700444 def verify_ssh_user_access(self):
445 """Verify ssh access to this host.
446
447 @returns False if ssh_ping fails due to Permissions error, True
448 otherwise.
449 """
450 try:
451 self.ssh_ping()
452 except (error.AutoservSshPermissionDeniedError,
453 error.AutoservSshPingHostError):
454 return False
455 return True
456
457
beeps46dadc92013-11-07 14:07:10 -0800458 def ssh_ping(self, timeout=60, base_cmd='true'):
beepsadd66d32013-03-04 17:21:51 -0800459 """
460 Pings remote host via ssh.
461
462 @param timeout: Time in seconds before giving up.
463 Defaults to 60 seconds.
beeps46dadc92013-11-07 14:07:10 -0800464 @param base_cmd: The base command to run with the ssh ping.
465 Defaults to true.
beepsadd66d32013-03-04 17:21:51 -0800466 @raise AutoservSSHTimeout: If the ssh ping times out.
467 @raise AutoservSshPermissionDeniedError: If ssh ping fails due to
468 permissions.
469 @raise AutoservSshPingHostError: For other AutoservRunErrors.
470 """
jadmanskica7da372008-10-21 16:26:52 +0000471 try:
beeps46dadc92013-11-07 14:07:10 -0800472 self.run(base_cmd, timeout=timeout, connect_timeout=timeout)
jadmanskica7da372008-10-21 16:26:52 +0000473 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000474 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000475 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000476 except error.AutoservSshPermissionDeniedError:
477 #let AutoservSshPermissionDeniedError be visible to the callers
478 raise
jadmanskica7da372008-10-21 16:26:52 +0000479 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000480 # convert the generic AutoservRunError into something more
481 # specific for this context
482 raise error.AutoservSshPingHostError(e.description + '\n' +
483 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000484
485
beeps46dadc92013-11-07 14:07:10 -0800486 def is_up(self, timeout=60, base_cmd='true'):
jadmanskica7da372008-10-21 16:26:52 +0000487 """
beeps46dadc92013-11-07 14:07:10 -0800488 Check if the remote host is up by ssh-ing and running a base command.
jadmanskica7da372008-10-21 16:26:52 +0000489
beepsadd66d32013-03-04 17:21:51 -0800490 @param timeout: timeout in seconds.
beeps46dadc92013-11-07 14:07:10 -0800491 @param base_cmd: a base command to run with ssh. The default is 'true'.
beepsadd66d32013-03-04 17:21:51 -0800492 @returns True if the remote host is up before the timeout expires,
493 False otherwise.
jadmanskica7da372008-10-21 16:26:52 +0000494 """
495 try:
beeps46dadc92013-11-07 14:07:10 -0800496 self.ssh_ping(timeout=timeout, base_cmd=base_cmd)
jadmanskica7da372008-10-21 16:26:52 +0000497 except error.AutoservError:
498 return False
499 else:
500 return True
501
502
503 def wait_up(self, timeout=None):
504 """
505 Wait until the remote host is up or the timeout expires.
506
507 In fact, it will wait until an ssh connection to the remote
508 host can be established, and getty is running.
509
jadmanskic0354912010-01-12 15:57:29 +0000510 @param timeout time limit in seconds before returning even
511 if the host is not up.
jadmanskica7da372008-10-21 16:26:52 +0000512
beepsadd66d32013-03-04 17:21:51 -0800513 @returns True if the host was found to be up before the timeout expires,
514 False otherwise
jadmanskica7da372008-10-21 16:26:52 +0000515 """
516 if timeout:
beeps46dadc92013-11-07 14:07:10 -0800517 current_time = int(time.time())
518 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000519
beepsadd66d32013-03-04 17:21:51 -0800520 while not timeout or current_time < end_time:
521 if self.is_up(timeout=end_time - current_time):
jadmanskica7da372008-10-21 16:26:52 +0000522 try:
523 if self.are_wait_up_processes_up():
jadmanski7ebac3d2010-06-17 16:06:31 +0000524 logging.debug('Host %s is now up', self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000525 return True
526 except error.AutoservError:
527 pass
528 time.sleep(1)
beeps46dadc92013-11-07 14:07:10 -0800529 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000530
jadmanski7ebac3d2010-06-17 16:06:31 +0000531 logging.debug('Host %s is still down after waiting %d seconds',
532 self.hostname, int(timeout + time.time() - end_time))
jadmanskica7da372008-10-21 16:26:52 +0000533 return False
534
535
jadmanskic0354912010-01-12 15:57:29 +0000536 def wait_down(self, timeout=None, warning_timer=None, old_boot_id=None):
jadmanskica7da372008-10-21 16:26:52 +0000537 """
538 Wait until the remote host is down or the timeout expires.
539
jadmanskic0354912010-01-12 15:57:29 +0000540 If old_boot_id is provided, this will wait until either the machine
541 is unpingable or self.get_boot_id() returns a value different from
542 old_boot_id. If the boot_id value has changed then the function
543 returns true under the assumption that the machine has shut down
544 and has now already come back up.
jadmanskica7da372008-10-21 16:26:52 +0000545
jadmanskic0354912010-01-12 15:57:29 +0000546 If old_boot_id is None then until the machine becomes unreachable the
547 method assumes the machine has not yet shut down.
jadmanskica7da372008-10-21 16:26:52 +0000548
beepsadd66d32013-03-04 17:21:51 -0800549 Based on this definition, the 4 possible permutations of timeout
550 and old_boot_id are:
551 1. timeout and old_boot_id: wait timeout seconds for either the
552 host to become unpingable, or the boot id
553 to change. In the latter case we've rebooted
554 and in the former case we've only shutdown,
555 but both cases return True.
556 2. only timeout: wait timeout seconds for the host to become unpingable.
557 If the host remains pingable throughout timeout seconds
558 we return False.
559 3. only old_boot_id: wait forever until either the host becomes
560 unpingable or the boot_id changes. Return true
561 when either of those conditions are met.
562 4. not timeout, not old_boot_id: wait forever till the host becomes
563 unpingable.
564
jadmanskic0354912010-01-12 15:57:29 +0000565 @param timeout Time limit in seconds before returning even
566 if the host is still up.
567 @param warning_timer Time limit in seconds that will generate
568 a warning if the host is not down yet.
569 @param old_boot_id A string containing the result of self.get_boot_id()
570 prior to the host being told to shut down. Can be None if this is
571 not available.
572
573 @returns True if the host was found to be down, False otherwise
jadmanskica7da372008-10-21 16:26:52 +0000574 """
mblighe5e3cf22010-05-27 23:33:14 +0000575 #TODO: there is currently no way to distinguish between knowing
576 #TODO: boot_id was unsupported and not knowing the boot_id.
beeps46dadc92013-11-07 14:07:10 -0800577 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000578 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000579 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000580
mbligh2ed998f2009-04-08 21:03:47 +0000581 if warning_timer:
582 warn_time = current_time + warning_timer
583
jadmanskic0354912010-01-12 15:57:29 +0000584 if old_boot_id is not None:
585 logging.debug('Host %s pre-shutdown boot_id is %s',
586 self.hostname, old_boot_id)
587
beepsadd66d32013-03-04 17:21:51 -0800588 # Impose semi real-time deadline constraints, since some clients
589 # (eg: watchdog timer tests) expect strict checking of time elapsed.
590 # Each iteration of this loop is treated as though it atomically
591 # completes within current_time, this is needed because if we used
592 # inline time.time() calls instead then the following could happen:
593 #
594 # while not timeout or time.time() < end_time: [23 < 30]
595 # some code. [takes 10 secs]
596 # try:
597 # new_boot_id = self.get_boot_id(timeout=end_time - time.time())
598 # [30 - 33]
599 # The last step will lead to a return True, when in fact the machine
600 # went down at 32 seconds (>30). Hence we need to pass get_boot_id
601 # the same time that allowed us into that iteration of the loop.
mbligh2ed998f2009-04-08 21:03:47 +0000602 while not timeout or current_time < end_time:
jadmanskic0354912010-01-12 15:57:29 +0000603 try:
beeps46dadc92013-11-07 14:07:10 -0800604 new_boot_id = self.get_boot_id(timeout=end_time-current_time)
mblighdbc7e4a2010-01-15 20:34:20 +0000605 except error.AutoservError:
jadmanskic0354912010-01-12 15:57:29 +0000606 logging.debug('Host %s is now unreachable over ssh, is down',
607 self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000608 return True
jadmanskic0354912010-01-12 15:57:29 +0000609 else:
610 # if the machine is up but the boot_id value has changed from
611 # old boot id, then we can assume the machine has gone down
612 # and then already come back up
613 if old_boot_id is not None and old_boot_id != new_boot_id:
614 logging.debug('Host %s now has boot_id %s and so must '
615 'have rebooted', self.hostname, new_boot_id)
616 return True
mbligh2ed998f2009-04-08 21:03:47 +0000617
618 if warning_timer and current_time > warn_time:
Scott Zawalskic86fdeb2013-10-23 10:24:04 -0400619 self.record("INFO", None, "shutdown",
mbligh2ed998f2009-04-08 21:03:47 +0000620 "Shutdown took longer than %ds" % warning_timer)
621 # Print the warning only once.
622 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000623 # If a machine is stuck switching runlevels
624 # This may cause the machine to reboot.
625 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000626
jadmanskica7da372008-10-21 16:26:52 +0000627 time.sleep(1)
beeps46dadc92013-11-07 14:07:10 -0800628 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000629
630 return False
jadmanskif6562912008-10-21 17:59:01 +0000631
mbligha0a27592009-01-24 01:41:36 +0000632
jadmanskif6562912008-10-21 17:59:01 +0000633 # tunable constants for the verify & repair code
mblighb86bfa12010-02-12 20:22:21 +0000634 AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER",
635 "gb_diskspace_required",
Fang Deng6b05f5b2013-03-20 13:42:11 -0700636 type=float,
637 default=20.0)
mbligha0a27592009-01-24 01:41:36 +0000638
jadmanskif6562912008-10-21 17:59:01 +0000639
showardca572982009-09-18 21:20:01 +0000640 def verify_connectivity(self):
641 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000642
showardb18134f2009-03-20 20:52:18 +0000643 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000644 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000645 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000646
jadmanski80deb752009-01-21 17:14:16 +0000647 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000648 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000649
mblighb49b5232009-02-12 21:54:49 +0000650
showardca572982009-09-18 21:20:01 +0000651 def verify_software(self):
652 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000653 try:
showardad812bf2009-10-20 23:49:56 +0000654 self.check_diskspace(autotest.Autotest.get_install_dir(self),
655 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000656 except error.AutoservHostError:
657 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000658 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000659 # autotest dir may not exist, etc. ignore
660 logging.debug('autodir space check exception, this is probably '
661 'safe to ignore\n' + traceback.format_exc())
mblighefccc1b2010-01-11 19:08:42 +0000662
663
664 def close(self):
665 super(AbstractSSHHost, self).close()
666 self._cleanup_master_ssh()
Fang Deng3af66202013-08-16 15:19:25 -0700667 os.remove(self.known_hosts_file)
Roshan Pius58e5dd32015-10-16 15:16:42 -0700668 self.rpc_server_tracker.disconnect_all()
mblighefccc1b2010-01-11 19:08:42 +0000669
670
671 def _cleanup_master_ssh(self):
672 """
673 Release all resources (process, temporary directory) used by an active
674 master SSH connection.
675 """
676 # If a master SSH connection is running, kill it.
677 if self.master_ssh_job is not None:
Aviv Keshet46250752013-08-27 15:52:06 -0700678 logging.debug('Nuking master_ssh_job.')
mblighefccc1b2010-01-11 19:08:42 +0000679 utils.nuke_subprocess(self.master_ssh_job.sp)
680 self.master_ssh_job = None
681
682 # Remove the temporary directory for the master SSH socket.
683 if self.master_ssh_tempdir is not None:
Aviv Keshet46250752013-08-27 15:52:06 -0700684 logging.debug('Cleaning master_ssh_tempdir.')
mblighefccc1b2010-01-11 19:08:42 +0000685 self.master_ssh_tempdir.clean()
686 self.master_ssh_tempdir = None
687 self.master_ssh_option = ''
688
689
Aviv Keshet0749a822013-10-17 09:53:26 -0700690 def start_master_ssh(self, timeout=5):
mblighefccc1b2010-01-11 19:08:42 +0000691 """
692 Called whenever a slave SSH connection needs to be initiated (e.g., by
693 run, rsync, scp). If master SSH support is enabled and a master SSH
694 connection is not active already, start a new one in the background.
695 Also, cleanup any zombie master SSH connections (e.g., dead due to
696 reboot).
Aviv Keshet0749a822013-10-17 09:53:26 -0700697
698 timeout: timeout in seconds (default 5) to wait for master ssh
699 connection to be established. If timeout is reached, a
700 warning message is logged, but no other action is taken.
mblighefccc1b2010-01-11 19:08:42 +0000701 """
702 if not enable_master_ssh:
703 return
704
Simran Basi3b858a22015-03-17 16:23:24 -0700705 # Multiple processes might try in parallel to clean up the old master
706 # ssh connection and create a new one, therefore use a lock to protect
707 # against race conditions.
708 with self._lock:
709 # If a previously started master SSH connection is not running
710 # anymore, it needs to be cleaned up and then restarted.
711 if self.master_ssh_job is not None:
712 socket_path = os.path.join(self.master_ssh_tempdir.name,
713 'socket')
714 if (not os.path.exists(socket_path) or
715 self.master_ssh_job.sp.poll() is not None):
716 logging.info("Master ssh connection to %s is down.",
717 self.hostname)
718 self._cleanup_master_ssh()
mblighefccc1b2010-01-11 19:08:42 +0000719
Simran Basi3b858a22015-03-17 16:23:24 -0700720 # Start a new master SSH connection.
721 if self.master_ssh_job is None:
722 # Create a shared socket in a temp location.
723 self.master_ssh_tempdir = autotemp.tempdir(
724 unique_id='ssh-master')
725 self.master_ssh_option = ("-o ControlPath=%s/socket" %
726 self.master_ssh_tempdir.name)
mblighefccc1b2010-01-11 19:08:42 +0000727
Simran Basi3b858a22015-03-17 16:23:24 -0700728 # Start the master SSH connection in the background.
729 master_cmd = self.ssh_command(
730 options="-N -o ControlMaster=yes")
731 logging.info("Starting master ssh connection '%s'", master_cmd)
732 self.master_ssh_job = utils.BgJob(master_cmd,
733 nickname='master-ssh',
734 no_pipes=True)
735 # To prevent a race between the the master ssh connection
736 # startup and its first attempted use, wait for socket file to
737 # exist before returning.
738 end_time = time.time() + timeout
739 socket_file_path = os.path.join(self.master_ssh_tempdir.name,
740 'socket')
741 while time.time() < end_time:
742 if os.path.exists(socket_file_path):
743 break
744 time.sleep(.2)
745 else:
746 logging.info('Timed out waiting for master-ssh connection '
747 'to be established.')
mbligh0a883702010-04-21 01:58:34 +0000748
749
750 def clear_known_hosts(self):
751 """Clears out the temporary ssh known_hosts file.
752
753 This is useful if the test SSHes to the machine, then reinstalls it,
754 then SSHes to it again. It can be called after the reinstall to
755 reduce the spam in the logs.
756 """
757 logging.info("Clearing known hosts for host '%s', file '%s'.",
Fang Deng3af66202013-08-16 15:19:25 -0700758 self.hostname, self.known_hosts_file)
mbligh0a883702010-04-21 01:58:34 +0000759 # Clear out the file by opening it for writing and then closing.
Fang Deng3af66202013-08-16 15:19:25 -0700760 fh = open(self.known_hosts_file, "w")
mbligh0a883702010-04-21 01:58:34 +0000761 fh.close()
Prashanth B98509c72014-04-04 16:01:34 -0700762
763
764 def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True):
765 """Copy log directories from a host to a local directory.
766
767 @param remote_src_dir: A destination directory on the host.
768 @param local_dest_dir: A path to a local destination directory.
769 If it doesn't exist it will be created.
770 @param ignore_errors: If True, ignore exceptions.
771
772 @raises OSError: If there were problems creating the local_dest_dir and
773 ignore_errors is False.
774 @raises AutoservRunError, AutotestRunError: If something goes wrong
775 while copying the directories and ignore_errors is False.
776 """
777 locally_created_dest = False
778 if (not os.path.exists(local_dest_dir)
779 or not os.path.isdir(local_dest_dir)):
780 try:
781 os.makedirs(local_dest_dir)
782 locally_created_dest = True
783 except OSError as e:
784 logging.warning('Unable to collect logs from host '
785 '%s: %s', self.hostname, e)
786 if not ignore_errors:
787 raise
788 return
789 try:
790 self.get_file(
791 remote_src_dir, local_dest_dir, preserve_symlinks=True)
792 except (error.AutotestRunError, error.AutoservRunError,
793 error.AutoservSSHTimeout) as e:
794 logging.warning('Collection of %s to local dir %s from host %s '
795 'failed: %s', remote_src_dir, local_dest_dir,
796 self.hostname, e)
797 if locally_created_dest:
798 shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors)
799 if not ignore_errors:
800 raise
Cheng-Yi Chianga155e7e2015-08-20 20:42:04 +0800801
802
803 def _create_ssh_tunnel(self, port, local_port):
804 """Create an ssh tunnel from local_port to port.
805
806 @param port: remote port on the host.
807 @param local_port: local forwarding port.
808
809 @return: the tunnel process.
810 """
811 tunnel_options = '-n -N -q -L %d:localhost:%d' % (local_port, port)
812 ssh_cmd = self.make_ssh_command(opts=tunnel_options)
813 tunnel_cmd = '%s %s' % (ssh_cmd, self.hostname)
814 logging.debug('Full tunnel command: %s', tunnel_cmd)
815 tunnel_proc = subprocess.Popen(tunnel_cmd, shell=True, close_fds=True)
816 logging.debug('Started ssh tunnel, local = %d'
817 ' remote = %d, pid = %d',
818 local_port, port, tunnel_proc.pid)
819 return tunnel_proc
Gilad Arnolda76bef02015-09-29 13:55:15 -0700820
821
Roshan Pius58e5dd32015-10-16 15:16:42 -0700822 def rpc_port_forward(self, port, local_port):
823 """
824 Forwards a port securely through a tunnel process from the server
825 to the DUT for RPC server connection.
826
827 @param port: remote port on the DUT.
828 @param local_port: local forwarding port.
829
830 @return: the tunnel process.
831 """
832 return self._create_ssh_tunnel(port, local_port)
833
834
835 def rpc_port_disconnect(self, tunnel_proc, port):
836 """
837 Disconnects a previously forwarded port from the server to the DUT for
838 RPC server connection.
839
840 @param tunnel_proc: the original tunnel process returned from
841 |rpc_port_forward|.
842 @param port: remote port on the DUT.
843
844 """
845 if tunnel_proc.poll() is None:
846 tunnel_proc.terminate()
847 logging.debug('Terminated tunnel, pid %d', tunnel_proc.pid)
848 else:
849 logging.debug('Tunnel pid %d terminated early, status %d',
850 tunnel_proc.pid, tunnel_proc.returncode)
851
852
Gilad Arnolda76bef02015-09-29 13:55:15 -0700853 def get_os_type(self):
854 """Returns the host OS descriptor (to be implemented in subclasses).
855
856 @return A string describing the OS type.
857 """
858 raise NotImplementedError