blob: fe585ff07f90f8c2d15cd9936302d856e7b80e03 [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:
mblighc971c5f2009-06-08 16:48:54 +0000300 # convert the generic AutoservRunError into something more
301 # specific for this context
302 raise error.AutoservSshPingHostError(e.description + '\n' +
303 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000304
305
306 def is_up(self):
307 """
308 Check if the remote host is up.
309
310 Returns:
311 True if the remote host is up, False otherwise
312 """
313 try:
314 self.ssh_ping()
315 except error.AutoservError:
316 return False
317 else:
318 return True
319
320
321 def wait_up(self, timeout=None):
322 """
323 Wait until the remote host is up or the timeout expires.
324
325 In fact, it will wait until an ssh connection to the remote
326 host can be established, and getty is running.
327
328 Args:
329 timeout: time limit in seconds before returning even
330 if the host is not up.
331
332 Returns:
333 True if the host was found to be up, False otherwise
334 """
335 if timeout:
336 end_time = time.time() + timeout
337
338 while not timeout or time.time() < end_time:
339 if self.is_up():
340 try:
341 if self.are_wait_up_processes_up():
342 return True
343 except error.AutoservError:
344 pass
345 time.sleep(1)
346
347 return False
348
349
mbligh2ed998f2009-04-08 21:03:47 +0000350 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000351 """
352 Wait until the remote host is down or the timeout expires.
353
354 In fact, it will wait until an ssh connection to the remote
355 host fails.
356
357 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000358 timeout: time limit in seconds before returning even
359 if the host is still up.
360 warning_timer: time limit in seconds that will generate
361 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000362
363 Returns:
364 True if the host was found to be down, False otherwise
365 """
mbligh2ed998f2009-04-08 21:03:47 +0000366 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000367 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000368 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000369
mbligh2ed998f2009-04-08 21:03:47 +0000370 if warning_timer:
371 warn_time = current_time + warning_timer
372
373 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000374 if not self.is_up():
375 return True
mbligh2ed998f2009-04-08 21:03:47 +0000376
377 if warning_timer and current_time > warn_time:
378 self.record("WARN", None, "shutdown",
379 "Shutdown took longer than %ds" % warning_timer)
380 # Print the warning only once.
381 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000382 # If a machine is stuck switching runlevels
383 # This may cause the machine to reboot.
384 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000385
jadmanskica7da372008-10-21 16:26:52 +0000386 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000387 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000388
389 return False
jadmanskif6562912008-10-21 17:59:01 +0000390
mbligha0a27592009-01-24 01:41:36 +0000391
jadmanskif6562912008-10-21 17:59:01 +0000392 # tunable constants for the verify & repair code
393 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000394
jadmanskif6562912008-10-21 17:59:01 +0000395
396 def verify(self):
mblighb49b5232009-02-12 21:54:49 +0000397 super(AbstractSSHHost, self).verify_hardware()
jadmanskif6562912008-10-21 17:59:01 +0000398
showardb18134f2009-03-20 20:52:18 +0000399 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000400 self.ssh_ping()
401
jadmanski80deb752009-01-21 17:14:16 +0000402 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000403 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000404
mblighb49b5232009-02-12 21:54:49 +0000405 super(AbstractSSHHost, self).verify_software()
406
jadmanskif6562912008-10-21 17:59:01 +0000407 try:
408 autodir = autotest._get_autodir(self)
409 if autodir:
jadmanskif6562912008-10-21 17:59:01 +0000410 self.check_diskspace(autodir,
411 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
412 except error.AutoservHostError:
413 raise # only want to raise if it's a space issue
414 except Exception:
415 pass # autotest dir may not exist, etc. ignore
416
417
showard170873e2009-01-07 00:22:26 +0000418class LoggerFile(object):
419 def write(self, data):
jadmanskifcc851b2009-01-07 17:31:12 +0000420 if data:
showardb18134f2009-03-20 20:52:18 +0000421 logging.debug(data.rstrip("\n"))
showard170873e2009-01-07 00:22:26 +0000422
423
424 def flush(self):
425 pass