blob: 411d9e9ba2c37ea9299c3bff361df4712e708fd6 [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
showardf1175bb2009-06-17 19:34:36 +000022# this constant can be passed to run() to tee stdout/stdout to the logging
23# module.
24TEE_TO_LOGS = object()
25
26
mblighe8b93af2009-01-30 00:45:53 +000027class AbstractSSHHost(SiteHost):
jadmanskica7da372008-10-21 16:26:52 +000028 """ This class represents a generic implementation of most of the
29 framework necessary for controlling a host via ssh. It implements
30 almost all of the abstract Host methods, except for the core
31 Host.run method. """
32
jadmanskif6562912008-10-21 17:59:01 +000033 def _initialize(self, hostname, user="root", port=22, password="",
34 *args, **dargs):
35 super(AbstractSSHHost, self)._initialize(hostname=hostname,
36 *args, **dargs)
mbligh6369cf22008-10-24 17:21:57 +000037 self.ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
jadmanskica7da372008-10-21 16:26:52 +000038 self.user = user
39 self.port = port
40 self.password = password
41
42
showard56176ec2009-10-28 19:52:30 +000043 def _encode_remote_paths(self, paths, escape=True):
jadmanski2583a432009-02-10 23:59:11 +000044 """ Given a list of file paths, encodes it as a single remote path, in
45 the style used by rsync and scp. """
showard56176ec2009-10-28 19:52:30 +000046 if escape:
47 paths = [utils.scp_remote_escape(path) for path in paths]
48 return '%s@%s:"%s"' % (self.user, self.hostname, " ".join(paths))
jadmanskica7da372008-10-21 16:26:52 +000049
jadmanskica7da372008-10-21 16:26:52 +000050
mbligh45561782009-05-11 21:14:34 +000051 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks):
jadmanskid7b79ed2009-01-07 17:19:48 +000052 """ Given a list of source paths and a destination path, produces the
53 appropriate rsync command for copying them. Remote paths must be
54 pre-encoded. """
55 ssh_cmd = make_ssh_command(self.user, self.port)
56 if delete_dest:
57 delete_flag = "--delete"
58 else:
59 delete_flag = ""
mbligh45561782009-05-11 21:14:34 +000060 if preserve_symlinks:
61 symlink_flag = ""
62 else:
63 symlink_flag = "-L"
64 command = "rsync %s %s --timeout=1800 --rsh='%s' -az %s %s"
65 return command % (symlink_flag, delete_flag, ssh_cmd,
66 " ".join(sources), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +000067
68
69 def _make_scp_cmd(self, sources, dest):
70 """ Given a list of source paths and a destination path, produces the
71 appropriate scp command for encoding it. Remote paths must be
72 pre-encoded. """
73 command = "scp -rpq -P %d %s '%s'"
74 return command % (self.port, " ".join(sources), dest)
75
76
77 def _make_rsync_compatible_globs(self, path, is_local):
78 """ Given an rsync-style path, returns a list of globbed paths
79 that will hopefully provide equivalent behaviour for scp. Does not
80 support the full range of rsync pattern matching behaviour, only that
81 exposed in the get/send_file interface (trailing slashes).
82
83 The is_local param is flag indicating if the paths should be
84 interpreted as local or remote paths. """
85
86 # non-trailing slash paths should just work
87 if len(path) == 0 or path[-1] != "/":
88 return [path]
89
90 # make a function to test if a pattern matches any files
91 if is_local:
showard56176ec2009-10-28 19:52:30 +000092 def glob_matches_files(path, pattern):
93 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +000094 else:
showard56176ec2009-10-28 19:52:30 +000095 def glob_matches_files(path, pattern):
96 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
97 pattern),
98 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +000099 return result.exit_status == 0
100
101 # take a set of globs that cover all files, and see which are needed
102 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +0000103 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +0000104
105 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000106 if is_local:
showard56176ec2009-10-28 19:52:30 +0000107 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
108 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000109 else:
showard56176ec2009-10-28 19:52:30 +0000110 return [utils.scp_remote_escape(path) + pattern
111 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000112
113
114 def _make_rsync_compatible_source(self, source, is_local):
115 """ Applies the same logic as _make_rsync_compatible_globs, but
116 applies it to an entire list of sources, producing a new list of
117 sources, properly quoted. """
118 return sum((self._make_rsync_compatible_globs(path, is_local)
119 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000120
121
mblighfeac0102009-04-28 18:31:12 +0000122 def _set_umask_perms(self, dest):
123 """Given a destination file/dir (recursively) set the permissions on
124 all the files and directories to the max allowed by running umask."""
125
126 # now this looks strange but I haven't found a way in Python to _just_
127 # get the umask, apparently the only option is to try to set it
128 umask = os.umask(0)
129 os.umask(umask)
130
131 max_privs = 0777 & ~umask
132
133 def set_file_privs(filename):
134 file_stat = os.stat(filename)
135
136 file_privs = max_privs
137 # if the original file permissions do not have at least one
138 # executable bit then do not set it anywhere
139 if not file_stat.st_mode & 0111:
140 file_privs &= ~0111
141
142 os.chmod(filename, file_privs)
143
144 # try a bottom-up walk so changes on directory permissions won't cut
145 # our access to the files/directories inside it
146 for root, dirs, files in os.walk(dest, topdown=False):
147 # when setting the privileges we emulate the chmod "X" behaviour
148 # that sets to execute only if it is a directory or any of the
149 # owner/group/other already has execute right
150 for dirname in dirs:
151 os.chmod(os.path.join(root, dirname), max_privs)
152
153 for filename in files:
154 set_file_privs(os.path.join(root, filename))
155
156
157 # now set privs for the dest itself
158 if os.path.isdir(dest):
159 os.chmod(dest, max_privs)
160 else:
161 set_file_privs(dest)
162
163
mbligh45561782009-05-11 21:14:34 +0000164 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
165 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000166 """
167 Copy files from the remote host to a local path.
168
169 Directories will be copied recursively.
170 If a source component is a directory with a trailing slash,
171 the content of the directory will be copied, otherwise, the
172 directory itself and its content will be copied. This
173 behavior is similar to that of the program 'rsync'.
174
175 Args:
176 source: either
177 1) a single file or directory, as a string
178 2) a list of one or more (possibly mixed)
179 files or directories
180 dest: a file or a directory (if source contains a
181 directory or more than one element, you must
182 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000183 delete_dest: if this is true, the command will also clear
184 out any old files at dest that are not in the
185 source
mblighfeac0102009-04-28 18:31:12 +0000186 preserve_perm: tells get_file() to try to preserve the sources
187 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000188 preserve_symlinks: try to preserve symlinks instead of
189 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000190
191 Raises:
192 AutoservRunError: the scp command failed
193 """
194 if isinstance(source, basestring):
195 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000196 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000197
jadmanskid7b79ed2009-01-07 17:19:48 +0000198 try:
jadmanski2583a432009-02-10 23:59:11 +0000199 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000200 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000201 rsync = self._make_rsync_cmd([remote_source], local_dest,
mbligh45561782009-05-11 21:14:34 +0000202 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000203 utils.run(rsync)
204 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000205 logging.warn("warning: rsync failed with: %s", e)
206 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000207
jadmanskid7b79ed2009-01-07 17:19:48 +0000208 # scp has no equivalent to --delete, just drop the entire dest dir
209 if delete_dest and os.path.isdir(dest):
210 shutil.rmtree(dest)
211 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000212
jadmanskid7b79ed2009-01-07 17:19:48 +0000213 remote_source = self._make_rsync_compatible_source(source, False)
214 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000215 # _make_rsync_compatible_source() already did the escaping
216 remote_source = self._encode_remote_paths(remote_source,
217 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000218 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000219 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000220 try:
221 utils.run(scp)
222 except error.CmdError, e:
223 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000224
mblighfeac0102009-04-28 18:31:12 +0000225 if not preserve_perm:
226 # we have no way to tell scp to not try to preserve the
227 # permissions so set them after copy instead.
228 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
229 # options are only in very recent rsync versions
230 self._set_umask_perms(dest)
231
jadmanskica7da372008-10-21 16:26:52 +0000232
mbligh45561782009-05-11 21:14:34 +0000233 def send_file(self, source, dest, delete_dest=False,
234 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000235 """
236 Copy files from a local path to the remote host.
237
238 Directories will be copied recursively.
239 If a source component is a directory with a trailing slash,
240 the content of the directory will be copied, otherwise, the
241 directory itself and its content will be copied. This
242 behavior is similar to that of the program 'rsync'.
243
244 Args:
245 source: either
246 1) a single file or directory, as a string
247 2) a list of one or more (possibly mixed)
248 files or directories
249 dest: a file or a directory (if source contains a
250 directory or more than one element, you must
251 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000252 delete_dest: if this is true, the command will also clear
253 out any old files at dest that are not in the
254 source
mbligh45561782009-05-11 21:14:34 +0000255 preserve_symlinks: controls if symlinks on the source will be
256 copied as such on the destination or transformed into the
257 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000258
259 Raises:
260 AutoservRunError: the scp command failed
261 """
262 if isinstance(source, basestring):
263 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000264 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000265
jadmanskid7b79ed2009-01-07 17:19:48 +0000266 try:
jadmanski2583a432009-02-10 23:59:11 +0000267 local_sources = [utils.sh_escape(path) for path in source]
268 rsync = self._make_rsync_cmd(local_sources, remote_dest,
mbligh45561782009-05-11 21:14:34 +0000269 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000270 utils.run(rsync)
271 except error.CmdError, e:
mblighd0e94982009-07-11 00:15:18 +0000272 logging.warn("Command rsync failed with: %s", e)
273 logging.info("Attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000274
jadmanskid7b79ed2009-01-07 17:19:48 +0000275 # scp has no equivalent to --delete, just drop the entire dest dir
276 if delete_dest:
showard27160152009-07-15 14:28:42 +0000277 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000278 ignore_status=True).exit_status == 0
279 if is_dir:
280 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000281 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000282 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000283
jadmanski2583a432009-02-10 23:59:11 +0000284 local_sources = self._make_rsync_compatible_source(source, True)
285 if local_sources:
286 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000287 try:
288 utils.run(scp)
289 except error.CmdError, e:
290 raise error.AutoservRunError(e.args[0], e.args[1])
291
jadmanskica7da372008-10-21 16:26:52 +0000292 self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest)
293 self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest)
294 if self.target_file_owner:
295 self.run('chown -R %s %s' % (self.target_file_owner, dest))
296
297
298 def ssh_ping(self, timeout=60):
299 try:
300 self.run("true", timeout=timeout, connect_timeout=timeout)
301 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000302 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000303 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000304 except error.AutoservSshPermissionDeniedError:
305 #let AutoservSshPermissionDeniedError be visible to the callers
306 raise
jadmanskica7da372008-10-21 16:26:52 +0000307 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000308 # convert the generic AutoservRunError into something more
309 # specific for this context
310 raise error.AutoservSshPingHostError(e.description + '\n' +
311 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000312
313
314 def is_up(self):
315 """
316 Check if the remote host is up.
317
318 Returns:
319 True if the remote host is up, False otherwise
320 """
321 try:
322 self.ssh_ping()
323 except error.AutoservError:
324 return False
325 else:
326 return True
327
328
329 def wait_up(self, timeout=None):
330 """
331 Wait until the remote host is up or the timeout expires.
332
333 In fact, it will wait until an ssh connection to the remote
334 host can be established, and getty is running.
335
336 Args:
337 timeout: time limit in seconds before returning even
338 if the host is not up.
339
340 Returns:
341 True if the host was found to be up, False otherwise
342 """
343 if timeout:
344 end_time = time.time() + timeout
345
346 while not timeout or time.time() < end_time:
347 if self.is_up():
348 try:
349 if self.are_wait_up_processes_up():
350 return True
351 except error.AutoservError:
352 pass
353 time.sleep(1)
354
355 return False
356
357
mbligh2ed998f2009-04-08 21:03:47 +0000358 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000359 """
360 Wait until the remote host is down or the timeout expires.
361
362 In fact, it will wait until an ssh connection to the remote
363 host fails.
364
365 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000366 timeout: time limit in seconds before returning even
367 if the host is still up.
368 warning_timer: time limit in seconds that will generate
369 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000370
371 Returns:
372 True if the host was found to be down, False otherwise
373 """
mbligh2ed998f2009-04-08 21:03:47 +0000374 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000375 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000376 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000377
mbligh2ed998f2009-04-08 21:03:47 +0000378 if warning_timer:
379 warn_time = current_time + warning_timer
380
381 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000382 if not self.is_up():
383 return True
mbligh2ed998f2009-04-08 21:03:47 +0000384
385 if warning_timer and current_time > warn_time:
386 self.record("WARN", None, "shutdown",
387 "Shutdown took longer than %ds" % warning_timer)
388 # Print the warning only once.
389 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000390 # If a machine is stuck switching runlevels
391 # This may cause the machine to reboot.
392 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000393
jadmanskica7da372008-10-21 16:26:52 +0000394 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000395 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000396
397 return False
jadmanskif6562912008-10-21 17:59:01 +0000398
mbligha0a27592009-01-24 01:41:36 +0000399
jadmanskif6562912008-10-21 17:59:01 +0000400 # tunable constants for the verify & repair code
401 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000402
jadmanskif6562912008-10-21 17:59:01 +0000403
showardca572982009-09-18 21:20:01 +0000404 def verify_connectivity(self):
405 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000406
showardb18134f2009-03-20 20:52:18 +0000407 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000408 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000409 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000410
jadmanski80deb752009-01-21 17:14:16 +0000411 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000412 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000413
mblighb49b5232009-02-12 21:54:49 +0000414
showardca572982009-09-18 21:20:01 +0000415 def verify_software(self):
416 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000417 try:
showardad812bf2009-10-20 23:49:56 +0000418 self.check_diskspace(autotest.Autotest.get_install_dir(self),
419 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000420 except error.AutoservHostError:
421 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000422 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000423 # autotest dir may not exist, etc. ignore
424 logging.debug('autodir space check exception, this is probably '
425 'safe to ignore\n' + traceback.format_exc())