blob: b14d972f2f4fc9aff6cce48151c8084dbcc72573 [file] [log] [blame]
showardca572982009-09-18 21:20:01 +00001import os, time, types, socket, shutil, glob, logging, traceback
showardf1175bb2009-06-17 19:34:36 +00002from autotest_lib.client.common_lib import error, logging_manager
jadmanski31c49b72008-10-27 20:44:48 +00003from autotest_lib.server import utils, autotest
mblighe8b93af2009-01-30 00:45:53 +00004from autotest_lib.server.hosts import remote
jadmanskica7da372008-10-21 16:26:52 +00005
6
jadmanskica7da372008-10-21 16:26:52 +00007def make_ssh_command(user="root", port=22, opts='', connect_timeout=30):
8 base_command = ("/usr/bin/ssh -a -x %s -o BatchMode=yes "
jadmanskid9365e52008-10-22 16:55:31 +00009 "-o ConnectTimeout=%d -o ServerAliveInterval=300 "
jadmanskica7da372008-10-21 16:26:52 +000010 "-l %s -p %d")
11 assert isinstance(connect_timeout, (int, long))
12 assert connect_timeout > 0 # can't disable the timeout
13 return base_command % (opts, connect_timeout, user, port)
14
15
mblighe8b93af2009-01-30 00:45:53 +000016# import site specific Host class
17SiteHost = utils.import_site_class(
18 __file__, "autotest_lib.server.hosts.site_host", "SiteHost",
19 remote.RemoteHost)
20
21
22class AbstractSSHHost(SiteHost):
mblighbc9402b2009-12-29 01:15:34 +000023 """
24 This class represents a generic implementation of most of the
jadmanskica7da372008-10-21 16:26:52 +000025 framework necessary for controlling a host via ssh. It implements
26 almost all of the abstract Host methods, except for the core
mblighbc9402b2009-12-29 01:15:34 +000027 Host.run method.
28 """
jadmanskica7da372008-10-21 16:26:52 +000029
jadmanskif6562912008-10-21 17:59:01 +000030 def _initialize(self, hostname, user="root", port=22, password="",
31 *args, **dargs):
32 super(AbstractSSHHost, self)._initialize(hostname=hostname,
33 *args, **dargs)
mbligh6369cf22008-10-24 17:21:57 +000034 self.ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
jadmanskica7da372008-10-21 16:26:52 +000035 self.user = user
36 self.port = port
37 self.password = password
38
mblighc9892c02010-01-06 19:02:16 +000039 # Check if rsync is available on the remote host. If it's not,
40 # don't try to use it for any future file transfers.
41 self.use_rsync = self._check_rsync()
42 if not self.use_rsync:
43 logging.warn("rsync not available on remote host %s -- disabled",
44 self.hostname)
45
46
47 def _check_rsync(self):
48 """
49 Check if rsync is available on the remote host.
50 """
51 try:
52 self.run("rsync --version", stdout_tee=None, stderr_tee=None)
53 except error.AutoservRunError:
54 return False
55 return True
56
jadmanskica7da372008-10-21 16:26:52 +000057
showard56176ec2009-10-28 19:52:30 +000058 def _encode_remote_paths(self, paths, escape=True):
mblighbc9402b2009-12-29 01:15:34 +000059 """
60 Given a list of file paths, encodes it as a single remote path, in
61 the style used by rsync and scp.
62 """
showard56176ec2009-10-28 19:52:30 +000063 if escape:
64 paths = [utils.scp_remote_escape(path) for path in paths]
65 return '%s@%s:"%s"' % (self.user, self.hostname, " ".join(paths))
jadmanskica7da372008-10-21 16:26:52 +000066
jadmanskica7da372008-10-21 16:26:52 +000067
mbligh45561782009-05-11 21:14:34 +000068 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks):
mblighbc9402b2009-12-29 01:15:34 +000069 """
70 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +000071 appropriate rsync command for copying them. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +000072 pre-encoded.
73 """
jadmanskid7b79ed2009-01-07 17:19:48 +000074 ssh_cmd = make_ssh_command(self.user, self.port)
75 if delete_dest:
76 delete_flag = "--delete"
77 else:
78 delete_flag = ""
mbligh45561782009-05-11 21:14:34 +000079 if preserve_symlinks:
80 symlink_flag = ""
81 else:
82 symlink_flag = "-L"
83 command = "rsync %s %s --timeout=1800 --rsh='%s' -az %s %s"
84 return command % (symlink_flag, delete_flag, ssh_cmd,
85 " ".join(sources), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +000086
87
88 def _make_scp_cmd(self, sources, dest):
mblighbc9402b2009-12-29 01:15:34 +000089 """
90 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +000091 appropriate scp command for encoding it. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +000092 pre-encoded.
93 """
mbligh29e15842009-12-23 22:02:54 +000094 command = "scp -rq -P %d %s '%s'"
jadmanskid7b79ed2009-01-07 17:19:48 +000095 return command % (self.port, " ".join(sources), dest)
96
97
98 def _make_rsync_compatible_globs(self, path, is_local):
mblighbc9402b2009-12-29 01:15:34 +000099 """
100 Given an rsync-style path, returns a list of globbed paths
jadmanskid7b79ed2009-01-07 17:19:48 +0000101 that will hopefully provide equivalent behaviour for scp. Does not
102 support the full range of rsync pattern matching behaviour, only that
103 exposed in the get/send_file interface (trailing slashes).
104
105 The is_local param is flag indicating if the paths should be
mblighbc9402b2009-12-29 01:15:34 +0000106 interpreted as local or remote paths.
107 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000108
109 # non-trailing slash paths should just work
110 if len(path) == 0 or path[-1] != "/":
111 return [path]
112
113 # make a function to test if a pattern matches any files
114 if is_local:
showard56176ec2009-10-28 19:52:30 +0000115 def glob_matches_files(path, pattern):
116 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +0000117 else:
showard56176ec2009-10-28 19:52:30 +0000118 def glob_matches_files(path, pattern):
119 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
120 pattern),
121 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +0000122 return result.exit_status == 0
123
124 # take a set of globs that cover all files, and see which are needed
125 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +0000126 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +0000127
128 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000129 if is_local:
showard56176ec2009-10-28 19:52:30 +0000130 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
131 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000132 else:
showard56176ec2009-10-28 19:52:30 +0000133 return [utils.scp_remote_escape(path) + pattern
134 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000135
136
137 def _make_rsync_compatible_source(self, source, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000138 """
139 Applies the same logic as _make_rsync_compatible_globs, but
jadmanskid7b79ed2009-01-07 17:19:48 +0000140 applies it to an entire list of sources, producing a new list of
mblighbc9402b2009-12-29 01:15:34 +0000141 sources, properly quoted.
142 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000143 return sum((self._make_rsync_compatible_globs(path, is_local)
144 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000145
146
mblighfeac0102009-04-28 18:31:12 +0000147 def _set_umask_perms(self, dest):
mblighbc9402b2009-12-29 01:15:34 +0000148 """
149 Given a destination file/dir (recursively) set the permissions on
150 all the files and directories to the max allowed by running umask.
151 """
mblighfeac0102009-04-28 18:31:12 +0000152
153 # now this looks strange but I haven't found a way in Python to _just_
154 # get the umask, apparently the only option is to try to set it
155 umask = os.umask(0)
156 os.umask(umask)
157
158 max_privs = 0777 & ~umask
159
160 def set_file_privs(filename):
161 file_stat = os.stat(filename)
162
163 file_privs = max_privs
164 # if the original file permissions do not have at least one
165 # executable bit then do not set it anywhere
166 if not file_stat.st_mode & 0111:
167 file_privs &= ~0111
168
169 os.chmod(filename, file_privs)
170
171 # try a bottom-up walk so changes on directory permissions won't cut
172 # our access to the files/directories inside it
173 for root, dirs, files in os.walk(dest, topdown=False):
174 # when setting the privileges we emulate the chmod "X" behaviour
175 # that sets to execute only if it is a directory or any of the
176 # owner/group/other already has execute right
177 for dirname in dirs:
178 os.chmod(os.path.join(root, dirname), max_privs)
179
180 for filename in files:
181 set_file_privs(os.path.join(root, filename))
182
183
184 # now set privs for the dest itself
185 if os.path.isdir(dest):
186 os.chmod(dest, max_privs)
187 else:
188 set_file_privs(dest)
189
190
mbligh45561782009-05-11 21:14:34 +0000191 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
192 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000193 """
194 Copy files from the remote host to a local path.
195
196 Directories will be copied recursively.
197 If a source component is a directory with a trailing slash,
198 the content of the directory will be copied, otherwise, the
199 directory itself and its content will be copied. This
200 behavior is similar to that of the program 'rsync'.
201
202 Args:
203 source: either
204 1) a single file or directory, as a string
205 2) a list of one or more (possibly mixed)
206 files or directories
207 dest: a file or a directory (if source contains a
208 directory or more than one element, you must
209 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000210 delete_dest: if this is true, the command will also clear
211 out any old files at dest that are not in the
212 source
mblighfeac0102009-04-28 18:31:12 +0000213 preserve_perm: tells get_file() to try to preserve the sources
214 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000215 preserve_symlinks: try to preserve symlinks instead of
216 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000217
218 Raises:
219 AutoservRunError: the scp command failed
220 """
221 if isinstance(source, basestring):
222 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000223 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000224
mblighc9892c02010-01-06 19:02:16 +0000225 # If rsync is disabled or fails, try scp.
226 try_scp = not self.use_rsync
227 if self.use_rsync:
228 try:
229 remote_source = self._encode_remote_paths(source)
230 local_dest = utils.sh_escape(dest)
231 rsync = self._make_rsync_cmd([remote_source], local_dest,
232 delete_dest, preserve_symlinks)
233 utils.run(rsync)
234 except error.CmdError, e:
235 logging.warn("trying scp, rsync failed: %s" % e)
236 try_scp = True
237
238 if try_scp:
jadmanskid7b79ed2009-01-07 17:19:48 +0000239 # scp has no equivalent to --delete, just drop the entire dest dir
240 if delete_dest and os.path.isdir(dest):
241 shutil.rmtree(dest)
242 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000243
jadmanskid7b79ed2009-01-07 17:19:48 +0000244 remote_source = self._make_rsync_compatible_source(source, False)
245 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000246 # _make_rsync_compatible_source() already did the escaping
247 remote_source = self._encode_remote_paths(remote_source,
248 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000249 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000250 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000251 try:
252 utils.run(scp)
253 except error.CmdError, e:
254 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000255
mblighfeac0102009-04-28 18:31:12 +0000256 if not preserve_perm:
257 # we have no way to tell scp to not try to preserve the
258 # permissions so set them after copy instead.
259 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
260 # options are only in very recent rsync versions
261 self._set_umask_perms(dest)
262
jadmanskica7da372008-10-21 16:26:52 +0000263
mbligh45561782009-05-11 21:14:34 +0000264 def send_file(self, source, dest, delete_dest=False,
265 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000266 """
267 Copy files from a local path to the remote host.
268
269 Directories will be copied recursively.
270 If a source component is a directory with a trailing slash,
271 the content of the directory will be copied, otherwise, the
272 directory itself and its content will be copied. This
273 behavior is similar to that of the program 'rsync'.
274
275 Args:
276 source: either
277 1) a single file or directory, as a string
278 2) a list of one or more (possibly mixed)
279 files or directories
280 dest: a file or a directory (if source contains a
281 directory or more than one element, you must
282 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000283 delete_dest: if this is true, the command will also clear
284 out any old files at dest that are not in the
285 source
mbligh45561782009-05-11 21:14:34 +0000286 preserve_symlinks: controls if symlinks on the source will be
287 copied as such on the destination or transformed into the
288 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000289
290 Raises:
291 AutoservRunError: the scp command failed
292 """
293 if isinstance(source, basestring):
294 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000295 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000296
mblighc9892c02010-01-06 19:02:16 +0000297 # If rsync is disabled or fails, try scp.
298 try_scp = not self.use_rsync
299 if self.use_rsync:
300 try:
301 local_sources = [utils.sh_escape(path) for path in source]
302 rsync = self._make_rsync_cmd(local_sources, remote_dest,
303 delete_dest, preserve_symlinks)
304 utils.run(rsync)
305 except error.CmdError, e:
306 logging.warn("trying scp, rsync failed: %s" % e)
307 try_scp = True
308
309 if try_scp:
jadmanskid7b79ed2009-01-07 17:19:48 +0000310 # scp has no equivalent to --delete, just drop the entire dest dir
311 if delete_dest:
showard27160152009-07-15 14:28:42 +0000312 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000313 ignore_status=True).exit_status == 0
314 if is_dir:
315 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000316 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000317 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000318
jadmanski2583a432009-02-10 23:59:11 +0000319 local_sources = self._make_rsync_compatible_source(source, True)
320 if local_sources:
321 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000322 try:
323 utils.run(scp)
324 except error.CmdError, e:
325 raise error.AutoservRunError(e.args[0], e.args[1])
326
jadmanskica7da372008-10-21 16:26:52 +0000327
328 def ssh_ping(self, timeout=60):
329 try:
330 self.run("true", timeout=timeout, connect_timeout=timeout)
331 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000332 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000333 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000334 except error.AutoservSshPermissionDeniedError:
335 #let AutoservSshPermissionDeniedError be visible to the callers
336 raise
jadmanskica7da372008-10-21 16:26:52 +0000337 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000338 # convert the generic AutoservRunError into something more
339 # specific for this context
340 raise error.AutoservSshPingHostError(e.description + '\n' +
341 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000342
343
344 def is_up(self):
345 """
346 Check if the remote host is up.
347
348 Returns:
349 True if the remote host is up, False otherwise
350 """
351 try:
352 self.ssh_ping()
353 except error.AutoservError:
354 return False
355 else:
356 return True
357
358
359 def wait_up(self, timeout=None):
360 """
361 Wait until the remote host is up or the timeout expires.
362
363 In fact, it will wait until an ssh connection to the remote
364 host can be established, and getty is running.
365
366 Args:
367 timeout: time limit in seconds before returning even
368 if the host is not up.
369
370 Returns:
371 True if the host was found to be up, False otherwise
372 """
373 if timeout:
374 end_time = time.time() + timeout
375
376 while not timeout or time.time() < end_time:
377 if self.is_up():
378 try:
379 if self.are_wait_up_processes_up():
380 return True
381 except error.AutoservError:
382 pass
383 time.sleep(1)
384
385 return False
386
387
mbligh2ed998f2009-04-08 21:03:47 +0000388 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000389 """
390 Wait until the remote host is down or the timeout expires.
391
392 In fact, it will wait until an ssh connection to the remote
393 host fails.
394
395 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000396 timeout: time limit in seconds before returning even
397 if the host is still up.
398 warning_timer: time limit in seconds that will generate
399 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000400
401 Returns:
402 True if the host was found to be down, False otherwise
403 """
mbligh2ed998f2009-04-08 21:03:47 +0000404 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000405 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000406 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000407
mbligh2ed998f2009-04-08 21:03:47 +0000408 if warning_timer:
409 warn_time = current_time + warning_timer
410
411 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000412 if not self.is_up():
413 return True
mbligh2ed998f2009-04-08 21:03:47 +0000414
415 if warning_timer and current_time > warn_time:
416 self.record("WARN", None, "shutdown",
417 "Shutdown took longer than %ds" % warning_timer)
418 # Print the warning only once.
419 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000420 # If a machine is stuck switching runlevels
421 # This may cause the machine to reboot.
422 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000423
jadmanskica7da372008-10-21 16:26:52 +0000424 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000425 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000426
427 return False
jadmanskif6562912008-10-21 17:59:01 +0000428
mbligha0a27592009-01-24 01:41:36 +0000429
jadmanskif6562912008-10-21 17:59:01 +0000430 # tunable constants for the verify & repair code
431 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000432
jadmanskif6562912008-10-21 17:59:01 +0000433
showardca572982009-09-18 21:20:01 +0000434 def verify_connectivity(self):
435 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000436
showardb18134f2009-03-20 20:52:18 +0000437 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000438 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000439 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000440
jadmanski80deb752009-01-21 17:14:16 +0000441 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000442 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000443
mblighb49b5232009-02-12 21:54:49 +0000444
showardca572982009-09-18 21:20:01 +0000445 def verify_software(self):
446 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000447 try:
showardad812bf2009-10-20 23:49:56 +0000448 self.check_diskspace(autotest.Autotest.get_install_dir(self),
449 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000450 except error.AutoservHostError:
451 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000452 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000453 # autotest dir may not exist, etc. ignore
454 logging.debug('autodir space check exception, this is probably '
455 'safe to ignore\n' + traceback.format_exc())