blob: e65ba8d08133d46eecfe12e163e6f46f81049c40 [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:
jadmanskid7b79ed2009-01-07 17:19:48 +0000214 # scp has no equivalent to --delete, just drop the entire dest dir
215 if delete_dest and os.path.isdir(dest):
216 shutil.rmtree(dest)
217 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000218
jadmanskid7b79ed2009-01-07 17:19:48 +0000219 remote_source = self._make_rsync_compatible_source(source, False)
220 if remote_source:
showard56176ec2009-10-28 19:52:30 +0000221 # _make_rsync_compatible_source() already did the escaping
222 remote_source = self._encode_remote_paths(remote_source,
223 escape=False)
jadmanskid7b79ed2009-01-07 17:19:48 +0000224 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000225 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000226 try:
227 utils.run(scp)
228 except error.CmdError, e:
229 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000230
mblighfeac0102009-04-28 18:31:12 +0000231 if not preserve_perm:
232 # we have no way to tell scp to not try to preserve the
233 # permissions so set them after copy instead.
234 # for rsync we could use "--no-p --chmod=ugo=rwX" but those
235 # options are only in very recent rsync versions
236 self._set_umask_perms(dest)
237
jadmanskica7da372008-10-21 16:26:52 +0000238
mbligh45561782009-05-11 21:14:34 +0000239 def send_file(self, source, dest, delete_dest=False,
240 preserve_symlinks=False):
jadmanskica7da372008-10-21 16:26:52 +0000241 """
242 Copy files from a local path to the remote host.
243
244 Directories will be copied recursively.
245 If a source component is a directory with a trailing slash,
246 the content of the directory will be copied, otherwise, the
247 directory itself and its content will be copied. This
248 behavior is similar to that of the program 'rsync'.
249
250 Args:
251 source: either
252 1) a single file or directory, as a string
253 2) a list of one or more (possibly mixed)
254 files or directories
255 dest: a file or a directory (if source contains a
256 directory or more than one element, you must
257 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000258 delete_dest: if this is true, the command will also clear
259 out any old files at dest that are not in the
260 source
mbligh45561782009-05-11 21:14:34 +0000261 preserve_symlinks: controls if symlinks on the source will be
262 copied as such on the destination or transformed into the
263 referenced file/directory
jadmanskica7da372008-10-21 16:26:52 +0000264
265 Raises:
266 AutoservRunError: the scp command failed
267 """
268 if isinstance(source, basestring):
269 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000270 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000271
jadmanskid7b79ed2009-01-07 17:19:48 +0000272 try:
jadmanski2583a432009-02-10 23:59:11 +0000273 local_sources = [utils.sh_escape(path) for path in source]
274 rsync = self._make_rsync_cmd(local_sources, remote_dest,
mbligh45561782009-05-11 21:14:34 +0000275 delete_dest, preserve_symlinks)
jadmanskid7b79ed2009-01-07 17:19:48 +0000276 utils.run(rsync)
277 except error.CmdError, e:
jadmanskid7b79ed2009-01-07 17:19:48 +0000278 # scp has no equivalent to --delete, just drop the entire dest dir
279 if delete_dest:
showard27160152009-07-15 14:28:42 +0000280 is_dir = self.run("ls -d %s/" % dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000281 ignore_status=True).exit_status == 0
282 if is_dir:
283 cmd = "rm -rf %s && mkdir %s"
mbligh5a0ca532009-08-03 16:44:34 +0000284 cmd %= (dest, dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000285 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000286
jadmanski2583a432009-02-10 23:59:11 +0000287 local_sources = self._make_rsync_compatible_source(source, True)
288 if local_sources:
289 scp = self._make_scp_cmd(local_sources, remote_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000290 try:
291 utils.run(scp)
292 except error.CmdError, e:
293 raise error.AutoservRunError(e.args[0], e.args[1])
294
jadmanskica7da372008-10-21 16:26:52 +0000295
296 def ssh_ping(self, timeout=60):
297 try:
298 self.run("true", timeout=timeout, connect_timeout=timeout)
299 except error.AutoservSSHTimeout:
mblighd0e94982009-07-11 00:15:18 +0000300 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
jadmanskica7da372008-10-21 16:26:52 +0000301 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000302 except error.AutoservSshPermissionDeniedError:
303 #let AutoservSshPermissionDeniedError be visible to the callers
304 raise
jadmanskica7da372008-10-21 16:26:52 +0000305 except error.AutoservRunError, e:
mblighc971c5f2009-06-08 16:48:54 +0000306 # convert the generic AutoservRunError into something more
307 # specific for this context
308 raise error.AutoservSshPingHostError(e.description + '\n' +
309 repr(e.result_obj))
jadmanskica7da372008-10-21 16:26:52 +0000310
311
312 def is_up(self):
313 """
314 Check if the remote host is up.
315
316 Returns:
317 True if the remote host is up, False otherwise
318 """
319 try:
320 self.ssh_ping()
321 except error.AutoservError:
322 return False
323 else:
324 return True
325
326
327 def wait_up(self, timeout=None):
328 """
329 Wait until the remote host is up or the timeout expires.
330
331 In fact, it will wait until an ssh connection to the remote
332 host can be established, and getty is running.
333
334 Args:
335 timeout: time limit in seconds before returning even
336 if the host is not up.
337
338 Returns:
339 True if the host was found to be up, False otherwise
340 """
341 if timeout:
342 end_time = time.time() + timeout
343
344 while not timeout or time.time() < end_time:
345 if self.is_up():
346 try:
347 if self.are_wait_up_processes_up():
348 return True
349 except error.AutoservError:
350 pass
351 time.sleep(1)
352
353 return False
354
355
mbligh2ed998f2009-04-08 21:03:47 +0000356 def wait_down(self, timeout=None, warning_timer=None):
jadmanskica7da372008-10-21 16:26:52 +0000357 """
358 Wait until the remote host is down or the timeout expires.
359
360 In fact, it will wait until an ssh connection to the remote
361 host fails.
362
363 Args:
mbligh2ed998f2009-04-08 21:03:47 +0000364 timeout: time limit in seconds before returning even
365 if the host is still up.
366 warning_timer: time limit in seconds that will generate
367 a warning if the host is not down yet.
jadmanskica7da372008-10-21 16:26:52 +0000368
369 Returns:
370 True if the host was found to be down, False otherwise
371 """
mbligh2ed998f2009-04-08 21:03:47 +0000372 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000373 if timeout:
mbligh2ed998f2009-04-08 21:03:47 +0000374 end_time = current_time + timeout
jadmanskica7da372008-10-21 16:26:52 +0000375
mbligh2ed998f2009-04-08 21:03:47 +0000376 if warning_timer:
377 warn_time = current_time + warning_timer
378
379 while not timeout or current_time < end_time:
jadmanskica7da372008-10-21 16:26:52 +0000380 if not self.is_up():
381 return True
mbligh2ed998f2009-04-08 21:03:47 +0000382
383 if warning_timer and current_time > warn_time:
384 self.record("WARN", None, "shutdown",
385 "Shutdown took longer than %ds" % warning_timer)
386 # Print the warning only once.
387 warning_timer = None
mbligha4464402009-04-17 20:13:41 +0000388 # If a machine is stuck switching runlevels
389 # This may cause the machine to reboot.
390 self.run('kill -HUP 1', ignore_status=True)
mbligh2ed998f2009-04-08 21:03:47 +0000391
jadmanskica7da372008-10-21 16:26:52 +0000392 time.sleep(1)
mbligh2ed998f2009-04-08 21:03:47 +0000393 current_time = time.time()
jadmanskica7da372008-10-21 16:26:52 +0000394
395 return False
jadmanskif6562912008-10-21 17:59:01 +0000396
mbligha0a27592009-01-24 01:41:36 +0000397
jadmanskif6562912008-10-21 17:59:01 +0000398 # tunable constants for the verify & repair code
399 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000400
jadmanskif6562912008-10-21 17:59:01 +0000401
showardca572982009-09-18 21:20:01 +0000402 def verify_connectivity(self):
403 super(AbstractSSHHost, self).verify_connectivity()
jadmanskif6562912008-10-21 17:59:01 +0000404
showardb18134f2009-03-20 20:52:18 +0000405 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000406 self.ssh_ping()
mbligh2ba7ab02009-08-24 22:09:26 +0000407 logging.info("Host (ssh) %s is alive", self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000408
jadmanski80deb752009-01-21 17:14:16 +0000409 if self.is_shutting_down():
mblighc971c5f2009-06-08 16:48:54 +0000410 raise error.AutoservHostIsShuttingDownError("Host is shutting down")
jadmanski80deb752009-01-21 17:14:16 +0000411
mblighb49b5232009-02-12 21:54:49 +0000412
showardca572982009-09-18 21:20:01 +0000413 def verify_software(self):
414 super(AbstractSSHHost, self).verify_software()
jadmanskif6562912008-10-21 17:59:01 +0000415 try:
showardad812bf2009-10-20 23:49:56 +0000416 self.check_diskspace(autotest.Autotest.get_install_dir(self),
417 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
jadmanskif6562912008-10-21 17:59:01 +0000418 except error.AutoservHostError:
419 raise # only want to raise if it's a space issue
showardad812bf2009-10-20 23:49:56 +0000420 except autotest.AutodirNotFoundError:
showardca572982009-09-18 21:20:01 +0000421 # autotest dir may not exist, etc. ignore
422 logging.debug('autodir space check exception, this is probably '
423 'safe to ignore\n' + traceback.format_exc())