blob: 158fad9112d09a1c549892ac6227bab49899719a [file] [log] [blame]
showardf1175bb2009-06-17 19:34:36 +00001import logging, os, sys, subprocess, tempfile, traceback
mblighb7681262008-11-20 20:11:47 +00002import time
mblighe48bcfb2008-11-11 17:09:44 +00003
showardf1175bb2009-06-17 19:34:36 +00004from autotest_lib.client.common_lib import utils
mblighe48bcfb2008-11-11 17:09:44 +00005from autotest_lib.server import utils as server_utils
6from autotest_lib.server.hosts import abstract_ssh, monitors
7
8MONITORDIR = monitors.__path__[0]
mblighb7681262008-11-20 20:11:47 +00009SUPPORTED_PYTHON_VERS = ('2.4', '2.5', '2.6')
10DEFAULT_PYTHON = '/usr/bin/python'
mblighe48bcfb2008-11-11 17:09:44 +000011
12
13class Error(Exception):
14 pass
15
16
17class InvalidPatternsPathError(Error):
18 """An invalid patterns_path was specified."""
19
20
21class InvalidConfigurationError(Error):
22 """An invalid configuration was specified."""
23
24
mblighb7681262008-11-20 20:11:47 +000025class FollowFilesLaunchError(Error):
26 """Error occurred launching followfiles remotely."""
27
28
mblighe48bcfb2008-11-11 17:09:44 +000029def run_cmd_on_host(hostname, cmd, stdin, stdout, stderr):
30 base_cmd = abstract_ssh.make_ssh_command()
31 full_cmd = "%s %s \"%s\"" % (base_cmd, hostname,
32 server_utils.sh_escape(cmd))
33
34 return subprocess.Popen(full_cmd, stdin=stdin, stdout=stdout,
35 stderr=stderr, shell=True)
36
37
mblighb7681262008-11-20 20:11:47 +000038def list_remote_pythons(host):
39 """List out installed pythons on host."""
40 result = host.run('ls /usr/bin/python*')
41 return result.stdout.splitlines()
42
43
44def select_supported_python(installed_pythons):
45 """Select a supported python from a list"""
46 for python in installed_pythons:
47 if python[-3:] in SUPPORTED_PYTHON_VERS:
48 return python
49
50
mblighe48bcfb2008-11-11 17:09:44 +000051def copy_monitordir(host):
52 """Copy over monitordir to a tmpdir on the remote host."""
53 tmp_dir = host.get_tmp_dir()
54 host.send_file(MONITORDIR, tmp_dir)
55 return os.path.join(tmp_dir, 'monitors')
56
57
58def launch_remote_followfiles(host, lastlines_dirpath, follow_paths):
59 """Launch followfiles.py remotely on follow_paths."""
mblighd0e94982009-07-11 00:15:18 +000060 logging.info('Launching followfiles on target: %s, %s, %s',
showardf1175bb2009-06-17 19:34:36 +000061 host.hostname, lastlines_dirpath, str(follow_paths))
mblighb7681262008-11-20 20:11:47 +000062
63 # First make sure a supported Python is on host
64 installed_pythons = list_remote_pythons(host)
65 supported_python = select_supported_python(installed_pythons)
66 if not supported_python:
67 if DEFAULT_PYTHON in installed_pythons:
mblighd0e94982009-07-11 00:15:18 +000068 logging.info('No versioned Python binary found, '
69 'defaulting to: %s', DEFAULT_PYTHON)
mblighb7681262008-11-20 20:11:47 +000070 supported_python = DEFAULT_PYTHON
71 else:
72 raise FollowFilesLaunchError('No supported Python on host.')
73
mblighe48bcfb2008-11-11 17:09:44 +000074 remote_monitordir = copy_monitordir(host)
75 remote_script_path = os.path.join(
76 remote_monitordir, 'followfiles.py')
77
78 followfiles_cmd = '%s %s --lastlines_dirpath=%s %s' % (
mblighb7681262008-11-20 20:11:47 +000079 supported_python, remote_script_path,
mblighe48bcfb2008-11-11 17:09:44 +000080 lastlines_dirpath, ' '.join(follow_paths))
81
82 devnull_r = open(os.devnull, 'r')
83 devnull_w = open(os.devnull, 'w')
84 remote_followfiles_proc = run_cmd_on_host(
85 host.hostname, followfiles_cmd, stdout=subprocess.PIPE,
86 stdin=devnull_r, stderr=devnull_w)
mblighb7681262008-11-20 20:11:47 +000087 # Give it enough time to crash if it's going to (it shouldn't).
88 time.sleep(5)
89 doa = remote_followfiles_proc.poll()
90 if doa:
mblighd0e94982009-07-11 00:15:18 +000091 raise FollowFilesLaunchError('ssh command crashed.')
mblighb7681262008-11-20 20:11:47 +000092
mblighe48bcfb2008-11-11 17:09:44 +000093 return remote_followfiles_proc
94
95
96def resolve_patterns_path(patterns_path):
97 """Resolve patterns_path to existing absolute local path or raise.
98
99 As a convenience we allow users to specify a non-absolute patterns_path.
100 However these need to be resolved before allowing them to be passed down
101 to console.py.
102
103 For now we expect non-absolute ones to be in self.monitordir.
104 """
105 if os.path.isabs(patterns_path):
106 if os.path.exists(patterns_path):
107 return patterns_path
108 else:
109 raise InvalidPatternsPathError('Absolute path does not exist.')
110 else:
111 patterns_path = os.path.join(MONITORDIR, patterns_path)
112 if os.path.exists(patterns_path):
113 return patterns_path
114 else:
115 raise InvalidPatternsPathError('Relative path does not exist.')
116
117
118def launch_local_console(
119 input_stream, console_log_path, pattern_paths=None):
120 """Launch console.py locally.
121
122 This will process the output from followfiles and
123 fire warning messages per configuration in pattern_paths.
124 """
125 r, w = os.pipe()
126 local_script_path = os.path.join(MONITORDIR, 'console.py')
127 console_cmd = [sys.executable, local_script_path]
128 if pattern_paths:
129 console_cmd.append('--pattern_paths=%s' % ','.join(pattern_paths))
130
131 console_cmd += [console_log_path, str(w)]
132
133 # Setup warning stream before we actually launch
134 warning_stream = os.fdopen(r, 'r', 0)
135
136 devnull_r = open(os.devnull, 'r')
137 devnull_w = open(os.devnull, 'w')
138 # Launch console.py locally
139 console_proc = subprocess.Popen(
140 console_cmd, stdin=input_stream,
141 stdout=devnull_w, stderr=devnull_w)
142 os.close(w)
143 return console_proc, warning_stream
144
145
146def _log_and_ignore_exceptions(f):
147 """Decorator: automatically log exception during a method call.
148 """
149 def wrapped(self, *args, **dargs):
150 try:
151 return f(self, *args, **dargs)
152 except Exception, e:
153 print "LogfileMonitor.%s failed with exception %s" % (f.__name__, e)
154 print "Exception ignored:"
155 traceback.print_exc(file=sys.stdout)
156 wrapped.__name__ = f.__name__
157 wrapped.__doc__ = f.__doc__
158 wrapped.__dict__.update(f.__dict__)
159 return wrapped
160
161
162class LogfileMonitorMixin(abstract_ssh.AbstractSSHHost):
163 """This can monitor one or more remote files using tail.
164
165 This class and its counterpart script, monitors/followfiles.py,
166 add most functionality one would need to launch and monitor
167 remote tail processes on self.hostname.
168
169 This can be used by subclassing normally or by calling
170 NewLogfileMonitorMixin (below)
171
172 It is configured via two class attributes:
173 follow_paths: Remote paths to monitor
174 pattern_paths: Local paths to alert pattern definition files.
175 """
176 follow_paths = ()
177 pattern_paths = ()
178
179 def _initialize(self, console_log=None, *args, **dargs):
180 super(LogfileMonitorMixin, self)._initialize(*args, **dargs)
181
182 self._lastlines_dirpath = None
183 self._console_proc = None
184 self._console_log = console_log or 'logfile_monitor.log'
mblighe48bcfb2008-11-11 17:09:44 +0000185
186
187 def reboot_followup(self, *args, **dargs):
188 super(LogfileMonitorMixin, self).reboot_followup(*args, **dargs)
189 self.__stop_loggers()
190 self.__start_loggers()
191
192
193 def start_loggers(self):
194 super(LogfileMonitorMixin, self).start_loggers()
195 self.__start_loggers()
196
197
198 def remote_path_exists(self, remote_path):
199 """Return True if remote_path exists, False otherwise."""
200 return not self.run(
201 'ls %s' % remote_path, ignore_status=True).exit_status
202
203
204 def check_remote_paths(self, remote_paths):
205 """Return list of remote_paths that currently exist."""
206 return [
207 path for path in remote_paths if self.remote_path_exists(path)]
208
209
210 @_log_and_ignore_exceptions
211 def __start_loggers(self):
212 """Start multifile monitoring logger.
213
214 Launch monitors/followfiles.py on the target and hook its output
215 to monitors/console.py locally.
216 """
217 # Check if follow_paths exist, in the case that one doesn't
218 # emit a warning and proceed.
219 follow_paths_set = set(self.follow_paths)
220 existing = self.check_remote_paths(follow_paths_set)
221 missing = follow_paths_set.difference(existing)
222 if missing:
223 # Log warning that we are missing expected remote paths.
mblighd0e94982009-07-11 00:15:18 +0000224 logging.warn('Target %s is missing expected remote paths: %s',
showardf1175bb2009-06-17 19:34:36 +0000225 self.hostname, ', '.join(missing))
mblighe48bcfb2008-11-11 17:09:44 +0000226
227 # If none of them exist just return (for now).
228 if not existing:
229 return
230
231 # Create a new lastlines_dirpath on the remote host if not already set.
232 if not self._lastlines_dirpath:
233 self._lastlines_dirpath = self.get_tmp_dir(parent='/var/log')
234
235 # Launch followfiles on target
mblighb7681262008-11-20 20:11:47 +0000236 try:
237 self._followfiles_proc = launch_remote_followfiles(
238 self, self._lastlines_dirpath, existing)
239 except FollowFilesLaunchError:
240 # We're hosed, there is no point in proceeding.
showardf1175bb2009-06-17 19:34:36 +0000241 logging.fatal('Failed to launch followfiles on target,'
242 ' aborting logfile monitoring: %s', self.hostname)
mblighb7681262008-11-20 20:11:47 +0000243 if self.job:
244 # Put a warning in the status.log
245 self.job.record(
246 'WARN', None, 'logfile.monitor',
247 'followfiles launch failed')
248 return
mblighe48bcfb2008-11-11 17:09:44 +0000249
250 # Ensure we have sane pattern_paths before launching console.py
251 sane_pattern_paths = []
252 for patterns_path in set(self.pattern_paths):
253 try:
254 patterns_path = resolve_patterns_path(patterns_path)
255 except InvalidPatternsPathError, e:
showardf1175bb2009-06-17 19:34:36 +0000256 logging.warn('Specified patterns_path is invalid: %s, %s',
257 patterns_path, str(e))
mblighe48bcfb2008-11-11 17:09:44 +0000258 else:
259 sane_pattern_paths.append(patterns_path)
260
261 # Launch console.py locally, pass in output stream from followfiles.
262 self._console_proc, self._logfile_warning_stream = \
263 launch_local_console(
264 self._followfiles_proc.stdout, self._console_log,
265 sane_pattern_paths)
266
267 if self.job:
268 self.job.warning_loggers.add(self._logfile_warning_stream)
269
270
271 def stop_loggers(self):
272 super(LogfileMonitorMixin, self).stop_loggers()
273 self.__stop_loggers()
274
275
276 @_log_and_ignore_exceptions
277 def __stop_loggers(self):
278 if self._console_proc:
279 utils.nuke_subprocess(self._console_proc)
280 utils.nuke_subprocess(self._followfiles_proc)
281 self._console_proc = self._followfile_proc = None
282 if self.job:
283 self.job.warning_loggers.discard(self._logfile_warning_stream)
284 self._logfile_warning_stream.close()
285
286
287def NewLogfileMonitorMixin(follow_paths, pattern_paths=None):
288 """Create a custom in-memory subclass of LogfileMonitorMixin.
289
290 Args:
291 follow_paths: list; Remote paths to tail.
292 pattern_paths: list; Local alert pattern definition files.
293 """
294 if not follow_paths or (pattern_paths and not follow_paths):
295 raise InvalidConfigurationError
296
297 return type(
298 'LogfileMonitorMixin%d' % id(follow_paths),
299 (LogfileMonitorMixin,),
300 {'follow_paths': follow_paths,
301 'pattern_paths': pattern_paths or ()})