blob: 61817621af12d0bb374b6b5f835e7ce016934c84 [file] [log] [blame]
showardb18134f2009-03-20 20:52:18 +00001import os, time, types, socket, shutil, glob, logging
2from autotest_lib.client.common_lib import error
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):
jadmanskica7da372008-10-21 16:26:52 +000023 """ This class represents a generic implementation of most of the
24 framework necessary for controlling a host via ssh. It implements
25 almost all of the abstract Host methods, except for the core
26 Host.run method. """
27
jadmanskif6562912008-10-21 17:59:01 +000028 def _initialize(self, hostname, user="root", port=22, password="",
29 *args, **dargs):
30 super(AbstractSSHHost, self)._initialize(hostname=hostname,
31 *args, **dargs)
mbligh6369cf22008-10-24 17:21:57 +000032 self.ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
jadmanskica7da372008-10-21 16:26:52 +000033 self.user = user
34 self.port = port
35 self.password = password
36
37
jadmanski2583a432009-02-10 23:59:11 +000038 def _encode_remote_paths(self, paths):
39 """ Given a list of file paths, encodes it as a single remote path, in
40 the style used by rsync and scp. """
41 escaped_paths = [utils.scp_remote_escape(path) for path in paths]
jadmanskid7b79ed2009-01-07 17:19:48 +000042 return '%s@%s:"%s"' % (self.user, self.hostname,
jadmanski2583a432009-02-10 23:59:11 +000043 " ".join(paths))
jadmanskica7da372008-10-21 16:26:52 +000044
jadmanskica7da372008-10-21 16:26:52 +000045
jadmanskid7b79ed2009-01-07 17:19:48 +000046 def _make_rsync_cmd(self, sources, dest, delete_dest):
47 """ Given a list of source paths and a destination path, produces the
48 appropriate rsync command for copying them. Remote paths must be
49 pre-encoded. """
50 ssh_cmd = make_ssh_command(self.user, self.port)
51 if delete_dest:
52 delete_flag = "--delete"
53 else:
54 delete_flag = ""
jadmanskia10c6602009-04-01 18:23:43 +000055 command = "rsync -L %s --timeout=1800 --rsh='%s' -az %s %s"
jadmanskid7b79ed2009-01-07 17:19:48 +000056 return command % (delete_flag, ssh_cmd, " ".join(sources), dest)
57
58
59 def _make_scp_cmd(self, sources, dest):
60 """ Given a list of source paths and a destination path, produces the
61 appropriate scp command for encoding it. Remote paths must be
62 pre-encoded. """
63 command = "scp -rpq -P %d %s '%s'"
64 return command % (self.port, " ".join(sources), dest)
65
66
67 def _make_rsync_compatible_globs(self, path, is_local):
68 """ Given an rsync-style path, returns a list of globbed paths
69 that will hopefully provide equivalent behaviour for scp. Does not
70 support the full range of rsync pattern matching behaviour, only that
71 exposed in the get/send_file interface (trailing slashes).
72
73 The is_local param is flag indicating if the paths should be
74 interpreted as local or remote paths. """
75
76 # non-trailing slash paths should just work
77 if len(path) == 0 or path[-1] != "/":
78 return [path]
79
80 # make a function to test if a pattern matches any files
81 if is_local:
82 def glob_matches_files(path):
83 return len(glob.glob(path)) > 0
84 else:
85 def glob_matches_files(path):
86 result = self.run("ls \"%s\"" % utils.sh_escape(path),
87 ignore_status=True)
88 return result.exit_status == 0
89
90 # take a set of globs that cover all files, and see which are needed
91 patterns = ["*", ".[!.]*"]
92 patterns = [p for p in patterns if glob_matches_files(path + p)]
93
94 # convert them into a set of paths suitable for the commandline
95 path = utils.sh_escape(path)
96 if is_local:
97 return ["\"%s\"%s" % (path, pattern) for pattern in patterns]
98 else:
99 return ["\"%s\"" % (path + pattern) for pattern in patterns]
100
101
102 def _make_rsync_compatible_source(self, source, is_local):
103 """ Applies the same logic as _make_rsync_compatible_globs, but
104 applies it to an entire list of sources, producing a new list of
105 sources, properly quoted. """
106 return sum((self._make_rsync_compatible_globs(path, is_local)
107 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000108
109
mblighfeac010c52009-04-28 18:31:12 +0000110 def _set_umask_perms(self, dest):
111 """Given a destination file/dir (recursively) set the permissions on
112 all the files and directories to the max allowed by running umask."""
113
114 # now this looks strange but I haven't found a way in Python to _just_
115 # get the umask, apparently the only option is to try to set it
116 umask = os.umask(0)
117 os.umask(umask)
118
119 max_privs = 0777 & ~umask
120
121 def set_file_privs(filename):
122 file_stat = os.stat(filename)
123
124 file_privs = max_privs
125 # if the original file permissions do not have at least one
126 # executable bit then do not set it anywhere
127 if not file_stat.st_mode & 0111:
128 file_privs &= ~0111
129
130 os.chmod(filename, file_privs)
131
132 # try a bottom-up walk so changes on directory permissions won't cut
133 # our access to the files/directories inside it
134 for root, dirs, files in os.walk(dest, topdown=False):
135 # when setting the privileges we emulate the chmod "X" behaviour
136 # that sets to execute only if it is a directory or any of the
137 # owner/group/other already has execute right
138 for dirname in dirs:
139 os.chmod(os.path.join(root, dirname), max_privs)
140
141 for filename in files:
142 set_file_privs(os.path.join(root, filename))
143
144
145 # now set privs for the dest itself
146 if os.path.isdir(dest):
147 os.chmod(dest, max_privs)
148 else:
149 set_file_privs(dest)
150
151
152 def get_file(self, source, dest, delete_dest=False, preserve_perm=True):
jadmanskica7da372008-10-21 16:26:52 +0000153 """
154 Copy files from the remote host to a local path.
155
156 Directories will be copied recursively.
157 If a source component is a directory with a trailing slash,
158 the content of the directory will be copied, otherwise, the
159 directory itself and its content will be copied. This
160 behavior is similar to that of the program 'rsync'.
161
162 Args:
163 source: either
164 1) a single file or directory, as a string
165 2) a list of one or more (possibly mixed)
166 files or directories
167 dest: a file or a directory (if source contains a
168 directory or more than one element, you must
169 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000170 delete_dest: if this is true, the command will also clear
171 out any old files at dest that are not in the
172 source
mblighfeac010c52009-04-28 18:31:12 +0000173 preserve_perm: tells get_file() to try to preserve the sources
174 permissions on files and dirs
jadmanskica7da372008-10-21 16:26:52 +0000175
176 Raises:
177 AutoservRunError: the scp command failed
178 """
179 if isinstance(source, basestring):
180 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000181 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000182
jadmanskid7b79ed2009-01-07 17:19:48 +0000183 try:
jadmanski2583a432009-02-10 23:59:11 +0000184 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000185 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000186 rsync = self._make_rsync_cmd([remote_source], local_dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000187 delete_dest)
188 utils.run(rsync)
189 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000190 logging.warn("warning: rsync failed with: %s", e)
191 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000192
jadmanskid7b79ed2009-01-07 17:19:48 +0000193 # scp has no equivalent to --delete, just drop the entire dest dir
194 if delete_dest and os.path.isdir(dest):
195 shutil.rmtree(dest)
196 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000197
jadmanskid7b79ed2009-01-07 17:19:48 +0000198 remote_source = self._make_rsync_compatible_source(source, False)
199 if remote_source:
jadmanski2583a432009-02-10 23:59:11 +0000200 remote_source = self._encode_remote_paths(remote_source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000201 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000202 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000203 try:
204 utils.run(scp)
205 except error.CmdError, e:
206 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000207
mblighfeac010c52009-04-28 18:31:12 +0000208 if not preserve_perm:
209 # we have no way to tell scp to not try to preserve the
210 # permissions so set them after copy instead.
211 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
212 # options are only in very recent rsync versions
213 self._set_umask_perms(dest)
214
jadmanskica7da372008-10-21 16:26:52 +0000215
mbligh89e258d2008-10-24 13:58:08 +0000216 def send_file(self, source, dest, delete_dest=False):
jadmanskica7da372008-10-21 16:26:52 +0000217 """
218 Copy files from a local path to the remote host.
219
220 Directories will be copied recursively.
221 If a source component is a directory with a trailing slash,
222 the content of the directory will be copied, otherwise, the
223 directory itself and its content will be copied. This
224 behavior is similar to that of the program 'rsync'.
225
226 Args:
227 source: either
228 1) a single file or directory, as a string
229 2) a list of one or more (possibly mixed)
230 files or directories
231 dest: a file or a directory (if source contains a
232 directory or more than one element, you must
233 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000234 delete_dest: if this is true, the command will also clear
235 out any old files at dest that are not in the
236 source
jadmanskica7da372008-10-21 16:26:52 +0000237
238 Raises:
239 AutoservRunError: the scp command failed
240 """
241 if isinstance(source, basestring):
242 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000243 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000244
jadmanskid7b79ed2009-01-07 17:19:48 +0000245 try:
jadmanski2583a432009-02-10 23:59:11 +0000246 local_sources = [utils.sh_escape(path) for path in source]
247 rsync = self._make_rsync_cmd(local_sources, remote_dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000248 delete_dest)
249 utils.run(rsync)
250 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000251 logging.warn("warning: rsync failed with: %s", e)
252 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000253
jadmanskid7b79ed2009-01-07 17:19:48 +0000254 # scp has no equivalent to --delete, just drop the entire dest dir
255 if delete_dest:
256 is_dir = self.run("ls -d %s/" % remote_dest,
257 ignore_status=True).exit_status == 0
258 if is_dir:
259 cmd = "rm -rf %s && mkdir %s"
260 cmd %= (remote_dest, remote_dest)
261 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000262
jadmanski2583a432009-02-10 23:59:11 +0000263 local_sources = self._make_rsync_compatible_source(source, True)
264 if local_sources:
265 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000266 try:
267 utils.run(scp)
268 except error.CmdError, e:
269 raise error.AutoservRunError(e.args[0], e.args[1])
270
jadmanskica7da372008-10-21 16:26:52 +0000271 self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest)
272 self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest)
273 if self.target_file_owner:
274 self.run('chown -R %s %s' % (self.target_file_owner, dest))
275
276
277 def ssh_ping(self, timeout=60):
278 try:
279 self.run("true", timeout=timeout, connect_timeout=timeout)
showardb18134f2009-03-20 20:52:18 +0000280 logging.info("ssh_ping of %s completed sucessfully", self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000281 except error.AutoservSSHTimeout:
282 msg = "ssh ping timed out (timeout = %d)" % timeout
283 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000284 except error.AutoservSshPermissionDeniedError:
285 #let AutoservSshPermissionDeniedError be visible to the callers
286 raise
jadmanskica7da372008-10-21 16:26:52 +0000287 except error.AutoservRunError, e:
288 msg = "command true failed in ssh ping"
289 raise error.AutoservRunError(msg, e.result_obj)
290
291
292 def is_up(self):
293 """
294 Check if the remote host is up.
295
296 Returns:
297 True if the remote host is up, False otherwise
298 """
299 try:
300 self.ssh_ping()
301 except error.AutoservError:
302 return False
303 else:
304 return True
305
306
307 def wait_up(self, timeout=None):
308 """
309 Wait until the remote host is up or the timeout expires.
310
311 In fact, it will wait until an ssh connection to the remote
312 host can be established, and getty is running.
313
314 Args:
315 timeout: time limit in seconds before returning even
316 if the host is not up.
317
318 Returns:
319 True if the host was found to be up, False otherwise
320 """
321 if timeout:
322 end_time = time.time() + timeout
323
324 while not timeout or time.time() < end_time:
325 if self.is_up():
326 try:
327 if self.are_wait_up_processes_up():
328 return True
329 except error.AutoservError:
330 pass
331 time.sleep(1)
332
333 return False
334
335
mbligh2ed998f2009-04-08 21:03:47 +0000336 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000337 """
338 Wait until the remote host is down or the timeout expires.
339
340 In fact, it will wait until an ssh connection to the remote
341 host fails.
342
343 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000344 timeout: time limit in seconds before returning even
345 if the host is still up.
346 warning_timer: time limit in seconds that will generate
347 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000348
349 Returns:
350 True if the host was found to be down, False otherwise
351 """
mbligh2ed998f2009-04-08 21:03:47 +0000352 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000353 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000354 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000355
mbligh2ed998f2009-04-08 21:03:47 +0000356 if warning_timer:
357 warn_time = current_time + warning_timer
358
359 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000360 if not self.is_up():
361 return True
mbligh2ed998f2009-04-08 21:03:47 +0000362
363 if warning_timer and current_time > warn_time:
364 self.record("WARN", None, "shutdown",
365 "Shutdown took longer than %ds" % warning_timer)
366 # Print the warning only once.
367 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000368 # If a machine is stuck switching runlevels
369 # This may cause the machine to reboot.
370 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000371
jadmanskica7da372008-10-21 16:26:52 +0000372 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000373 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000374
375 return False
jadmanskif6562912008-10-21 17:59:01 +0000376
mbligha0a27592009-01-24 01:41:36 +0000377
jadmanskif6562912008-10-21 17:59:01 +0000378 # tunable constants for the verify & repair code
379 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000380
jadmanskif6562912008-10-21 17:59:01 +0000381
382 def verify(self):
mblighb49b5232009-02-12 21:54:49 +0000383 super(AbstractSSHHost, self).verify_hardware()
jadmanskif6562912008-10-21 17:59:01 +0000384
showardb18134f2009-03-20 20:52:18 +0000385 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000386 self.ssh_ping()
387
jadmanski80deb752009-01-21 17:14:16 +0000388 if self.is_shutting_down():
389 raise error.AutoservHostError("Host is shutting down")
390
mblighb49b5232009-02-12 21:54:49 +0000391 super(AbstractSSHHost, self).verify_software()
392
jadmanskif6562912008-10-21 17:59:01 +0000393 try:
394 autodir = autotest._get_autodir(self)
395 if autodir:
jadmanskif6562912008-10-21 17:59:01 +0000396 self.check_diskspace(autodir,
397 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
398 except error.AutoservHostError:
399 raise # only want to raise if it's a space issue
400 except Exception:
401 pass # autotest dir may not exist, etc. ignore
402
403
showard170873e2009-01-07 00:22:26 +0000404class LoggerFile(object):
405 def write(self, data):
jadmanskifcc851b2009-01-07 17:31:12 +0000406 if data:
showardb18134f2009-03-20 20:52:18 +0000407 logging.debug(data.rstrip("\n"))
showard170873e2009-01-07 00:22:26 +0000408
409
410 def flush(self):
411 pass