blob: 182a09050a6b3c38b13e857c3dc16aa76f765985 [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
jadmanskid7b79ed2009-01-07 17:19:48 +000046 def _make_rsync_cmd(self, sources, dest, delete_dest):
47 """ 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 = ""
55 command = "rsync -L %s --rsh='%s' -az %s %s"
56 return command % (delete_flag, ssh_cmd, " ".join(sources), dest)
57
58
59 def _make_scp_cmd(self, sources, dest):
60 """ Given a list of source paths and a destination path, produces the
61 appropriate scp command for encoding it. Remote paths must be
62 pre-encoded. """
63 command = "scp -rpq -P %d %s '%s'"
64 return command % (self.port, " ".join(sources), dest)
65
66
67 def _make_rsync_compatible_globs(self, path, is_local):
68 """ Given an rsync-style path, returns a list of globbed paths
69 that will hopefully provide equivalent behaviour for scp. Does not
70 support the full range of rsync pattern matching behaviour, only that
71 exposed in the get/send_file interface (trailing slashes).
72
73 The is_local param is flag indicating if the paths should be
74 interpreted as local or remote paths. """
75
76 # non-trailing slash paths should just work
77 if len(path) == 0 or path[-1] != "/":
78 return [path]
79
80 # make a function to test if a pattern matches any files
81 if is_local:
82 def glob_matches_files(path):
83 return len(glob.glob(path)) > 0
84 else:
85 def glob_matches_files(path):
86 result = self.run("ls \"%s\"" % utils.sh_escape(path),
87 ignore_status=True)
88 return result.exit_status == 0
89
90 # take a set of globs that cover all files, and see which are needed
91 patterns = ["*", ".[!.]*"]
92 patterns = [p for p in patterns if glob_matches_files(path + p)]
93
94 # convert them into a set of paths suitable for the commandline
95 path = utils.sh_escape(path)
96 if is_local:
97 return ["\"%s\"%s" % (path, pattern) for pattern in patterns]
98 else:
99 return ["\"%s\"" % (path + pattern) for pattern in patterns]
100
101
102 def _make_rsync_compatible_source(self, source, is_local):
103 """ Applies the same logic as _make_rsync_compatible_globs, but
104 applies it to an entire list of sources, producing a new list of
105 sources, properly quoted. """
106 return sum((self._make_rsync_compatible_globs(path, is_local)
107 for path in source), [])
jadmanskica7da372008-10-21 16:26:52 +0000108
109
mbligh89e258d2008-10-24 13:58:08 +0000110 def get_file(self, source, dest, delete_dest=False):
jadmanskica7da372008-10-21 16:26:52 +0000111 """
112 Copy files from the remote host to a local path.
113
114 Directories will be copied recursively.
115 If a source component is a directory with a trailing slash,
116 the content of the directory will be copied, otherwise, the
117 directory itself and its content will be copied. This
118 behavior is similar to that of the program 'rsync'.
119
120 Args:
121 source: either
122 1) a single file or directory, as a string
123 2) a list of one or more (possibly mixed)
124 files or directories
125 dest: a file or a directory (if source contains a
126 directory or more than one element, you must
127 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000128 delete_dest: if this is true, the command will also clear
129 out any old files at dest that are not in the
130 source
jadmanskica7da372008-10-21 16:26:52 +0000131
132 Raises:
133 AutoservRunError: the scp command failed
134 """
135 if isinstance(source, basestring):
136 source = [source]
jadmanskid7b79ed2009-01-07 17:19:48 +0000137 dest = os.path.abspath(dest)
jadmanskica7da372008-10-21 16:26:52 +0000138
jadmanskid7b79ed2009-01-07 17:19:48 +0000139 try:
jadmanski2583a432009-02-10 23:59:11 +0000140 remote_source = self._encode_remote_paths(source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000141 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000142 rsync = self._make_rsync_cmd([remote_source], local_dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000143 delete_dest)
144 utils.run(rsync)
145 except error.CmdError, e:
showardb18134f2009-03-20 20:52:18 +0000146 logging.warn("warning: rsync failed with: %s", e)
147 logging.info("attempting to copy with scp instead")
jadmanskica7da372008-10-21 16:26:52 +0000148
jadmanskid7b79ed2009-01-07 17:19:48 +0000149 # scp has no equivalent to --delete, just drop the entire dest dir
150 if delete_dest and os.path.isdir(dest):
151 shutil.rmtree(dest)
152 os.mkdir(dest)
jadmanskica7da372008-10-21 16:26:52 +0000153
jadmanskid7b79ed2009-01-07 17:19:48 +0000154 remote_source = self._make_rsync_compatible_source(source, False)
155 if remote_source:
jadmanski2583a432009-02-10 23:59:11 +0000156 remote_source = self._encode_remote_paths(remote_source)
jadmanskid7b79ed2009-01-07 17:19:48 +0000157 local_dest = utils.sh_escape(dest)
jadmanski2583a432009-02-10 23:59:11 +0000158 scp = self._make_scp_cmd([remote_source], local_dest)
jadmanskid7b79ed2009-01-07 17:19:48 +0000159 try:
160 utils.run(scp)
161 except error.CmdError, e:
162 raise error.AutoservRunError(e.args[0], e.args[1])
jadmanskica7da372008-10-21 16:26:52 +0000163
164
mbligh89e258d2008-10-24 13:58:08 +0000165 def send_file(self, source, dest, delete_dest=False):
jadmanskica7da372008-10-21 16:26:52 +0000166 """
167 Copy files from a local path to the remote host.
168
169 Directories will be copied recursively.
170 If a source component is a directory with a trailing slash,
171 the content of the directory will be copied, otherwise, the
172 directory itself and its content will be copied. This
173 behavior is similar to that of the program 'rsync'.
174
175 Args:
176 source: either
177 1) a single file or directory, as a string
178 2) a list of one or more (possibly mixed)
179 files or directories
180 dest: a file or a directory (if source contains a
181 directory or more than one element, you must
182 supply a directory dest)
mbligh89e258d2008-10-24 13:58:08 +0000183 delete_dest: if this is true, the command will also clear
184 out any old files at dest that are not in the
185 source
jadmanskica7da372008-10-21 16:26:52 +0000186
187 Raises:
188 AutoservRunError: the scp command failed
189 """
190 if isinstance(source, basestring):
191 source = [source]
jadmanski2583a432009-02-10 23:59:11 +0000192 remote_dest = self._encode_remote_paths([dest])
jadmanskica7da372008-10-21 16:26:52 +0000193
jadmanskid7b79ed2009-01-07 17:19:48 +0000194 try:
jadmanski2583a432009-02-10 23:59:11 +0000195 local_sources = [utils.sh_escape(path) for path in source]
196 rsync = self._make_rsync_cmd(local_sources, remote_dest,
jadmanskid7b79ed2009-01-07 17:19:48 +0000197 delete_dest)
198 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:
205 is_dir = self.run("ls -d %s/" % remote_dest,
206 ignore_status=True).exit_status == 0
207 if is_dir:
208 cmd = "rm -rf %s && mkdir %s"
209 cmd %= (remote_dest, remote_dest)
210 self.run(cmd)
jadmanskica7da372008-10-21 16:26:52 +0000211
jadmanski2583a432009-02-10 23:59:11 +0000212 local_sources = self._make_rsync_compatible_source(source, True)
213 if local_sources:
214 scp = self._make_scp_cmd(local_sources, remote_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])
219
jadmanskica7da372008-10-21 16:26:52 +0000220 self.run('find "%s" -type d -print0 | xargs -0r chmod o+rx' % dest)
221 self.run('find "%s" -type f -print0 | xargs -0r chmod o+r' % dest)
222 if self.target_file_owner:
223 self.run('chown -R %s %s' % (self.target_file_owner, dest))
224
225
226 def ssh_ping(self, timeout=60):
227 try:
228 self.run("true", timeout=timeout, connect_timeout=timeout)
showardb18134f2009-03-20 20:52:18 +0000229 logging.info("ssh_ping of %s completed sucessfully", self.hostname)
jadmanskica7da372008-10-21 16:26:52 +0000230 except error.AutoservSSHTimeout:
231 msg = "ssh ping timed out (timeout = %d)" % timeout
232 raise error.AutoservSSHTimeout(msg)
mbligh9d738d62009-03-09 21:17:10 +0000233 except error.AutoservSshPermissionDeniedError:
234 #let AutoservSshPermissionDeniedError be visible to the callers
235 raise
jadmanskica7da372008-10-21 16:26:52 +0000236 except error.AutoservRunError, e:
237 msg = "command true failed in ssh ping"
238 raise error.AutoservRunError(msg, e.result_obj)
239
240
241 def is_up(self):
242 """
243 Check if the remote host is up.
244
245 Returns:
246 True if the remote host is up, False otherwise
247 """
248 try:
249 self.ssh_ping()
250 except error.AutoservError:
251 return False
252 else:
253 return True
254
255
256 def wait_up(self, timeout=None):
257 """
258 Wait until the remote host is up or the timeout expires.
259
260 In fact, it will wait until an ssh connection to the remote
261 host can be established, and getty is running.
262
263 Args:
264 timeout: time limit in seconds before returning even
265 if the host is not up.
266
267 Returns:
268 True if the host was found to be up, False otherwise
269 """
270 if timeout:
271 end_time = time.time() + timeout
272
273 while not timeout or time.time() < end_time:
274 if self.is_up():
275 try:
276 if self.are_wait_up_processes_up():
277 return True
278 except error.AutoservError:
279 pass
280 time.sleep(1)
281
282 return False
283
284
285 def wait_down(self, timeout=None):
286 """
287 Wait until the remote host is down or the timeout expires.
288
289 In fact, it will wait until an ssh connection to the remote
290 host fails.
291
292 Args:
293 timeout: time limit in seconds before returning even
294 if the host is not up.
295
296 Returns:
297 True if the host was found to be down, False otherwise
298 """
299 if timeout:
300 end_time = time.time() + timeout
301
302 while not timeout or time.time() < end_time:
303 if not self.is_up():
304 return True
305 time.sleep(1)
306
307 return False
jadmanskif6562912008-10-21 17:59:01 +0000308
mbligha0a27592009-01-24 01:41:36 +0000309
jadmanskif6562912008-10-21 17:59:01 +0000310 # tunable constants for the verify & repair code
311 AUTOTEST_GB_DISKSPACE_REQUIRED = 20
mbligha0a27592009-01-24 01:41:36 +0000312
jadmanskif6562912008-10-21 17:59:01 +0000313
314 def verify(self):
mblighb49b5232009-02-12 21:54:49 +0000315 super(AbstractSSHHost, self).verify_hardware()
jadmanskif6562912008-10-21 17:59:01 +0000316
showardb18134f2009-03-20 20:52:18 +0000317 logging.info('Pinging host ' + self.hostname)
jadmanskif6562912008-10-21 17:59:01 +0000318 self.ssh_ping()
319
jadmanski80deb752009-01-21 17:14:16 +0000320 if self.is_shutting_down():
321 raise error.AutoservHostError("Host is shutting down")
322
mblighb49b5232009-02-12 21:54:49 +0000323 super(AbstractSSHHost, self).verify_software()
324
jadmanskif6562912008-10-21 17:59:01 +0000325 try:
326 autodir = autotest._get_autodir(self)
327 if autodir:
jadmanskif6562912008-10-21 17:59:01 +0000328 self.check_diskspace(autodir,
329 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
330 except error.AutoservHostError:
331 raise # only want to raise if it's a space issue
332 except Exception:
333 pass # autotest dir may not exist, etc. ignore
334
335
showard170873e2009-01-07 00:22:26 +0000336class LoggerFile(object):
337 def write(self, data):
jadmanskifcc851b2009-01-07 17:31:12 +0000338 if data:
showardb18134f2009-03-20 20:52:18 +0000339 logging.debug(data.rstrip("\n"))
showard170873e2009-01-07 00:22:26 +0000340
341
342 def flush(self):
343 pass