blob: 9c41c9abf3a0505e4231ee9482ddf201775cfc97 [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
mbligh45561782009-05-11 21:14:34 +000046 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks):
jadmanskid7b79ed2009-01-07 17:19:48 +000047 """ 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 = ""
mbligh45561782009-05-11 21:14:34 +000055 if preserve_symlinks:
56 symlink_flag = ""
57 else:
58 symlink_flag = "-L"
59 command = "rsync %s %s --timeout=1800 --rsh='%s' -az %s %s"
60 return command % (symlink_flag, delete_flag, ssh_cmd,
61 " ".join(sources), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +000062
63
64 def _make_scp_cmd(self, sources, dest):
65 """ Given a list of source paths and a destination path, produces the
66 appropriate scp command for encoding it. Remote paths must be
67 pre-encoded. """
68 command = "scp -rpq -P %d %s '%s'"
69 return command % (self.port, " ".join(sources), dest)
70
71
72 def _make_rsync_compatible_globs(self, path, is_local):
73 """ Given an rsync-style path, returns a list of globbed paths
74 that will hopefully provide equivalent behaviour for scp. Does not
75 support the full range of rsync pattern matching behaviour, only that
76 exposed in the get/send_file interface (trailing slashes).
77
78 The is_local param is flag indicating if the paths should be
79 interpreted as local or remote paths. """
80
81 # non-trailing slash paths should just work
82 if len(path) == 0 or path[-1] != "/":
83 return [path]
84
85 # make a function to test if a pattern matches any files
86 if is_local:
87 def glob_matches_files(path):
88 return len(glob.glob(path)) > 0
89 else:
90 def glob_matches_files(path):
91 result = self.run("ls \"%s\"" % utils.sh_escape(path),
92 ignore_status=True)
93 return result.exit_status == 0
94
95 # take a set of globs that cover all files, and see which are needed
96 patterns = ["*", ".[!.]*"]
97 patterns = [p for p in patterns if glob_matches_files(path + p)]
98
99 # convert them into a set of paths suitable for the commandline
100 path = utils.sh_escape(path)
101 if is_local:
102 return ["\"%s\"%s" % (path, pattern) for pattern in patterns]
103 else:
104 return ["\"%s\"" % (path + pattern) for pattern in patterns]
105
106
107 def _make_rsync_compatible_source(self, source, is_local):
108 """ Applies the same logic as _make_rsync_compatible_globs, but
109 applies it to an entire list of sources, producing a new list of
110 sources, properly quoted. """
111 return sum((self._make_rsync_compatible_globs(path, is_local)
112 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000113
114
mblighfeac0102009-04-28 18:31:12 +0000115 def _set_umask_perms(self, dest):
116 """Given a destination file/dir (recursively) set the permissions on
117 all the files and directories to the max allowed by running umask."""
118
119 # now this looks strange but I haven't found a way in Python to _just_
120 # get the umask, apparently the only option is to try to set it
121 umask = os.umask(0)
122 os.umask(umask)
123
124 max_privs = 0777 & ~umask
125
126 def set_file_privs(filename):
127 file_stat = os.stat(filename)
128
129 file_privs = max_privs
130 # if the original file permissions do not have at least one
131 # executable bit then do not set it anywhere
132 if not file_stat.st_mode & 0111:
133 file_privs &= ~0111
134
135 os.chmod(filename, file_privs)
136
137 # try a bottom-up walk so changes on directory permissions won't cut
138 # our access to the files/directories inside it
139 for root, dirs, files in os.walk(dest, topdown=False):
140 # when setting the privileges we emulate the chmod "X" behaviour
141 # that sets to execute only if it is a directory or any of the
142 # owner/group/other already has execute right
143 for dirname in dirs:
144 os.chmod(os.path.join(root, dirname), max_privs)
145
146 for filename in files:
147 set_file_privs(os.path.join(root, filename))
148
149
150 # now set privs for the dest itself
151 if os.path.isdir(dest):
152 os.chmod(dest, max_privs)
153 else:
154 set_file_privs(dest)
155
156
mbligh45561782009-05-11 21:14:34 +0000157 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
158 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000159 """
160 Copy files from the remote host to a local path.
161
162 Directories will be copied recursively.
163 If a source component is a directory with a trailing slash,
164 the content of the directory will be copied, otherwise, the
165 directory itself and its content will be copied. This
166 behavior is similar to that of the program 'rsync'.
167
168 Args:
169 source: either
170 1) a single file or directory, as a string
171 2) a list of one or more (possibly mixed)
172 files or directories
173 dest: a file or a directory (if source contains a
174 directory or more than one element, you must
175 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000176 delete_dest: if this is true, the command will also clear
177 out any old files at dest that are not in the
178 source
mblighfeac0102009-04-28 18:31:12 +0000179 preserve_perm: tells get_file() to try to preserve the sources
180 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000181 preserve_symlinks: try to preserve symlinks instead of
182 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000183
184 Raises:
185 AutoservRunError: the scp command failed
186 """
187 if isinstance(source, basestring):
188 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000189 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000190
jadmanskid7b79ed2009-01-07 17:19:48 +0000191 try:
jadmanski2583a432009-02-10 23:59:11 +0000192 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000193 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000194 rsync = self._make_rsync_cmd([remote_source], local_dest,
mbligh45561782009-05-11 21:14:34 +0000195 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000196 utils.run(rsync)
197 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000198 logging.warn("warning: rsync failed with: %s", e)
199 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000200
jadmanskid7b79ed2009-01-07 17:19:48 +0000201 # scp has no equivalent to --delete, just drop the entire dest dir
202 if delete_dest and os.path.isdir(dest):
203 shutil.rmtree(dest)
204 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000205
jadmanskid7b79ed2009-01-07 17:19:48 +0000206 remote_source = self._make_rsync_compatible_source(source, False)
207 if remote_source:
jadmanski2583a432009-02-10 23:59:11 +0000208 remote_source = self._encode_remote_paths(remote_source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000209 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000210 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000211 try:
212 utils.run(scp)
213 except error.CmdError, e:
214 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000215
mblighfeac0102009-04-28 18:31:12 +0000216 if not preserve_perm:
217 # we have no way to tell scp to not try to preserve the
218 # permissions so set them after copy instead.
219 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
220 # options are only in very recent rsync versions
221 self._set_umask_perms(dest)
222
jadmanskica7da372008-10-21 16:26:52 +0000223
mbligh45561782009-05-11 21:14:34 +0000224 def send_file(self, source, dest, delete_dest=False,
225 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000226 """
227 Copy files from a local path to the remote host.
228
229 Directories will be copied recursively.
230 If a source component is a directory with a trailing slash,
231 the content of the directory will be copied, otherwise, the
232 directory itself and its content will be copied. This
233 behavior is similar to that of the program 'rsync'.
234
235 Args:
236 source: either
237 1) a single file or directory, as a string
238 2) a list of one or more (possibly mixed)
239 files or directories
240 dest: a file or a directory (if source contains a
241 directory or more than one element, you must
242 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000243 delete_dest: if this is true, the command will also clear
244 out any old files at dest that are not in the
245 source
mbligh45561782009-05-11 21:14:34 +0000246 preserve_symlinks: controls if symlinks on the source will be
247 copied as such on the destination or transformed into the
248 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000249
250 Raises:
251 AutoservRunError: the scp command failed
252 """
253 if isinstance(source, basestring):
254 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000255 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000256
jadmanskid7b79ed2009-01-07 17:19:48 +0000257 try:
jadmanski2583a432009-02-10 23:59:11 +0000258 local_sources = [utils.sh_escape(path) for path in source]
259 rsync = self._make_rsync_cmd(local_sources, remote_dest,
mbligh45561782009-05-11 21:14:34 +0000260 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000261 utils.run(rsync)
262 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000263 logging.warn("warning: rsync failed with: %s", e)
264 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000265
jadmanskid7b79ed2009-01-07 17:19:48 +0000266 # scp has no equivalent to --delete, just drop the entire dest dir
267 if delete_dest:
268 is_dir = self.run("ls -d %s/" % remote_dest,
269 ignore_status=True).exit_status == 0
270 if is_dir:
271 cmd = "rm -rf %s && mkdir %s"
272 cmd %= (remote_dest, remote_dest)
273 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000274
jadmanski2583a432009-02-10 23:59:11 +0000275 local_sources = self._make_rsync_compatible_source(source, True)
276 if local_sources:
277 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000278 try:
279 utils.run(scp)
280 except error.CmdError, e:
281 raise error.AutoservRunError(e.args[0], e.args[1])
282
jadmanskica7da372008-10-21 16:26:52 +0000283 self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest)
284 self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest)
285 if self.target_file_owner:
286 self.run('chown -R %s %s' % (self.target_file_owner, dest))
287
288
289 def ssh_ping(self, timeout=60):
290 try:
291 self.run("true", timeout=timeout, connect_timeout=timeout)
showardb18134f2009-03-20 20:52:18 +0000292 logging.info("ssh_ping of %s completed sucessfully", self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000293 except error.AutoservSSHTimeout:
294 msg = "ssh ping timed out (timeout = %d)" % timeout
295 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000296 except error.AutoservSshPermissionDeniedError:
297 #let AutoservSshPermissionDeniedError be visible to the callers
298 raise
jadmanskica7da372008-10-21 16:26:52 +0000299 except error.AutoservRunError, e:
300 msg = "command true failed in ssh ping"
301 raise error.AutoservRunError(msg, e.result_obj)
302
303
304 def is_up(self):
305 """
306 Check if the remote host is up.
307
308 Returns:
309 True if the remote host is up, False otherwise
310 """
311 try:
312 self.ssh_ping()
313 except error.AutoservError:
314 return False
315 else:
316 return True
317
318
319 def wait_up(self, timeout=None):
320 """
321 Wait until the remote host is up or the timeout expires.
322
323 In fact, it will wait until an ssh connection to the remote
324 host can be established, and getty is running.
325
326 Args:
327 timeout: time limit in seconds before returning even
328 if the host is not up.
329
330 Returns:
331 True if the host was found to be up, False otherwise
332 """
333 if timeout:
334 end_time = time.time() + timeout
335
336 while not timeout or time.time() < end_time:
337 if self.is_up():
338 try:
339 if self.are_wait_up_processes_up():
340 return True
341 except error.AutoservError:
342 pass
343 time.sleep(1)
344
345 return False
346
347
mbligh2ed998f2009-04-08 21:03:47 +0000348 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000349 """
350 Wait until the remote host is down or the timeout expires.
351
352 In fact, it will wait until an ssh connection to the remote
353 host fails.
354
355 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000356 timeout: time limit in seconds before returning even
357 if the host is still up.
358 warning_timer: time limit in seconds that will generate
359 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000360
361 Returns:
362 True if the host was found to be down, False otherwise
363 """
mbligh2ed998f2009-04-08 21:03:47 +0000364 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000365 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000366 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000367
mbligh2ed998f2009-04-08 21:03:47 +0000368 if warning_timer:
369 warn_time = current_time + warning_timer
370
371 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000372 if not self.is_up():
373 return True
mbligh2ed998f2009-04-08 21:03:47 +0000374
375 if warning_timer and current_time > warn_time:
376 self.record("WARN", None, "shutdown",
377 "Shutdown took longer than %ds" % warning_timer)
378 # Print the warning only once.
379 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000380 # If a machine is stuck switching runlevels
381 # This may cause the machine to reboot.
382 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000383
jadmanskica7da372008-10-21 16:26:52 +0000384 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000385 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000386
387 return False
jadmanskif6562912008-10-21 17:59:01 +0000388
mbligha0a27592009-01-24 01:41:36 +0000389
jadmanskif6562912008-10-21 17:59:01 +0000390 # tunable constants for the verify & repair code
391 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000392
jadmanskif6562912008-10-21 17:59:01 +0000393
394 def verify(self):
mblighb49b5232009-02-12 21:54:49 +0000395 super(AbstractSSHHost, self).verify_hardware()
jadmanskif6562912008-10-21 17:59:01 +0000396
showardb18134f2009-03-20 20:52:18 +0000397 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000398 self.ssh_ping()
399
jadmanski80deb752009-01-21 17:14:16 +0000400 if self.is_shutting_down():
401 raise error.AutoservHostError("Host is shutting down")
402
mblighb49b5232009-02-12 21:54:49 +0000403 super(AbstractSSHHost, self).verify_software()
404
jadmanskif6562912008-10-21 17:59:01 +0000405 try:
406 autodir = autotest._get_autodir(self)
407 if autodir:
jadmanskif6562912008-10-21 17:59:01 +0000408 self.check_diskspace(autodir,
409 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
410 except error.AutoservHostError:
411 raise # only want to raise if it's a space issue
412 except Exception:
413 pass # autotest dir may not exist, etc. ignore
414
415
showard170873e2009-01-07 00:22:26 +0000416class LoggerFile(object):
417 def write(self, data):
jadmanskifcc851b2009-01-07 17:31:12 +0000418 if data:
showardb18134f2009-03-20 20:52:18 +0000419 logging.debug(data.rstrip("\n"))
showard170873e2009-01-07 00:22:26 +0000420
421
422 def flush(self):
423 pass