blob: ee6556d7523f52e75ebf4a6d3559bc50975f1494 [file] [log] [blame]
showardb18134f2009-03-20 20:52:18 +00001import os, time, types, socket, shutil, glob, logging
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
jadmanski2583a432009-02-10 23:59:11 +000043 def _encode_remote_paths(self, paths):
44 """ Given a list of file paths, encodes it as a single remote path, in
45 the style used by rsync and scp. """
46 escaped_paths = [utils.scp_remote_escape(path) for path in paths]
jadmanskid7b79ed2009-01-07 17:19:48 +000047 return '%s@%s:"%s"' % (self.user, self.hostname,
jadmanski2583a432009-02-10 23:59:11 +000048 " ".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:
92 def glob_matches_files(path):
93 return len(glob.glob(path)) > 0
94 else:
95 def glob_matches_files(path):
96 result = self.run("ls \"%s\"" % utils.sh_escape(path),
97 ignore_status=True)
98 return result.exit_status == 0
99
100 # take a set of globs that cover all files, and see which are needed
101 patterns = ["*", ".[!.]*"]
102 patterns = [p for p in patterns if glob_matches_files(path + p)]
103
104 # convert them into a set of paths suitable for the commandline
105 path = utils.sh_escape(path)
106 if is_local:
107 return ["\"%s\"%s" % (path, pattern) for pattern in patterns]
108 else:
109 return ["\"%s\"" % (path + pattern) for pattern in patterns]
110
111
112 def _make_rsync_compatible_source(self, source, is_local):
113 """ Applies the same logic as _make_rsync_compatible_globs, but
114 applies it to an entire list of sources, producing a new list of
115 sources, properly quoted. """
116 return sum((self._make_rsync_compatible_globs(path, is_local)
117 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000118
119
mblighfeac0102009-04-28 18:31:12 +0000120 def _set_umask_perms(self, dest):
121 """Given a destination file/dir (recursively) set the permissions on
122 all the files and directories to the max allowed by running umask."""
123
124 # now this looks strange but I haven't found a way in Python to _just_
125 # get the umask, apparently the only option is to try to set it
126 umask = os.umask(0)
127 os.umask(umask)
128
129 max_privs = 0777 & ~umask
130
131 def set_file_privs(filename):
132 file_stat = os.stat(filename)
133
134 file_privs = max_privs
135 # if the original file permissions do not have at least one
136 # executable bit then do not set it anywhere
137 if not file_stat.st_mode & 0111:
138 file_privs &= ~0111
139
140 os.chmod(filename, file_privs)
141
142 # try a bottom-up walk so changes on directory permissions won't cut
143 # our access to the files/directories inside it
144 for root, dirs, files in os.walk(dest, topdown=False):
145 # when setting the privileges we emulate the chmod "X" behaviour
146 # that sets to execute only if it is a directory or any of the
147 # owner/group/other already has execute right
148 for dirname in dirs:
149 os.chmod(os.path.join(root, dirname), max_privs)
150
151 for filename in files:
152 set_file_privs(os.path.join(root, filename))
153
154
155 # now set privs for the dest itself
156 if os.path.isdir(dest):
157 os.chmod(dest, max_privs)
158 else:
159 set_file_privs(dest)
160
161
mbligh45561782009-05-11 21:14:34 +0000162 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
163 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000164 """
165 Copy files from the remote host to a local path.
166
167 Directories will be copied recursively.
168 If a source component is a directory with a trailing slash,
169 the content of the directory will be copied, otherwise, the
170 directory itself and its content will be copied. This
171 behavior is similar to that of the program 'rsync'.
172
173 Args:
174 source: either
175 1) a single file or directory, as a string
176 2) a list of one or more (possibly mixed)
177 files or directories
178 dest: a file or a directory (if source contains a
179 directory or more than one element, you must
180 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000181 delete_dest: if this is true, the command will also clear
182 out any old files at dest that are not in the
183 source
mblighfeac0102009-04-28 18:31:12 +0000184 preserve_perm: tells get_file() to try to preserve the sources
185 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000186 preserve_symlinks: try to preserve symlinks instead of
187 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000188
189 Raises:
190 AutoservRunError: the scp command failed
191 """
192 if isinstance(source, basestring):
193 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000194 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000195
jadmanskid7b79ed2009-01-07 17:19:48 +0000196 try:
jadmanski2583a432009-02-10 23:59:11 +0000197 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000198 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000199 rsync = self._make_rsync_cmd([remote_source], local_dest,
mbligh45561782009-05-11 21:14:34 +0000200 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000201 utils.run(rsync)
202 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000203 logging.warn("warning: rsync failed with: %s", e)
204 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000205
jadmanskid7b79ed2009-01-07 17:19:48 +0000206 # scp has no equivalent to --delete, just drop the entire dest dir
207 if delete_dest and os.path.isdir(dest):
208 shutil.rmtree(dest)
209 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000210
jadmanskid7b79ed2009-01-07 17:19:48 +0000211 remote_source = self._make_rsync_compatible_source(source, False)
212 if remote_source:
jadmanski2583a432009-02-10 23:59:11 +0000213 remote_source = self._encode_remote_paths(remote_source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000214 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000215 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000216 try:
217 utils.run(scp)
218 except error.CmdError, e:
219 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000220
mblighfeac0102009-04-28 18:31:12 +0000221 if not preserve_perm:
222 # we have no way to tell scp to not try to preserve the
223 # permissions so set them after copy instead.
224 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
225 # options are only in very recent rsync versions
226 self._set_umask_perms(dest)
227
jadmanskica7da372008-10-21 16:26:52 +0000228
mbligh45561782009-05-11 21:14:34 +0000229 def send_file(self, source, dest, delete_dest=False,
230 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000231 """
232 Copy files from a local path to the remote host.
233
234 Directories will be copied recursively.
235 If a source component is a directory with a trailing slash,
236 the content of the directory will be copied, otherwise, the
237 directory itself and its content will be copied. This
238 behavior is similar to that of the program 'rsync'.
239
240 Args:
241 source: either
242 1) a single file or directory, as a string
243 2) a list of one or more (possibly mixed)
244 files or directories
245 dest: a file or a directory (if source contains a
246 directory or more than one element, you must
247 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000248 delete_dest: if this is true, the command will also clear
249 out any old files at dest that are not in the
250 source
mbligh45561782009-05-11 21:14:34 +0000251 preserve_symlinks: controls if symlinks on the source will be
252 copied as such on the destination or transformed into the
253 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000254
255 Raises:
256 AutoservRunError: the scp command failed
257 """
258 if isinstance(source, basestring):
259 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000260 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000261
jadmanskid7b79ed2009-01-07 17:19:48 +0000262 try:
jadmanski2583a432009-02-10 23:59:11 +0000263 local_sources = [utils.sh_escape(path) for path in source]
264 rsync = self._make_rsync_cmd(local_sources, remote_dest,
mbligh45561782009-05-11 21:14:34 +0000265 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000266 utils.run(rsync)
267 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000268 logging.warn("warning: rsync failed with: %s", e)
269 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000270
jadmanskid7b79ed2009-01-07 17:19:48 +0000271 # scp has no equivalent to --delete, just drop the entire dest dir
272 if delete_dest:
273 is_dir = self.run("ls -d %s/" % remote_dest,
274 ignore_status=True).exit_status == 0
275 if is_dir:
276 cmd = "rm -rf %s && mkdir %s"
277 cmd %= (remote_dest, remote_dest)
278 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000279
jadmanski2583a432009-02-10 23:59:11 +0000280 local_sources = self._make_rsync_compatible_source(source, True)
281 if local_sources:
282 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000283 try:
284 utils.run(scp)
285 except error.CmdError, e:
286 raise error.AutoservRunError(e.args[0], e.args[1])
287
jadmanskica7da372008-10-21 16:26:52 +0000288 self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest)
289 self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest)
290 if self.target_file_owner:
291 self.run('chown -R %s %s' % (self.target_file_owner, dest))
292
293
294 def ssh_ping(self, timeout=60):
295 try:
296 self.run("true", timeout=timeout, connect_timeout=timeout)
showardb18134f2009-03-20 20:52:18 +0000297 logging.info("ssh_ping of %s completed sucessfully", self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000298 except error.AutoservSSHTimeout:
299 msg = "ssh ping timed out (timeout = %d)" % timeout
300 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000301 except error.AutoservSshPermissionDeniedError:
302 #let AutoservSshPermissionDeniedError be visible to the callers
303 raise
jadmanskica7da372008-10-21 16:26:52 +0000304 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000305 # convert the generic AutoservRunError into something more
306 # specific for this context
307 raise error.AutoservSshPingHostError(e.description + '\n' +
308 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000309
310
311 def is_up(self):
312 """
313 Check if the remote host is up.
314
315 Returns:
316 True if the remote host is up, False otherwise
317 """
318 try:
319 self.ssh_ping()
320 except error.AutoservError:
321 return False
322 else:
323 return True
324
325
326 def wait_up(self, timeout=None):
327 """
328 Wait until the remote host is up or the timeout expires.
329
330 In fact, it will wait until an ssh connection to the remote
331 host can be established, and getty is running.
332
333 Args:
334 timeout: time limit in seconds before returning even
335 if the host is not up.
336
337 Returns:
338 True if the host was found to be up, False otherwise
339 """
340 if timeout:
341 end_time = time.time() + timeout
342
343 while not timeout or time.time() < end_time:
344 if self.is_up():
345 try:
346 if self.are_wait_up_processes_up():
347 return True
348 except error.AutoservError:
349 pass
350 time.sleep(1)
351
352 return False
353
354
mbligh2ed998f2009-04-08 21:03:47 +0000355 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000356 """
357 Wait until the remote host is down or the timeout expires.
358
359 In fact, it will wait until an ssh connection to the remote
360 host fails.
361
362 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000363 timeout: time limit in seconds before returning even
364 if the host is still up.
365 warning_timer: time limit in seconds that will generate
366 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000367
368 Returns:
369 True if the host was found to be down, False otherwise
370 """
mbligh2ed998f2009-04-08 21:03:47 +0000371 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000372 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000373 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000374
mbligh2ed998f2009-04-08 21:03:47 +0000375 if warning_timer:
376 warn_time = current_time + warning_timer
377
378 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000379 if not self.is_up():
380 return True
mbligh2ed998f2009-04-08 21:03:47 +0000381
382 if warning_timer and current_time > warn_time:
383 self.record("WARN", None, "shutdown",
384 "Shutdown took longer than %ds" % warning_timer)
385 # Print the warning only once.
386 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000387 # If a machine is stuck switching runlevels
388 # This may cause the machine to reboot.
389 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000390
jadmanskica7da372008-10-21 16:26:52 +0000391 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000392 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000393
394 return False
jadmanskif6562912008-10-21 17:59:01 +0000395
mbligha0a27592009-01-24 01:41:36 +0000396
jadmanskif6562912008-10-21 17:59:01 +0000397 # tunable constants for the verify & repair code
398 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000399
jadmanskif6562912008-10-21 17:59:01 +0000400
401 def verify(self):
mblighb49b5232009-02-12 21:54:49 +0000402 super(AbstractSSHHost, self).verify_hardware()
jadmanskif6562912008-10-21 17:59:01 +0000403
showardb18134f2009-03-20 20:52:18 +0000404 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000405 self.ssh_ping()
406
jadmanski80deb752009-01-21 17:14:16 +0000407 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000408 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000409
mblighb49b5232009-02-12 21:54:49 +0000410 super(AbstractSSHHost, self).verify_software()
411
jadmanskif6562912008-10-21 17:59:01 +0000412 try:
413 autodir = autotest._get_autodir(self)
414 if autodir:
jadmanskif6562912008-10-21 17:59:01 +0000415 self.check_diskspace(autodir,
416 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
417 except error.AutoservHostError:
418 raise # only want to raise if it's a space issue
419 except Exception:
420 pass # autotest dir may not exist, etc. ignore
421
422
showardf1175bb2009-06-17 19:34:36 +0000423 def _get_stream_tee_file(self, stream, level, verbose):
424 if stream is not TEE_TO_LOGS:
425 return stream
426 if not verbose:
427 return None
428 return logging_manager.LoggingFile(level=level)