blob: 8b5778a4087a2b6c7c3dbf2f8ad20cc41049a301 [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):
mblighbc9402b2009-12-29 01:15:34 +000023 """
24 This class represents a generic implementation of most of the
jadmanskica7da372008-10-21 16:26:52 +000025 framework necessary for controlling a host via ssh. It implements
26 almost all of the abstract Host methods, except for the core
mblighbc9402b2009-12-29 01:15:34 +000027 Host.run method.
28 """
jadmanskica7da372008-10-21 16:26:52 +000029
jadmanskif6562912008-10-21 17:59:01 +000030 def _initialize(self, hostname, user="root", port=22, password="",
31 *args, **dargs):
32 super(AbstractSSHHost, self)._initialize(hostname=hostname,
33 *args, **dargs)
mbligh6369cf22008-10-24 17:21:57 +000034 self.ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
jadmanskica7da372008-10-21 16:26:52 +000035 self.user = user
36 self.port = port
37 self.password = password
38
39
showard56176ec2009-10-28 19:52:30 +000040 def _encode_remote_paths(self, paths, escape=True):
mblighbc9402b2009-12-29 01:15:34 +000041 """
42 Given a list of file paths, encodes it as a single remote path, in
43 the style used by rsync and scp.
44 """
showard56176ec2009-10-28 19:52:30 +000045 if escape:
46 paths = [utils.scp_remote_escape(path) for path in paths]
47 return '%s@%s:"%s"' % (self.user, self.hostname, " ".join(paths))
jadmanskica7da372008-10-21 16:26:52 +000048
jadmanskica7da372008-10-21 16:26:52 +000049
mbligh45561782009-05-11 21:14:34 +000050 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks):
mblighbc9402b2009-12-29 01:15:34 +000051 """
52 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +000053 appropriate rsync command for copying them. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +000054 pre-encoded.
55 """
jadmanskid7b79ed2009-01-07 17:19:48 +000056 ssh_cmd = make_ssh_command(self.user, self.port)
57 if delete_dest:
58 delete_flag = "--delete"
59 else:
60 delete_flag = ""
mbligh45561782009-05-11 21:14:34 +000061 if preserve_symlinks:
62 symlink_flag = ""
63 else:
64 symlink_flag = "-L"
65 command = "rsync %s %s --timeout=1800 --rsh='%s' -az %s %s"
66 return command % (symlink_flag, delete_flag, ssh_cmd,
67 " ".join(sources), dest)
jadmanskid7b79ed2009-01-07 17:19:48 +000068
69
70 def _make_scp_cmd(self, sources, dest):
mblighbc9402b2009-12-29 01:15:34 +000071 """
72 Given a list of source paths and a destination path, produces the
jadmanskid7b79ed2009-01-07 17:19:48 +000073 appropriate scp command for encoding it. Remote paths must be
mblighbc9402b2009-12-29 01:15:34 +000074 pre-encoded.
75 """
mbligh29e15842009-12-23 22:02:54 +000076 command = "scp -rq -P %d %s '%s'"
jadmanskid7b79ed2009-01-07 17:19:48 +000077 return command % (self.port, " ".join(sources), dest)
78
79
80 def _make_rsync_compatible_globs(self, path, is_local):
mblighbc9402b2009-12-29 01:15:34 +000081 """
82 Given an rsync-style path, returns a list of globbed paths
jadmanskid7b79ed2009-01-07 17:19:48 +000083 that will hopefully provide equivalent behaviour for scp. Does not
84 support the full range of rsync pattern matching behaviour, only that
85 exposed in the get/send_file interface (trailing slashes).
86
87 The is_local param is flag indicating if the paths should be
mblighbc9402b2009-12-29 01:15:34 +000088 interpreted as local or remote paths.
89 """
jadmanskid7b79ed2009-01-07 17:19:48 +000090
91 # non-trailing slash paths should just work
92 if len(path) == 0 or path[-1] != "/":
93 return [path]
94
95 # make a function to test if a pattern matches any files
96 if is_local:
showard56176ec2009-10-28 19:52:30 +000097 def glob_matches_files(path, pattern):
98 return len(glob.glob(path + pattern)) > 0
jadmanskid7b79ed2009-01-07 17:19:48 +000099 else:
showard56176ec2009-10-28 19:52:30 +0000100 def glob_matches_files(path, pattern):
101 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
102 pattern),
103 stdout_tee=None, ignore_status=True)
jadmanskid7b79ed2009-01-07 17:19:48 +0000104 return result.exit_status == 0
105
106 # take a set of globs that cover all files, and see which are needed
107 patterns = ["*", ".[!.]*"]
showard56176ec2009-10-28 19:52:30 +0000108 patterns = [p for p in patterns if glob_matches_files(path, p)]
jadmanskid7b79ed2009-01-07 17:19:48 +0000109
110 # convert them into a set of paths suitable for the commandline
jadmanskid7b79ed2009-01-07 17:19:48 +0000111 if is_local:
showard56176ec2009-10-28 19:52:30 +0000112 return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
113 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000114 else:
showard56176ec2009-10-28 19:52:30 +0000115 return [utils.scp_remote_escape(path) + pattern
116 for pattern in patterns]
jadmanskid7b79ed2009-01-07 17:19:48 +0000117
118
119 def _make_rsync_compatible_source(self, source, is_local):
mblighbc9402b2009-12-29 01:15:34 +0000120 """
121 Applies the same logic as _make_rsync_compatible_globs, but
jadmanskid7b79ed2009-01-07 17:19:48 +0000122 applies it to an entire list of sources, producing a new list of
mblighbc9402b2009-12-29 01:15:34 +0000123 sources, properly quoted.
124 """
jadmanskid7b79ed2009-01-07 17:19:48 +0000125 return sum((self._make_rsync_compatible_globs(path, is_local)
126 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000127
128
mblighfeac0102009-04-28 18:31:12 +0000129 def _set_umask_perms(self, dest):
mblighbc9402b2009-12-29 01:15:34 +0000130 """
131 Given a destination file/dir (recursively) set the permissions on
132 all the files and directories to the max allowed by running umask.
133 """
mblighfeac0102009-04-28 18:31:12 +0000134
135 # now this looks strange but I haven't found a way in Python to _just_
136 # get the umask, apparently the only option is to try to set it
137 umask = os.umask(0)
138 os.umask(umask)
139
140 max_privs = 0777 & ~umask
141
142 def set_file_privs(filename):
143 file_stat = os.stat(filename)
144
145 file_privs = max_privs
146 # if the original file permissions do not have at least one
147 # executable bit then do not set it anywhere
148 if not file_stat.st_mode & 0111:
149 file_privs &= ~0111
150
151 os.chmod(filename, file_privs)
152
153 # try a bottom-up walk so changes on directory permissions won't cut
154 # our access to the files/directories inside it
155 for root, dirs, files in os.walk(dest, topdown=False):
156 # when setting the privileges we emulate the chmod "X" behaviour
157 # that sets to execute only if it is a directory or any of the
158 # owner/group/other already has execute right
159 for dirname in dirs:
160 os.chmod(os.path.join(root, dirname), max_privs)
161
162 for filename in files:
163 set_file_privs(os.path.join(root, filename))
164
165
166 # now set privs for the dest itself
167 if os.path.isdir(dest):
168 os.chmod(dest, max_privs)
169 else:
170 set_file_privs(dest)
171
172
mbligh45561782009-05-11 21:14:34 +0000173 def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
174 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000175 """
176 Copy files from the remote host to a local path.
177
178 Directories will be copied recursively.
179 If a source component is a directory with a trailing slash,
180 the content of the directory will be copied, otherwise, the
181 directory itself and its content will be copied. This
182 behavior is similar to that of the program 'rsync'.
183
184 Args:
185 source: either
186 1) a single file or directory, as a string
187 2) a list of one or more (possibly mixed)
188 files or directories
189 dest: a file or a directory (if source contains a
190 directory or more than one element, you must
191 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000192 delete_dest: if this is true, the command will also clear
193 out any old files at dest that are not in the
194 source
mblighfeac0102009-04-28 18:31:12 +0000195 preserve_perm: tells get_file() to try to preserve the sources
196 permissions on files and dirs
mbligh45561782009-05-11 21:14:34 +0000197 preserve_symlinks: try to preserve symlinks instead of
198 transforming them into files/dirs on copy
jadmanskica7da372008-10-21 16:26:52 +0000199
200 Raises:
201 AutoservRunError: the scp command failed
202 """
203 if isinstance(source, basestring):
204 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000205 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000206
jadmanskid7b79ed2009-01-07 17:19:48 +0000207 try:
jadmanski2583a432009-02-10 23:59:11 +0000208 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000209 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000210 rsync = self._make_rsync_cmd([remote_source], local_dest,
mbligh45561782009-05-11 21:14:34 +0000211 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000212 utils.run(rsync)
213 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000214 logging.warn("warning: rsync failed with: %s", e)
215 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000216
jadmanskid7b79ed2009-01-07 17:19:48 +0000217 # scp has no equivalent to --delete, just drop the entire dest dir
218 if delete_dest and os.path.isdir(dest):
219 shutil.rmtree(dest)
220 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000221
jadmanskid7b79ed2009-01-07 17:19:48 +0000222 remote_source = self._make_rsync_compatible_source(source, False)
223 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000224 # _make_rsync_compatible_source() already did the escaping
225 remote_source = self._encode_remote_paths(remote_source,
226 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000227 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000228 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000229 try:
230 utils.run(scp)
231 except error.CmdError, e:
232 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000233
mblighfeac0102009-04-28 18:31:12 +0000234 if not preserve_perm:
235 # we have no way to tell scp to not try to preserve the
236 # permissions so set them after copy instead.
237 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
238 # options are only in very recent rsync versions
239 self._set_umask_perms(dest)
240
jadmanskica7da372008-10-21 16:26:52 +0000241
mbligh45561782009-05-11 21:14:34 +0000242 def send_file(self, source, dest, delete_dest=False,
243 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000244 """
245 Copy files from a local path to the remote host.
246
247 Directories will be copied recursively.
248 If a source component is a directory with a trailing slash,
249 the content of the directory will be copied, otherwise, the
250 directory itself and its content will be copied. This
251 behavior is similar to that of the program 'rsync'.
252
253 Args:
254 source: either
255 1) a single file or directory, as a string
256 2) a list of one or more (possibly mixed)
257 files or directories
258 dest: a file or a directory (if source contains a
259 directory or more than one element, you must
260 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000261 delete_dest: if this is true, the command will also clear
262 out any old files at dest that are not in the
263 source
mbligh45561782009-05-11 21:14:34 +0000264 preserve_symlinks: controls if symlinks on the source will be
265 copied as such on the destination or transformed into the
266 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000267
268 Raises:
269 AutoservRunError: the scp command failed
270 """
271 if isinstance(source, basestring):
272 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000273 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000274
jadmanskid7b79ed2009-01-07 17:19:48 +0000275 try:
jadmanski2583a432009-02-10 23:59:11 +0000276 local_sources = [utils.sh_escape(path) for path in source]
277 rsync = self._make_rsync_cmd(local_sources, remote_dest,
mbligh45561782009-05-11 21:14:34 +0000278 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000279 utils.run(rsync)
280 except error.CmdError, e:
mblighd0e94982009-07-11 00:15:18 +0000281 logging.warn("Command rsync failed with: %s", e)
282 logging.info("Attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000283
jadmanskid7b79ed2009-01-07 17:19:48 +0000284 # scp has no equivalent to --delete, just drop the entire dest dir
285 if delete_dest:
showard27160152009-07-15 14:28:42 +0000286 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000287 ignore_status=True).exit_status == 0
288 if is_dir:
289 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000290 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000291 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000292
jadmanski2583a432009-02-10 23:59:11 +0000293 local_sources = self._make_rsync_compatible_source(source, True)
294 if local_sources:
295 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000296 try:
297 utils.run(scp)
298 except error.CmdError, e:
299 raise error.AutoservRunError(e.args[0], e.args[1])
300
jadmanskica7da372008-10-21 16:26:52 +0000301
302 def ssh_ping(self, timeout=60):
303 try:
304 self.run("true", timeout=timeout, connect_timeout=timeout)
305 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000306 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000307 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000308 except error.AutoservSshPermissionDeniedError:
309 #let AutoservSshPermissionDeniedError be visible to the callers
310 raise
jadmanskica7da372008-10-21 16:26:52 +0000311 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000312 # convert the generic AutoservRunError into something more
313 # specific for this context
314 raise error.AutoservSshPingHostError(e.description + '\n' +
315 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000316
317
318 def is_up(self):
319 """
320 Check if the remote host is up.
321
322 Returns:
323 True if the remote host is up, False otherwise
324 """
325 try:
326 self.ssh_ping()
327 except error.AutoservError:
328 return False
329 else:
330 return True
331
332
333 def wait_up(self, timeout=None):
334 """
335 Wait until the remote host is up or the timeout expires.
336
337 In fact, it will wait until an ssh connection to the remote
338 host can be established, and getty is running.
339
340 Args:
341 timeout: time limit in seconds before returning even
342 if the host is not up.
343
344 Returns:
345 True if the host was found to be up, False otherwise
346 """
347 if timeout:
348 end_time = time.time() + timeout
349
350 while not timeout or time.time() < end_time:
351 if self.is_up():
352 try:
353 if self.are_wait_up_processes_up():
354 return True
355 except error.AutoservError:
356 pass
357 time.sleep(1)
358
359 return False
360
361
mbligh2ed998f2009-04-08 21:03:47 +0000362 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000363 """
364 Wait until the remote host is down or the timeout expires.
365
366 In fact, it will wait until an ssh connection to the remote
367 host fails.
368
369 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000370 timeout: time limit in seconds before returning even
371 if the host is still up.
372 warning_timer: time limit in seconds that will generate
373 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000374
375 Returns:
376 True if the host was found to be down, False otherwise
377 """
mbligh2ed998f2009-04-08 21:03:47 +0000378 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000379 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000380 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000381
mbligh2ed998f2009-04-08 21:03:47 +0000382 if warning_timer:
383 warn_time = current_time + warning_timer
384
385 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000386 if not self.is_up():
387 return True
mbligh2ed998f2009-04-08 21:03:47 +0000388
389 if warning_timer and current_time > warn_time:
390 self.record("WARN", None, "shutdown",
391 "Shutdown took longer than %ds" % warning_timer)
392 # Print the warning only once.
393 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000394 # If a machine is stuck switching runlevels
395 # This may cause the machine to reboot.
396 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000397
jadmanskica7da372008-10-21 16:26:52 +0000398 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000399 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000400
401 return False
jadmanskif6562912008-10-21 17:59:01 +0000402
mbligha0a27592009-01-24 01:41:36 +0000403
jadmanskif6562912008-10-21 17:59:01 +0000404 # tunable constants for the verify & repair code
405 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000406
jadmanskif6562912008-10-21 17:59:01 +0000407
showardca572982009-09-18 21:20:01 +0000408 def verify_connectivity(self):
409 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000410
showardb18134f2009-03-20 20:52:18 +0000411 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000412 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000413 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000414
jadmanski80deb752009-01-21 17:14:16 +0000415 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000416 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000417
mblighb49b5232009-02-12 21:54:49 +0000418
showardca572982009-09-18 21:20:01 +0000419 def verify_software(self):
420 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000421 try:
showardad812bf2009-10-20 23:49:56 +0000422 self.check_diskspace(autotest.Autotest.get_install_dir(self),
423 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000424 except error.AutoservHostError:
425 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000426 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000427 # autotest dir may not exist, etc. ignore
428 logging.debug('autodir space check exception, this is probably '
429 'safe to ignore\n' + traceback.format_exc())