blob: af38b25b15c8c4f3cdd5c82620a7bd06b9c810ca [file] [log] [blame]
Aviv Keshet07f16242013-10-10 07:54:19 -07001import os, time, socket, shutil, glob, logging, traceback, tempfile
Simran Basi3b858a22015-03-17 16:23:24 -07002from multiprocessing import Lock
Aviv Keshet53a216a2013-08-27 13:58:46 -07003from autotest_lib.client.common_lib import autotemp, error
jadmanski31c49b72008-10-27 20:44:48 +00004from autotest_lib.server import utils, autotest
mblighe8b93af2009-01-30 00:45:53 +00005from autotest_lib.server.hosts import remote
mblighefccc1b2010-01-11 19:08:42 +00006from autotest_lib.client.common_lib.global_config import global_config
jadmanskica7da372008-10-21 16:26:52 +00007
Aviv Keshet53a216a2013-08-27 13:58:46 -07008# pylint: disable-msg=C0111
jadmanskica7da372008-10-21 16:26:52 +00009
mblighb86bfa12010-02-12 20:22:21 +000010get_value = global_config.get_config_value
11enable_master_ssh = get_value('AUTOSERV', 'enable_master_ssh', type=bool,
12 default=False)
mblighefccc1b2010-01-11 19:08:42 +000013
14
Fang Deng96667ca2013-08-01 17:46:18 -070015class AbstractSSHHost(remote.RemoteHost):
mblighbc9402b2009-12-29 01:15:34 +000016 """
17 This class represents a generic implementation of most of the
jadmanskica7da372008-10-21 16:26:52 +000018 framework necessary for controlling a host via ssh. It implements
19 almost all of the abstract Host methods, except for the core
mblighbc9402b2009-12-29 01:15:34 +000020 Host.run method.
21 """
jadmanskica7da372008-10-21 16:26:52 +000022
jadmanskif6562912008-10-21 17:59:01 +000023 def _initialize(self, hostname, user="root", port=22, password="",
24 *args, **dargs):
25 super(AbstractSSHHost, self)._initialize(hostname=hostname,
26 *args, **dargs)
Dan Shic07b8932014-12-11 15:22:30 -080027 # IP address is retrieved only on demand. Otherwise the host
28 # initialization will fail for host is not online.
29 self._ip = None
jadmanskica7da372008-10-21 16:26:52 +000030 self.user = user
31 self.port = port
32 self.password = password
showard6eafb492010-01-15 20:29:06 +000033 self._use_rsync = None
Fang Deng3af66202013-08-16 15:19:25 -070034 self.known_hosts_file = tempfile.mkstemp()[1]
jadmanskica7da372008-10-21 16:26:52 +000035
mblighefccc1b2010-01-11 19:08:42 +000036 """
37 Master SSH connection background job, socket temp directory and socket
38 control path option. If master-SSH is enabled, these fields will be
39 initialized by start_master_ssh when a new SSH connection is initiated.
40 """
41 self.master_ssh_job = None
42 self.master_ssh_tempdir = None
43 self.master_ssh_option = ''
44
Simran Basi3b858a22015-03-17 16:23:24 -070045 # Create a Lock to protect against race conditions.
46 self._lock = Lock()
47
showard6eafb492010-01-15 20:29:06 +000048
Dan Shic07b8932014-12-11 15:22:30 -080049 @property
50 def ip(self):
51 """@return IP address of the host.
52 """
53 if not self._ip:
54 self._ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
55 return self._ip
56
57
Fang Deng96667ca2013-08-01 17:46:18 -070058 def make_ssh_command(self, user="root", port=22, opts='',
59 hosts_file='/dev/null',
60 connect_timeout=30, alive_interval=300):
61 base_command = ("/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no "
62 "-o UserKnownHostsFile=%s -o BatchMode=yes "
63 "-o ConnectTimeout=%d -o ServerAliveInterval=%d "
64 "-l %s -p %d")
65 assert isinstance(connect_timeout, (int, long))
66 assert connect_timeout > 0 # can't disable the timeout
67 return base_command % (opts, hosts_file, connect_timeout,
68 alive_interval, user, port)
69
70
showard6eafb492010-01-15 20:29:06 +000071 def use_rsync(self):
72 if self._use_rsync is not None:
73 return self._use_rsync
74
mblighc9892c02010-01-06 19:02:16 +000075 # Check if rsync is available on the remote host. If it's not,
76 # don't try to use it for any future file transfers.
showard6eafb492010-01-15 20:29:06 +000077 self._use_rsync = self._check_rsync()
78 if not self._use_rsync:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -070079 logging.warning("rsync not available on remote host %s -- disabled",
mblighc9892c02010-01-06 19:02:16 +000080 self.hostname)
Eric Lie0493a42010-11-15 13:05:43 -080081 return self._use_rsync
mblighc9892c02010-01-06 19:02:16 +000082
83
84 def _check_rsync(self):
85 """
86 Check if rsync is available on the remote host.
87 """
88 try:
89 self.run("rsync --version", stdout_tee=None, stderr_tee=None)
90 except error.AutoservRunError:
91 return False
92 return True
93
jadmanskica7da372008-10-21 16:26:52 +000094
showard56176ec2009-10-28 19:52:30 +000095 def _encode_remote_paths(self, paths, escape=True):
mblighbc9402b2009-12-29 01:15:34 +000096 """
97 Given a list of file paths, encodes it as a single remote path, in
98 the style used by rsync and scp.
99 """
showard56176ec2009-10-28 19:52:30 +0000100 if escape:
101 paths = [utils.scp_remote_escape(path) for path in paths]
102 return '%s@%s:"%s"' % (self.user, self.hostname, " ".join(paths))
jadmanskica7da372008-10-21 16:26:52 +0000103
jadmanskica7da372008-10-21 16:26:52 +0000104
mbligh45561782009-05-11 21:14:34 +0000105 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks):
mblighbc9402b2009-12-29 01:15:34 +0000106 """
107 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +0000108 appropriate rsync command for copying them. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +0000109 pre-encoded.
110 """
Fang Deng96667ca2013-08-01 17:46:18 -0700111 ssh_cmd = self.make_ssh_command(user=self.user, port=self.port,
112 opts=self.master_ssh_option,
113 hosts_file=self.known_hosts_file)
jadmanskid7b79ed2009-01-07 17:19:48 +0000114 if delete_dest:
115 delete_flag = "--delete"
116 else:
117 delete_flag = ""
mbligh45561782009-05-11 21:14:34 +0000118 if preserve_symlinks:
119 symlink_flag = ""
120 else:
121 symlink_flag = "-L"
Dan Shi06d7fbf2014-02-12 12:34:41 -0800122 command = ("rsync %s %s --timeout=1800 --rsh='%s' -az --no-o --no-g "
David Hendricksb8904182014-06-02 15:22:49 -0700123 "%s \"%s\"")
mbligh45561782009-05-11 21:14:34 +0000124 return command % (symlink_flag, delete_flag, ssh_cmd,
David Hendricksb8904182014-06-02 15:22:49 -0700125 " ".join(['"%s"' % p for p in sources]), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000126
127
Eric Li861b2d52011-02-04 14:50:35 -0800128 def _make_ssh_cmd(self, cmd):
129 """
130 Create a base ssh command string for the host which can be used
131 to run commands directly on the machine
132 """
Fang Deng96667ca2013-08-01 17:46:18 -0700133 base_cmd = self.make_ssh_command(user=self.user, port=self.port,
134 opts=self.master_ssh_option,
135 hosts_file=self.known_hosts_file)
Eric Li861b2d52011-02-04 14:50:35 -0800136
137 return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd))
138
jadmanskid7b79ed2009-01-07 17:19:48 +0000139 def _make_scp_cmd(self, sources, dest):
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 scp command for encoding it. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +0000143 pre-encoded.
144 """
mblighc0649d62010-01-15 18:15:58 +0000145 command = ("scp -rq %s -o StrictHostKeyChecking=no "
lmraf676f32010-02-04 03:36:26 +0000146 "-o UserKnownHostsFile=%s -P %d %s '%s'")
Fang Deng3af66202013-08-16 15:19:25 -0700147 return command % (self.master_ssh_option, self.known_hosts_file,
mblighefccc1b2010-01-11 19:08:42 +0000148 self.port, " ".join(sources), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000149
150
151 def _make_rsync_compatible_globs(self, path, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000152 """
153 Given an rsync-style path, returns a list of globbed paths
jadmanskid7b79ed2009-01-07 17:19:48 +0000154 that will hopefully provide equivalent behaviour for scp. Does not
155 support the full range of rsync pattern matching behaviour, only that
156 exposed in the get/send_file interface (trailing slashes).
157
158 The is_local param is flag indicating if the paths should be
mblighbc9402b2009-12-29 01:15:34 +0000159 interpreted as local or remote paths.
160 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000161
162 # non-trailing slash paths should just work
163 if len(path) == 0 or path[-1] != "/":
164 return [path]
165
166 # make a function to test if a pattern matches any files
167 if is_local:
showard56176ec2009-10-28 19:52:30 +0000168 def glob_matches_files(path, pattern):
169 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +0000170 else:
showard56176ec2009-10-28 19:52:30 +0000171 def glob_matches_files(path, pattern):
172 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
173 pattern),
174 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +0000175 return result.exit_status == 0
176
177 # take a set of globs that cover all files, and see which are needed
178 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +0000179 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +0000180
181 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000182 if is_local:
showard56176ec2009-10-28 19:52:30 +0000183 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
184 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000185 else:
showard56176ec2009-10-28 19:52:30 +0000186 return [utils.scp_remote_escape(path) + pattern
187 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000188
189
190 def _make_rsync_compatible_source(self, source, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000191 """
192 Applies the same logic as _make_rsync_compatible_globs, but
jadmanskid7b79ed2009-01-07 17:19:48 +0000193 applies it to an entire list of sources, producing a new list of
mblighbc9402b2009-12-29 01:15:34 +0000194 sources, properly quoted.
195 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000196 return sum((self._make_rsync_compatible_globs(path, is_local)
197 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000198
199
mblighfeac010c52009-04-28 18:31:12 +0000200 def _set_umask_perms(self, dest):
mblighbc9402b2009-12-29 01:15:34 +0000201 """
202 Given a destination file/dir (recursively) set the permissions on
203 all the files and directories to the max allowed by running umask.
204 """
mblighfeac010c52009-04-28 18:31:12 +0000205
206 # now this looks strange but I haven't found a way in Python to _just_
207 # get the umask, apparently the only option is to try to set it
208 umask = os.umask(0)
209 os.umask(umask)
210
211 max_privs = 0777 & ~umask
212
213 def set_file_privs(filename):
Chris Masone567d0d92011-12-19 09:38:30 -0800214 """Sets mode of |filename|. Assumes |filename| exists."""
215 file_stat = os.stat(filename)
mblighfeac010c52009-04-28 18:31:12 +0000216
217 file_privs = max_privs
218 # if the original file permissions do not have at least one
219 # executable bit then do not set it anywhere
220 if not file_stat.st_mode & 0111:
221 file_privs &= ~0111
222
223 os.chmod(filename, file_privs)
224
225 # try a bottom-up walk so changes on directory permissions won't cut
226 # our access to the files/directories inside it
227 for root, dirs, files in os.walk(dest, topdown=False):
228 # when setting the privileges we emulate the chmod "X" behaviour
229 # that sets to execute only if it is a directory or any of the
230 # owner/group/other already has execute right
231 for dirname in dirs:
232 os.chmod(os.path.join(root, dirname), max_privs)
233
Chris Masone567d0d92011-12-19 09:38:30 -0800234 # Filter out broken symlinks as we go.
235 for filename in filter(os.path.exists, files):
mblighfeac010c52009-04-28 18:31:12 +0000236 set_file_privs(os.path.join(root, filename))
237
238
239 # now set privs for the dest itself
240 if os.path.isdir(dest):
241 os.chmod(dest, max_privs)
242 else:
243 set_file_privs(dest)
244
245
mbligh45561782009-05-11 21:14:34 +0000246 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
247 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000248 """
249 Copy files from the remote host to a local path.
250
251 Directories will be copied recursively.
252 If a source component is a directory with a trailing slash,
253 the content of the directory will be copied, otherwise, the
254 directory itself and its content will be copied. This
255 behavior is similar to that of the program 'rsync'.
256
257 Args:
258 source: either
259 1) a single file or directory, as a string
260 2) a list of one or more (possibly mixed)
261 files or directories
262 dest: a file or a directory (if source contains a
263 directory or more than one element, you must
264 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000265 delete_dest: if this is true, the command will also clear
266 out any old files at dest that are not in the
267 source
mblighfeac010c52009-04-28 18:31:12 +0000268 preserve_perm: tells get_file() to try to preserve the sources
269 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000270 preserve_symlinks: try to preserve symlinks instead of
271 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000272
273 Raises:
274 AutoservRunError: the scp command failed
275 """
Simran Basi882f15b2013-10-29 14:59:34 -0700276 logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,'
277 'preserve_perm: %s, preserve_symlinks:%s', source, dest,
278 delete_dest, preserve_perm, preserve_symlinks)
mblighefccc1b2010-01-11 19:08:42 +0000279 # Start a master SSH connection if necessary.
280 self.start_master_ssh()
281
jadmanskica7da372008-10-21 16:26:52 +0000282 if isinstance(source, basestring):
283 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000284 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000285
mblighc9892c02010-01-06 19:02:16 +0000286 # If rsync is disabled or fails, try scp.
showard6eafb492010-01-15 20:29:06 +0000287 try_scp = True
288 if self.use_rsync():
Simran Basi882f15b2013-10-29 14:59:34 -0700289 logging.debug('Using Rsync.')
mblighc9892c02010-01-06 19:02:16 +0000290 try:
291 remote_source = self._encode_remote_paths(source)
292 local_dest = utils.sh_escape(dest)
293 rsync = self._make_rsync_cmd([remote_source], local_dest,
294 delete_dest, preserve_symlinks)
295 utils.run(rsync)
showard6eafb492010-01-15 20:29:06 +0000296 try_scp = False
mblighc9892c02010-01-06 19:02:16 +0000297 except error.CmdError, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700298 logging.warning("trying scp, rsync failed: %s", e)
mblighc9892c02010-01-06 19:02:16 +0000299
300 if try_scp:
Simran Basi882f15b2013-10-29 14:59:34 -0700301 logging.debug('Trying scp.')
jadmanskid7b79ed2009-01-07 17:19:48 +0000302 # scp has no equivalent to --delete, just drop the entire dest dir
303 if delete_dest and os.path.isdir(dest):
304 shutil.rmtree(dest)
305 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000306
jadmanskid7b79ed2009-01-07 17:19:48 +0000307 remote_source = self._make_rsync_compatible_source(source, False)
308 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000309 # _make_rsync_compatible_source() already did the escaping
310 remote_source = self._encode_remote_paths(remote_source,
311 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000312 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000313 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000314 try:
315 utils.run(scp)
316 except error.CmdError, e:
Simran Basi882f15b2013-10-29 14:59:34 -0700317 logging.debug('scp failed: %s', e)
jadmanskid7b79ed2009-01-07 17:19:48 +0000318 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000319
mblighfeac010c52009-04-28 18:31:12 +0000320 if not preserve_perm:
321 # we have no way to tell scp to not try to preserve the
322 # permissions so set them after copy instead.
323 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
324 # options are only in very recent rsync versions
325 self._set_umask_perms(dest)
326
jadmanskica7da372008-10-21 16:26:52 +0000327
mbligh45561782009-05-11 21:14:34 +0000328 def send_file(self, source, dest, delete_dest=False,
329 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000330 """
331 Copy files from a local path to the remote host.
332
333 Directories will be copied recursively.
334 If a source component is a directory with a trailing slash,
335 the content of the directory will be copied, otherwise, the
336 directory itself and its content will be copied. This
337 behavior is similar to that of the program 'rsync'.
338
339 Args:
340 source: either
341 1) a single file or directory, as a string
342 2) a list of one or more (possibly mixed)
343 files or directories
344 dest: a file or a directory (if source contains a
345 directory or more than one element, you must
346 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000347 delete_dest: if this is true, the command will also clear
348 out any old files at dest that are not in the
349 source
mbligh45561782009-05-11 21:14:34 +0000350 preserve_symlinks: controls if symlinks on the source will be
351 copied as such on the destination or transformed into the
352 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000353
354 Raises:
355 AutoservRunError: the scp command failed
356 """
Simran Basi882f15b2013-10-29 14:59:34 -0700357 logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,'
358 'preserve_symlinks:%s', source, dest,
359 delete_dest, preserve_symlinks)
mblighefccc1b2010-01-11 19:08:42 +0000360 # Start a master SSH connection if necessary.
361 self.start_master_ssh()
362
jadmanskica7da372008-10-21 16:26:52 +0000363 if isinstance(source, basestring):
364 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000365 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000366
mblighc9892c02010-01-06 19:02:16 +0000367 # If rsync is disabled or fails, try scp.
showard6eafb492010-01-15 20:29:06 +0000368 try_scp = True
369 if self.use_rsync():
Simran Basi882f15b2013-10-29 14:59:34 -0700370 logging.debug('Using Rsync.')
mblighc9892c02010-01-06 19:02:16 +0000371 try:
372 local_sources = [utils.sh_escape(path) for path in source]
373 rsync = self._make_rsync_cmd(local_sources, remote_dest,
374 delete_dest, preserve_symlinks)
375 utils.run(rsync)
showard6eafb492010-01-15 20:29:06 +0000376 try_scp = False
mblighc9892c02010-01-06 19:02:16 +0000377 except error.CmdError, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700378 logging.warning("trying scp, rsync failed: %s", e)
mblighc9892c02010-01-06 19:02:16 +0000379
380 if try_scp:
Simran Basi882f15b2013-10-29 14:59:34 -0700381 logging.debug('Trying scp.')
jadmanskid7b79ed2009-01-07 17:19:48 +0000382 # scp has no equivalent to --delete, just drop the entire dest dir
383 if delete_dest:
showard27160152009-07-15 14:28:42 +0000384 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000385 ignore_status=True).exit_status == 0
386 if is_dir:
387 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000388 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000389 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000390
jadmanski2583a432009-02-10 23:59:11 +0000391 local_sources = self._make_rsync_compatible_source(source, True)
392 if local_sources:
393 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000394 try:
395 utils.run(scp)
396 except error.CmdError, e:
Simran Basi882f15b2013-10-29 14:59:34 -0700397 logging.debug('scp failed: %s', e)
jadmanskid7b79ed2009-01-07 17:19:48 +0000398 raise error.AutoservRunError(e.args[0], e.args[1])
399
jadmanskica7da372008-10-21 16:26:52 +0000400
beeps46dadc92013-11-07 14:07:10 -0800401 def ssh_ping(self, timeout=60, base_cmd='true'):
beepsadd66d32013-03-04 17:21:51 -0800402 """
403 Pings remote host via ssh.
404
405 @param timeout: Time in seconds before giving up.
406 Defaults to 60 seconds.
beeps46dadc92013-11-07 14:07:10 -0800407 @param base_cmd: The base command to run with the ssh ping.
408 Defaults to true.
beepsadd66d32013-03-04 17:21:51 -0800409 @raise AutoservSSHTimeout: If the ssh ping times out.
410 @raise AutoservSshPermissionDeniedError: If ssh ping fails due to
411 permissions.
412 @raise AutoservSshPingHostError: For other AutoservRunErrors.
413 """
jadmanskica7da372008-10-21 16:26:52 +0000414 try:
beeps46dadc92013-11-07 14:07:10 -0800415 self.run(base_cmd, timeout=timeout, connect_timeout=timeout)
jadmanskica7da372008-10-21 16:26:52 +0000416 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000417 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000418 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000419 except error.AutoservSshPermissionDeniedError:
420 #let AutoservSshPermissionDeniedError be visible to the callers
421 raise
jadmanskica7da372008-10-21 16:26:52 +0000422 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000423 # convert the generic AutoservRunError into something more
424 # specific for this context
425 raise error.AutoservSshPingHostError(e.description + '\n' +
426 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000427
428
beeps46dadc92013-11-07 14:07:10 -0800429 def is_up(self, timeout=60, base_cmd='true'):
jadmanskica7da372008-10-21 16:26:52 +0000430 """
beeps46dadc92013-11-07 14:07:10 -0800431 Check if the remote host is up by ssh-ing and running a base command.
jadmanskica7da372008-10-21 16:26:52 +0000432
beepsadd66d32013-03-04 17:21:51 -0800433 @param timeout: timeout in seconds.
beeps46dadc92013-11-07 14:07:10 -0800434 @param base_cmd: a base command to run with ssh. The default is 'true'.
beepsadd66d32013-03-04 17:21:51 -0800435 @returns True if the remote host is up before the timeout expires,
436 False otherwise.
jadmanskica7da372008-10-21 16:26:52 +0000437 """
438 try:
beeps46dadc92013-11-07 14:07:10 -0800439 self.ssh_ping(timeout=timeout, base_cmd=base_cmd)
jadmanskica7da372008-10-21 16:26:52 +0000440 except error.AutoservError:
441 return False
442 else:
443 return True
444
445
446 def wait_up(self, timeout=None):
447 """
448 Wait until the remote host is up or the timeout expires.
449
450 In fact, it will wait until an ssh connection to the remote
451 host can be established, and getty is running.
452
jadmanskic0354912010-01-12 15:57:29 +0000453 @param timeout time limit in seconds before returning even
454 if the host is not up.
jadmanskica7da372008-10-21 16:26:52 +0000455
beepsadd66d32013-03-04 17:21:51 -0800456 @returns True if the host was found to be up before the timeout expires,
457 False otherwise
jadmanskica7da372008-10-21 16:26:52 +0000458 """
459 if timeout:
beeps46dadc92013-11-07 14:07:10 -0800460 current_time = int(time.time())
461 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000462
beepsadd66d32013-03-04 17:21:51 -0800463 while not timeout or current_time < end_time:
464 if self.is_up(timeout=end_time - current_time):
jadmanskica7da372008-10-21 16:26:52 +0000465 try:
466 if self.are_wait_up_processes_up():
jadmanski7ebac3d2010-06-17 16:06:31 +0000467 logging.debug('Host %s is now up', self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000468 return True
469 except error.AutoservError:
470 pass
471 time.sleep(1)
beeps46dadc92013-11-07 14:07:10 -0800472 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000473
jadmanski7ebac3d2010-06-17 16:06:31 +0000474 logging.debug('Host %s is still down after waiting %d seconds',
475 self.hostname, int(timeout + time.time() - end_time))
jadmanskica7da372008-10-21 16:26:52 +0000476 return False
477
478
jadmanskic0354912010-01-12 15:57:29 +0000479 def wait_down(self, timeout=None, warning_timer=None, old_boot_id=None):
jadmanskica7da372008-10-21 16:26:52 +0000480 """
481 Wait until the remote host is down or the timeout expires.
482
jadmanskic0354912010-01-12 15:57:29 +0000483 If old_boot_id is provided, this will wait until either the machine
484 is unpingable or self.get_boot_id() returns a value different from
485 old_boot_id. If the boot_id value has changed then the function
486 returns true under the assumption that the machine has shut down
487 and has now already come back up.
jadmanskica7da372008-10-21 16:26:52 +0000488
jadmanskic0354912010-01-12 15:57:29 +0000489 If old_boot_id is None then until the machine becomes unreachable the
490 method assumes the machine has not yet shut down.
jadmanskica7da372008-10-21 16:26:52 +0000491
beepsadd66d32013-03-04 17:21:51 -0800492 Based on this definition, the 4 possible permutations of timeout
493 and old_boot_id are:
494 1. timeout and old_boot_id: wait timeout seconds for either the
495 host to become unpingable, or the boot id
496 to change. In the latter case we've rebooted
497 and in the former case we've only shutdown,
498 but both cases return True.
499 2. only timeout: wait timeout seconds for the host to become unpingable.
500 If the host remains pingable throughout timeout seconds
501 we return False.
502 3. only old_boot_id: wait forever until either the host becomes
503 unpingable or the boot_id changes. Return true
504 when either of those conditions are met.
505 4. not timeout, not old_boot_id: wait forever till the host becomes
506 unpingable.
507
jadmanskic0354912010-01-12 15:57:29 +0000508 @param timeout Time limit in seconds before returning even
509 if the host is still up.
510 @param warning_timer Time limit in seconds that will generate
511 a warning if the host is not down yet.
512 @param old_boot_id A string containing the result of self.get_boot_id()
513 prior to the host being told to shut down. Can be None if this is
514 not available.
515
516 @returns True if the host was found to be down, False otherwise
jadmanskica7da372008-10-21 16:26:52 +0000517 """
mblighe5e3cf22010-05-27 23:33:14 +0000518 #TODO: there is currently no way to distinguish between knowing
519 #TODO: boot_id was unsupported and not knowing the boot_id.
beeps46dadc92013-11-07 14:07:10 -0800520 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000521 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000522 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000523
mbligh2ed998f2009-04-08 21:03:47 +0000524 if warning_timer:
525 warn_time = current_time + warning_timer
526
jadmanskic0354912010-01-12 15:57:29 +0000527 if old_boot_id is not None:
528 logging.debug('Host %s pre-shutdown boot_id is %s',
529 self.hostname, old_boot_id)
530
beepsadd66d32013-03-04 17:21:51 -0800531 # Impose semi real-time deadline constraints, since some clients
532 # (eg: watchdog timer tests) expect strict checking of time elapsed.
533 # Each iteration of this loop is treated as though it atomically
534 # completes within current_time, this is needed because if we used
535 # inline time.time() calls instead then the following could happen:
536 #
537 # while not timeout or time.time() < end_time: [23 < 30]
538 # some code. [takes 10 secs]
539 # try:
540 # new_boot_id = self.get_boot_id(timeout=end_time - time.time())
541 # [30 - 33]
542 # The last step will lead to a return True, when in fact the machine
543 # went down at 32 seconds (>30). Hence we need to pass get_boot_id
544 # the same time that allowed us into that iteration of the loop.
mbligh2ed998f2009-04-08 21:03:47 +0000545 while not timeout or current_time < end_time:
jadmanskic0354912010-01-12 15:57:29 +0000546 try:
beeps46dadc92013-11-07 14:07:10 -0800547 new_boot_id = self.get_boot_id(timeout=end_time-current_time)
mblighdbc7e4a2010-01-15 20:34:20 +0000548 except error.AutoservError:
jadmanskic0354912010-01-12 15:57:29 +0000549 logging.debug('Host %s is now unreachable over ssh, is down',
550 self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000551 return True
jadmanskic0354912010-01-12 15:57:29 +0000552 else:
553 # if the machine is up but the boot_id value has changed from
554 # old boot id, then we can assume the machine has gone down
555 # and then already come back up
556 if old_boot_id is not None and old_boot_id != new_boot_id:
557 logging.debug('Host %s now has boot_id %s and so must '
558 'have rebooted', self.hostname, new_boot_id)
559 return True
mbligh2ed998f2009-04-08 21:03:47 +0000560
561 if warning_timer and current_time > warn_time:
Scott Zawalskic86fdeb2013-10-23 10:24:04 -0400562 self.record("INFO", None, "shutdown",
mbligh2ed998f2009-04-08 21:03:47 +0000563 "Shutdown took longer than %ds" % warning_timer)
564 # Print the warning only once.
565 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000566 # If a machine is stuck switching runlevels
567 # This may cause the machine to reboot.
568 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000569
jadmanskica7da372008-10-21 16:26:52 +0000570 time.sleep(1)
beeps46dadc92013-11-07 14:07:10 -0800571 current_time = int(time.time())
jadmanskica7da372008-10-21 16:26:52 +0000572
573 return False
jadmanskif6562912008-10-21 17:59:01 +0000574
mbligha0a27592009-01-24 01:41:36 +0000575
jadmanskif6562912008-10-21 17:59:01 +0000576 # tunable constants for the verify & repair code
mblighb86bfa12010-02-12 20:22:21 +0000577 AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER",
578 "gb_diskspace_required",
Fang Deng6b05f5b2013-03-20 13:42:11 -0700579 type=float,
580 default=20.0)
mbligha0a27592009-01-24 01:41:36 +0000581
jadmanskif6562912008-10-21 17:59:01 +0000582
showardca572982009-09-18 21:20:01 +0000583 def verify_connectivity(self):
584 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000585
showardb18134f2009-03-20 20:52:18 +0000586 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000587 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000588 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000589
jadmanski80deb752009-01-21 17:14:16 +0000590 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000591 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000592
mblighb49b5232009-02-12 21:54:49 +0000593
showardca572982009-09-18 21:20:01 +0000594 def verify_software(self):
595 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000596 try:
showardad812bf2009-10-20 23:49:56 +0000597 self.check_diskspace(autotest.Autotest.get_install_dir(self),
598 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000599 except error.AutoservHostError:
600 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000601 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000602 # autotest dir may not exist, etc. ignore
603 logging.debug('autodir space check exception, this is probably '
604 'safe to ignore\n' + traceback.format_exc())
mblighefccc1b2010-01-11 19:08:42 +0000605
606
607 def close(self):
608 super(AbstractSSHHost, self).close()
609 self._cleanup_master_ssh()
Fang Deng3af66202013-08-16 15:19:25 -0700610 os.remove(self.known_hosts_file)
mblighefccc1b2010-01-11 19:08:42 +0000611
612
613 def _cleanup_master_ssh(self):
614 """
615 Release all resources (process, temporary directory) used by an active
616 master SSH connection.
617 """
618 # If a master SSH connection is running, kill it.
619 if self.master_ssh_job is not None:
Aviv Keshet46250752013-08-27 15:52:06 -0700620 logging.debug('Nuking master_ssh_job.')
mblighefccc1b2010-01-11 19:08:42 +0000621 utils.nuke_subprocess(self.master_ssh_job.sp)
622 self.master_ssh_job = None
623
624 # Remove the temporary directory for the master SSH socket.
625 if self.master_ssh_tempdir is not None:
Aviv Keshet46250752013-08-27 15:52:06 -0700626 logging.debug('Cleaning master_ssh_tempdir.')
mblighefccc1b2010-01-11 19:08:42 +0000627 self.master_ssh_tempdir.clean()
628 self.master_ssh_tempdir = None
629 self.master_ssh_option = ''
630
631
Aviv Keshet0749a822013-10-17 09:53:26 -0700632 def start_master_ssh(self, timeout=5):
mblighefccc1b2010-01-11 19:08:42 +0000633 """
634 Called whenever a slave SSH connection needs to be initiated (e.g., by
635 run, rsync, scp). If master SSH support is enabled and a master SSH
636 connection is not active already, start a new one in the background.
637 Also, cleanup any zombie master SSH connections (e.g., dead due to
638 reboot).
Aviv Keshet0749a822013-10-17 09:53:26 -0700639
640 timeout: timeout in seconds (default 5) to wait for master ssh
641 connection to be established. If timeout is reached, a
642 warning message is logged, but no other action is taken.
mblighefccc1b2010-01-11 19:08:42 +0000643 """
644 if not enable_master_ssh:
645 return
646
Simran Basi3b858a22015-03-17 16:23:24 -0700647 # Multiple processes might try in parallel to clean up the old master
648 # ssh connection and create a new one, therefore use a lock to protect
649 # against race conditions.
650 with self._lock:
651 # If a previously started master SSH connection is not running
652 # anymore, it needs to be cleaned up and then restarted.
653 if self.master_ssh_job is not None:
654 socket_path = os.path.join(self.master_ssh_tempdir.name,
655 'socket')
656 if (not os.path.exists(socket_path) or
657 self.master_ssh_job.sp.poll() is not None):
658 logging.info("Master ssh connection to %s is down.",
659 self.hostname)
660 self._cleanup_master_ssh()
mblighefccc1b2010-01-11 19:08:42 +0000661
Simran Basi3b858a22015-03-17 16:23:24 -0700662 # Start a new master SSH connection.
663 if self.master_ssh_job is None:
664 # Create a shared socket in a temp location.
665 self.master_ssh_tempdir = autotemp.tempdir(
666 unique_id='ssh-master')
667 self.master_ssh_option = ("-o ControlPath=%s/socket" %
668 self.master_ssh_tempdir.name)
mblighefccc1b2010-01-11 19:08:42 +0000669
Simran Basi3b858a22015-03-17 16:23:24 -0700670 # Start the master SSH connection in the background.
671 master_cmd = self.ssh_command(
672 options="-N -o ControlMaster=yes")
673 logging.info("Starting master ssh connection '%s'", master_cmd)
674 self.master_ssh_job = utils.BgJob(master_cmd,
675 nickname='master-ssh',
676 no_pipes=True)
677 # To prevent a race between the the master ssh connection
678 # startup and its first attempted use, wait for socket file to
679 # exist before returning.
680 end_time = time.time() + timeout
681 socket_file_path = os.path.join(self.master_ssh_tempdir.name,
682 'socket')
683 while time.time() < end_time:
684 if os.path.exists(socket_file_path):
685 break
686 time.sleep(.2)
687 else:
688 logging.info('Timed out waiting for master-ssh connection '
689 'to be established.')
mbligh0a883702010-04-21 01:58:34 +0000690
691
692 def clear_known_hosts(self):
693 """Clears out the temporary ssh known_hosts file.
694
695 This is useful if the test SSHes to the machine, then reinstalls it,
696 then SSHes to it again. It can be called after the reinstall to
697 reduce the spam in the logs.
698 """
699 logging.info("Clearing known hosts for host '%s', file '%s'.",
Fang Deng3af66202013-08-16 15:19:25 -0700700 self.hostname, self.known_hosts_file)
mbligh0a883702010-04-21 01:58:34 +0000701 # Clear out the file by opening it for writing and then closing.
Fang Deng3af66202013-08-16 15:19:25 -0700702 fh = open(self.known_hosts_file, "w")
mbligh0a883702010-04-21 01:58:34 +0000703 fh.close()
Prashanth B98509c72014-04-04 16:01:34 -0700704
705
706 def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True):
707 """Copy log directories from a host to a local directory.
708
709 @param remote_src_dir: A destination directory on the host.
710 @param local_dest_dir: A path to a local destination directory.
711 If it doesn't exist it will be created.
712 @param ignore_errors: If True, ignore exceptions.
713
714 @raises OSError: If there were problems creating the local_dest_dir and
715 ignore_errors is False.
716 @raises AutoservRunError, AutotestRunError: If something goes wrong
717 while copying the directories and ignore_errors is False.
718 """
719 locally_created_dest = False
720 if (not os.path.exists(local_dest_dir)
721 or not os.path.isdir(local_dest_dir)):
722 try:
723 os.makedirs(local_dest_dir)
724 locally_created_dest = True
725 except OSError as e:
726 logging.warning('Unable to collect logs from host '
727 '%s: %s', self.hostname, e)
728 if not ignore_errors:
729 raise
730 return
731 try:
732 self.get_file(
733 remote_src_dir, local_dest_dir, preserve_symlinks=True)
734 except (error.AutotestRunError, error.AutoservRunError,
735 error.AutoservSSHTimeout) as e:
736 logging.warning('Collection of %s to local dir %s from host %s '
737 'failed: %s', remote_src_dir, local_dest_dir,
738 self.hostname, e)
739 if locally_created_dest:
740 shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors)
741 if not ignore_errors:
742 raise