blob: d4708480fef9968d5582ac98a7413ad7b8bb7db5 [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,
showard27160152009-07-15 14:28:42 +000048 " ".join(escaped_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:
mblighd0e94982009-07-11 00:15:18 +0000268 logging.warn("Command 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:
showard27160152009-07-15 14:28:42 +0000273 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000274 ignore_status=True).exit_status == 0
275 if is_dir:
276 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000277 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000278 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)
297 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000298 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000299 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000300 except error.AutoservSshPermissionDeniedError:
301 #let AutoservSshPermissionDeniedError be visible to the callers
302 raise
jadmanskica7da372008-10-21 16:26:52 +0000303 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000304 # convert the generic AutoservRunError into something more
305 # specific for this context
306 raise error.AutoservSshPingHostError(e.description + '\n' +
307 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000308
309
310 def is_up(self):
311 """
312 Check if the remote host is up.
313
314 Returns:
315 True if the remote host is up, False otherwise
316 """
317 try:
318 self.ssh_ping()
319 except error.AutoservError:
320 return False
321 else:
322 return True
323
324
325 def wait_up(self, timeout=None):
326 """
327 Wait until the remote host is up or the timeout expires.
328
329 In fact, it will wait until an ssh connection to the remote
330 host can be established, and getty is running.
331
332 Args:
333 timeout: time limit in seconds before returning even
334 if the host is not up.
335
336 Returns:
337 True if the host was found to be up, False otherwise
338 """
339 if timeout:
340 end_time = time.time() + timeout
341
342 while not timeout or time.time() < end_time:
343 if self.is_up():
344 try:
345 if self.are_wait_up_processes_up():
346 return True
347 except error.AutoservError:
348 pass
349 time.sleep(1)
350
351 return False
352
353
mbligh2ed998f2009-04-08 21:03:47 +0000354 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000355 """
356 Wait until the remote host is down or the timeout expires.
357
358 In fact, it will wait until an ssh connection to the remote
359 host fails.
360
361 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000362 timeout: time limit in seconds before returning even
363 if the host is still up.
364 warning_timer: time limit in seconds that will generate
365 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000366
367 Returns:
368 True if the host was found to be down, False otherwise
369 """
mbligh2ed998f2009-04-08 21:03:47 +0000370 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000371 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000372 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000373
mbligh2ed998f2009-04-08 21:03:47 +0000374 if warning_timer:
375 warn_time = current_time + warning_timer
376
377 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000378 if not self.is_up():
379 return True
mbligh2ed998f2009-04-08 21:03:47 +0000380
381 if warning_timer and current_time > warn_time:
382 self.record("WARN", None, "shutdown",
383 "Shutdown took longer than %ds" % warning_timer)
384 # Print the warning only once.
385 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000386 # If a machine is stuck switching runlevels
387 # This may cause the machine to reboot.
388 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000389
jadmanskica7da372008-10-21 16:26:52 +0000390 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000391 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000392
393 return False
jadmanskif6562912008-10-21 17:59:01 +0000394
mbligha0a27592009-01-24 01:41:36 +0000395
jadmanskif6562912008-10-21 17:59:01 +0000396 # tunable constants for the verify & repair code
397 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000398
jadmanskif6562912008-10-21 17:59:01 +0000399
400 def verify(self):
mblighb49b5232009-02-12 21:54:49 +0000401 super(AbstractSSHHost, self).verify_hardware()
jadmanskif6562912008-10-21 17:59:01 +0000402
showardb18134f2009-03-20 20:52:18 +0000403 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000404 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000405 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000406
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