blob: ff3c19c8ac867038ed8b9a6e404aa043c322d2be [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
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
showard56176ec2009-10-28 19:52:30 +000038 def _encode_remote_paths(self, paths, escape=True):
jadmanski2583a432009-02-10 23:59:11 +000039 """ Given a list of file paths, encodes it as a single remote path, in
40 the style used by rsync and scp. """
showard56176ec2009-10-28 19:52:30 +000041 if escape:
42 paths = [utils.scp_remote_escape(path) for path in paths]
43 return '%s@%s:"%s"' % (self.user, self.hostname, " ".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. """
mbligh29e15842009-12-23 22:02:54 +000068 command = "scp -rq -P %d %s '%s'"
jadmanskid7b79ed2009-01-07 17:19:48 +000069 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:
showard56176ec2009-10-28 19:52:30 +000087 def glob_matches_files(path, pattern):
88 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +000089 else:
showard56176ec2009-10-28 19:52:30 +000090 def glob_matches_files(path, pattern):
91 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
92 pattern),
93 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +000094 return result.exit_status == 0
95
96 # take a set of globs that cover all files, and see which are needed
97 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +000098 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +000099
100 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000101 if is_local:
showard56176ec2009-10-28 19:52:30 +0000102 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
103 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000104 else:
showard56176ec2009-10-28 19:52:30 +0000105 return [utils.scp_remote_escape(path) + pattern
106 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000107
108
109 def _make_rsync_compatible_source(self, source, is_local):
110 """ Applies the same logic as _make_rsync_compatible_globs, but
111 applies it to an entire list of sources, producing a new list of
112 sources, properly quoted. """
113 return sum((self._make_rsync_compatible_globs(path, is_local)
114 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000115
116
mblighfeac0102009-04-28 18:31:12 +0000117 def _set_umask_perms(self, dest):
118 """Given a destination file/dir (recursively) set the permissions on
119 all the files and directories to the max allowed by running umask."""
120
121 # now this looks strange but I haven't found a way in Python to _just_
122 # get the umask, apparently the only option is to try to set it
123 umask = os.umask(0)
124 os.umask(umask)
125
126 max_privs = 0777 & ~umask
127
128 def set_file_privs(filename):
129 file_stat = os.stat(filename)
130
131 file_privs = max_privs
132 # if the original file permissions do not have at least one
133 # executable bit then do not set it anywhere
134 if not file_stat.st_mode & 0111:
135 file_privs &= ~0111
136
137 os.chmod(filename, file_privs)
138
139 # try a bottom-up walk so changes on directory permissions won't cut
140 # our access to the files/directories inside it
141 for root, dirs, files in os.walk(dest, topdown=False):
142 # when setting the privileges we emulate the chmod "X" behaviour
143 # that sets to execute only if it is a directory or any of the
144 # owner/group/other already has execute right
145 for dirname in dirs:
146 os.chmod(os.path.join(root, dirname), max_privs)
147
148 for filename in files:
149 set_file_privs(os.path.join(root, filename))
150
151
152 # now set privs for the dest itself
153 if os.path.isdir(dest):
154 os.chmod(dest, max_privs)
155 else:
156 set_file_privs(dest)
157
158
mbligh45561782009-05-11 21:14:34 +0000159 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
160 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000161 """
162 Copy files from the remote host to a local path.
163
164 Directories will be copied recursively.
165 If a source component is a directory with a trailing slash,
166 the content of the directory will be copied, otherwise, the
167 directory itself and its content will be copied. This
168 behavior is similar to that of the program 'rsync'.
169
170 Args:
171 source: either
172 1) a single file or directory, as a string
173 2) a list of one or more (possibly mixed)
174 files or directories
175 dest: a file or a directory (if source contains a
176 directory or more than one element, you must
177 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000178 delete_dest: if this is true, the command will also clear
179 out any old files at dest that are not in the
180 source
mblighfeac0102009-04-28 18:31:12 +0000181 preserve_perm: tells get_file() to try to preserve the sources
182 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000183 preserve_symlinks: try to preserve symlinks instead of
184 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000185
186 Raises:
187 AutoservRunError: the scp command failed
188 """
189 if isinstance(source, basestring):
190 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000191 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000192
jadmanskid7b79ed2009-01-07 17:19:48 +0000193 try:
jadmanski2583a432009-02-10 23:59:11 +0000194 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000195 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000196 rsync = self._make_rsync_cmd([remote_source], local_dest,
mbligh45561782009-05-11 21:14:34 +0000197 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000198 utils.run(rsync)
199 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000200 logging.warn("warning: rsync failed with: %s", e)
201 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000202
jadmanskid7b79ed2009-01-07 17:19:48 +0000203 # scp has no equivalent to --delete, just drop the entire dest dir
204 if delete_dest and os.path.isdir(dest):
205 shutil.rmtree(dest)
206 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000207
jadmanskid7b79ed2009-01-07 17:19:48 +0000208 remote_source = self._make_rsync_compatible_source(source, False)
209 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000210 # _make_rsync_compatible_source() already did the escaping
211 remote_source = self._encode_remote_paths(remote_source,
212 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000213 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000214 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000215 try:
216 utils.run(scp)
217 except error.CmdError, e:
218 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000219
mblighfeac0102009-04-28 18:31:12 +0000220 if not preserve_perm:
221 # we have no way to tell scp to not try to preserve the
222 # permissions so set them after copy instead.
223 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
224 # options are only in very recent rsync versions
225 self._set_umask_perms(dest)
226
jadmanskica7da372008-10-21 16:26:52 +0000227
mbligh45561782009-05-11 21:14:34 +0000228 def send_file(self, source, dest, delete_dest=False,
229 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000230 """
231 Copy files from a local path to the remote host.
232
233 Directories will be copied recursively.
234 If a source component is a directory with a trailing slash,
235 the content of the directory will be copied, otherwise, the
236 directory itself and its content will be copied. This
237 behavior is similar to that of the program 'rsync'.
238
239 Args:
240 source: either
241 1) a single file or directory, as a string
242 2) a list of one or more (possibly mixed)
243 files or directories
244 dest: a file or a directory (if source contains a
245 directory or more than one element, you must
246 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000247 delete_dest: if this is true, the command will also clear
248 out any old files at dest that are not in the
249 source
mbligh45561782009-05-11 21:14:34 +0000250 preserve_symlinks: controls if symlinks on the source will be
251 copied as such on the destination or transformed into the
252 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000253
254 Raises:
255 AutoservRunError: the scp command failed
256 """
257 if isinstance(source, basestring):
258 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000259 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000260
jadmanskid7b79ed2009-01-07 17:19:48 +0000261 try:
jadmanski2583a432009-02-10 23:59:11 +0000262 local_sources = [utils.sh_escape(path) for path in source]
263 rsync = self._make_rsync_cmd(local_sources, remote_dest,
mbligh45561782009-05-11 21:14:34 +0000264 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000265 utils.run(rsync)
266 except error.CmdError, e:
mblighd0e94982009-07-11 00:15:18 +0000267 logging.warn("Command rsync failed with: %s", e)
268 logging.info("Attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000269
jadmanskid7b79ed2009-01-07 17:19:48 +0000270 # scp has no equivalent to --delete, just drop the entire dest dir
271 if delete_dest:
showard27160152009-07-15 14:28:42 +0000272 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000273 ignore_status=True).exit_status == 0
274 if is_dir:
275 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000276 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000277 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000278
jadmanski2583a432009-02-10 23:59:11 +0000279 local_sources = self._make_rsync_compatible_source(source, True)
280 if local_sources:
281 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000282 try:
283 utils.run(scp)
284 except error.CmdError, e:
285 raise error.AutoservRunError(e.args[0], e.args[1])
286
jadmanskica7da372008-10-21 16:26:52 +0000287
288 def ssh_ping(self, timeout=60):
289 try:
290 self.run("true", timeout=timeout, connect_timeout=timeout)
291 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000292 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000293 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000294 except error.AutoservSshPermissionDeniedError:
295 #let AutoservSshPermissionDeniedError be visible to the callers
296 raise
jadmanskica7da372008-10-21 16:26:52 +0000297 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000298 # convert the generic AutoservRunError into something more
299 # specific for this context
300 raise error.AutoservSshPingHostError(e.description + '\n' +
301 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000302
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
showardca572982009-09-18 21:20:01 +0000394 def verify_connectivity(self):
395 super(AbstractSSHHost, self).verify_connectivity()
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()
mbligh2ba7ab02009-08-24 22:09:26 +0000399 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000400
jadmanski80deb752009-01-21 17:14:16 +0000401 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000402 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000403
mblighb49b5232009-02-12 21:54:49 +0000404
showardca572982009-09-18 21:20:01 +0000405 def verify_software(self):
406 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000407 try:
showardad812bf2009-10-20 23:49:56 +0000408 self.check_diskspace(autotest.Autotest.get_install_dir(self),
409 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000410 except error.AutoservHostError:
411 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000412 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000413 # autotest dir may not exist, etc. ignore
414 logging.debug('autodir space check exception, this is probably '
415 'safe to ignore\n' + traceback.format_exc())