blob: b8e030a480e50725dfea7558589adef34cdc013f [file] [log] [blame]
mblighe48bcfb2008-11-11 17:09:44 +00001import os, sys, subprocess, tempfile, traceback
mblighb7681262008-11-20 20:11:47 +00002import time
mblighe48bcfb2008-11-11 17:09:44 +00003
4from autotest_lib.client.common_lib import debug, utils
5from 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."""
mblighb7681262008-11-20 20:11:47 +000060 logfile_monitor_log = debug.get_logger()
61 logfile_monitor_log.info(
62 'logfile_monitor: Launching followfiles on target: %s, %s, %s',
63 host.hostname, lastlines_dirpath, str(follow_paths))
64
65 # First make sure a supported Python is on host
66 installed_pythons = list_remote_pythons(host)
67 supported_python = select_supported_python(installed_pythons)
68 if not supported_python:
69 if DEFAULT_PYTHON in installed_pythons:
70 logfile_monitor_log.info(
71 'logfile_monitor: No versioned Python binary found,'
72 ' defaulting to: %s', DEFAULT_PYTHON)
73 supported_python = DEFAULT_PYTHON
74 else:
75 raise FollowFilesLaunchError('No supported Python on host.')
76
mblighe48bcfb2008-11-11 17:09:44 +000077 remote_monitordir = copy_monitordir(host)
78 remote_script_path = os.path.join(
79 remote_monitordir, 'followfiles.py')
80
81 followfiles_cmd = '%s %s --lastlines_dirpath=%s %s' % (
mblighb7681262008-11-20 20:11:47 +000082 supported_python, remote_script_path,
mblighe48bcfb2008-11-11 17:09:44 +000083 lastlines_dirpath, ' '.join(follow_paths))
84
85 devnull_r = open(os.devnull, 'r')
86 devnull_w = open(os.devnull, 'w')
87 remote_followfiles_proc = run_cmd_on_host(
88 host.hostname, followfiles_cmd, stdout=subprocess.PIPE,
89 stdin=devnull_r, stderr=devnull_w)
mblighb7681262008-11-20 20:11:47 +000090 # Give it enough time to crash if it's going to (it shouldn't).
91 time.sleep(5)
92 doa = remote_followfiles_proc.poll()
93 if doa:
94 raise FollowFilesLaunchError('SSH command crashed.')
95
mblighe48bcfb2008-11-11 17:09:44 +000096 return remote_followfiles_proc
97
98
99def resolve_patterns_path(patterns_path):
100 """Resolve patterns_path to existing absolute local path or raise.
101
102 As a convenience we allow users to specify a non-absolute patterns_path.
103 However these need to be resolved before allowing them to be passed down
104 to console.py.
105
106 For now we expect non-absolute ones to be in self.monitordir.
107 """
108 if os.path.isabs(patterns_path):
109 if os.path.exists(patterns_path):
110 return patterns_path
111 else:
112 raise InvalidPatternsPathError('Absolute path does not exist.')
113 else:
114 patterns_path = os.path.join(MONITORDIR, patterns_path)
115 if os.path.exists(patterns_path):
116 return patterns_path
117 else:
118 raise InvalidPatternsPathError('Relative path does not exist.')
119
120
121def launch_local_console(
122 input_stream, console_log_path, pattern_paths=None):
123 """Launch console.py locally.
124
125 This will process the output from followfiles and
126 fire warning messages per configuration in pattern_paths.
127 """
128 r, w = os.pipe()
129 local_script_path = os.path.join(MONITORDIR, 'console.py')
130 console_cmd = [sys.executable, local_script_path]
131 if pattern_paths:
132 console_cmd.append('--pattern_paths=%s' % ','.join(pattern_paths))
133
134 console_cmd += [console_log_path, str(w)]
135
136 # Setup warning stream before we actually launch
137 warning_stream = os.fdopen(r, 'r', 0)
138
139 devnull_r = open(os.devnull, 'r')
140 devnull_w = open(os.devnull, 'w')
141 # Launch console.py locally
142 console_proc = subprocess.Popen(
143 console_cmd, stdin=input_stream,
144 stdout=devnull_w, stderr=devnull_w)
145 os.close(w)
146 return console_proc, warning_stream
147
148
149def _log_and_ignore_exceptions(f):
150 """Decorator: automatically log exception during a method call.
151 """
152 def wrapped(self, *args, **dargs):
153 try:
154 return f(self, *args, **dargs)
155 except Exception, e:
156 print "LogfileMonitor.%s failed with exception %s" % (f.__name__, e)
157 print "Exception ignored:"
158 traceback.print_exc(file=sys.stdout)
159 wrapped.__name__ = f.__name__
160 wrapped.__doc__ = f.__doc__
161 wrapped.__dict__.update(f.__dict__)
162 return wrapped
163
164
165class LogfileMonitorMixin(abstract_ssh.AbstractSSHHost):
166 """This can monitor one or more remote files using tail.
167
168 This class and its counterpart script, monitors/followfiles.py,
169 add most functionality one would need to launch and monitor
170 remote tail processes on self.hostname.
171
172 This can be used by subclassing normally or by calling
173 NewLogfileMonitorMixin (below)
174
175 It is configured via two class attributes:
176 follow_paths: Remote paths to monitor
177 pattern_paths: Local paths to alert pattern definition files.
178 """
179 follow_paths = ()
180 pattern_paths = ()
181
182 def _initialize(self, console_log=None, *args, **dargs):
183 super(LogfileMonitorMixin, self)._initialize(*args, **dargs)
184
185 self._lastlines_dirpath = None
186 self._console_proc = None
187 self._console_log = console_log or 'logfile_monitor.log'
188 self.logfile_monitor_log = debug.get_logger()
189
190
191 def reboot_followup(self, *args, **dargs):
192 super(LogfileMonitorMixin, self).reboot_followup(*args, **dargs)
193 self.__stop_loggers()
194 self.__start_loggers()
195
196
197 def start_loggers(self):
198 super(LogfileMonitorMixin, self).start_loggers()
199 self.__start_loggers()
200
201
202 def remote_path_exists(self, remote_path):
203 """Return True if remote_path exists, False otherwise."""
204 return not self.run(
205 'ls %s' % remote_path, ignore_status=True).exit_status
206
207
208 def check_remote_paths(self, remote_paths):
209 """Return list of remote_paths that currently exist."""
210 return [
211 path for path in remote_paths if self.remote_path_exists(path)]
212
213
214 @_log_and_ignore_exceptions
215 def __start_loggers(self):
216 """Start multifile monitoring logger.
217
218 Launch monitors/followfiles.py on the target and hook its output
219 to monitors/console.py locally.
220 """
221 # Check if follow_paths exist, in the case that one doesn't
222 # emit a warning and proceed.
223 follow_paths_set = set(self.follow_paths)
224 existing = self.check_remote_paths(follow_paths_set)
225 missing = follow_paths_set.difference(existing)
226 if missing:
227 # Log warning that we are missing expected remote paths.
228 self.logfile_monitor_log.warn(
229 'Target(%s) missing expected remote paths: %s',
230 self.hostname, ', '.join(missing))
231
232 # If none of them exist just return (for now).
233 if not existing:
234 return
235
236 # Create a new lastlines_dirpath on the remote host if not already set.
237 if not self._lastlines_dirpath:
238 self._lastlines_dirpath = self.get_tmp_dir(parent='/var/log')
239
240 # Launch followfiles on target
mblighb7681262008-11-20 20:11:47 +0000241 try:
242 self._followfiles_proc = launch_remote_followfiles(
243 self, self._lastlines_dirpath, existing)
244 except FollowFilesLaunchError:
245 # We're hosed, there is no point in proceeding.
246 self.logfile_monitor_log.fatal(
247 'Failed to launch followfiles on target,'
248 ' aborting logfile monitoring: %s', self.hostname)
249 if self.job:
250 # Put a warning in the status.log
251 self.job.record(
252 'WARN', None, 'logfile.monitor',
253 'followfiles launch failed')
254 return
mblighe48bcfb2008-11-11 17:09:44 +0000255
256 # Ensure we have sane pattern_paths before launching console.py
257 sane_pattern_paths = []
258 for patterns_path in set(self.pattern_paths):
259 try:
260 patterns_path = resolve_patterns_path(patterns_path)
261 except InvalidPatternsPathError, e:
262 self.logfile_monitor_log.warn(
263 'Specified patterns_path is invalid: %s, %s',
264 patterns_path, str(e))
265 else:
266 sane_pattern_paths.append(patterns_path)
267
268 # Launch console.py locally, pass in output stream from followfiles.
269 self._console_proc, self._logfile_warning_stream = \
270 launch_local_console(
271 self._followfiles_proc.stdout, self._console_log,
272 sane_pattern_paths)
273
274 if self.job:
275 self.job.warning_loggers.add(self._logfile_warning_stream)
276
277
278 def stop_loggers(self):
279 super(LogfileMonitorMixin, self).stop_loggers()
280 self.__stop_loggers()
281
282
283 @_log_and_ignore_exceptions
284 def __stop_loggers(self):
285 if self._console_proc:
286 utils.nuke_subprocess(self._console_proc)
287 utils.nuke_subprocess(self._followfiles_proc)
288 self._console_proc = self._followfile_proc = None
289 if self.job:
290 self.job.warning_loggers.discard(self._logfile_warning_stream)
291 self._logfile_warning_stream.close()
292
293
294def NewLogfileMonitorMixin(follow_paths, pattern_paths=None):
295 """Create a custom in-memory subclass of LogfileMonitorMixin.
296
297 Args:
298 follow_paths: list; Remote paths to tail.
299 pattern_paths: list; Local alert pattern definition files.
300 """
301 if not follow_paths or (pattern_paths and not follow_paths):
302 raise InvalidConfigurationError
303
304 return type(
305 'LogfileMonitorMixin%d' % id(follow_paths),
306 (LogfileMonitorMixin,),
307 {'follow_paths': follow_paths,
308 'pattern_paths': pattern_paths or ()})