blob: 9595cc82adaad7c80eac825b4ec722d8108a1eea [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
mblighb7681262008-11-20 20:11:47 +000029def list_remote_pythons(host):
30 """List out installed pythons on host."""
mblighdc603752010-03-04 21:08:44 +000031 result = host.run('ls /usr/bin/python[0-9]*')
mblighb7681262008-11-20 20:11:47 +000032 return result.stdout.splitlines()
33
34
35def select_supported_python(installed_pythons):
36 """Select a supported python from a list"""
37 for python in installed_pythons:
38 if python[-3:] in SUPPORTED_PYTHON_VERS:
39 return python
40
41
mblighe48bcfb2008-11-11 17:09:44 +000042def copy_monitordir(host):
43 """Copy over monitordir to a tmpdir on the remote host."""
44 tmp_dir = host.get_tmp_dir()
45 host.send_file(MONITORDIR, tmp_dir)
46 return os.path.join(tmp_dir, 'monitors')
47
48
49def launch_remote_followfiles(host, lastlines_dirpath, follow_paths):
50 """Launch followfiles.py remotely on follow_paths."""
mblighd0e94982009-07-11 00:15:18 +000051 logging.info('Launching followfiles on target: %s, %s, %s',
showardf1175bb2009-06-17 19:34:36 +000052 host.hostname, lastlines_dirpath, str(follow_paths))
mblighb7681262008-11-20 20:11:47 +000053
54 # First make sure a supported Python is on host
55 installed_pythons = list_remote_pythons(host)
56 supported_python = select_supported_python(installed_pythons)
57 if not supported_python:
58 if DEFAULT_PYTHON in installed_pythons:
mblighd0e94982009-07-11 00:15:18 +000059 logging.info('No versioned Python binary found, '
60 'defaulting to: %s', DEFAULT_PYTHON)
mblighb7681262008-11-20 20:11:47 +000061 supported_python = DEFAULT_PYTHON
62 else:
63 raise FollowFilesLaunchError('No supported Python on host.')
64
mblighe48bcfb2008-11-11 17:09:44 +000065 remote_monitordir = copy_monitordir(host)
Eric Li7edb3042011-01-06 17:57:17 -080066 remote_script_path = os.path.join(remote_monitordir, 'followfiles.py')
mblighe48bcfb2008-11-11 17:09:44 +000067
68 followfiles_cmd = '%s %s --lastlines_dirpath=%s %s' % (
mblighb7681262008-11-20 20:11:47 +000069 supported_python, remote_script_path,
mblighe48bcfb2008-11-11 17:09:44 +000070 lastlines_dirpath, ' '.join(follow_paths))
71
Eric Li7edb3042011-01-06 17:57:17 -080072 remote_ff_proc = subprocess.Popen(host._make_ssh_cmd(followfiles_cmd),
73 stdin=open(os.devnull, 'r'),
74 stdout=subprocess.PIPE, shell=True)
75
76
mblighb7681262008-11-20 20:11:47 +000077 # Give it enough time to crash if it's going to (it shouldn't).
78 time.sleep(5)
Eric Li7edb3042011-01-06 17:57:17 -080079 doa = remote_ff_proc.poll()
mblighb7681262008-11-20 20:11:47 +000080 if doa:
mblighd0e94982009-07-11 00:15:18 +000081 raise FollowFilesLaunchError('ssh command crashed.')
mblighb7681262008-11-20 20:11:47 +000082
Eric Li7edb3042011-01-06 17:57:17 -080083 return remote_ff_proc
mblighe48bcfb2008-11-11 17:09:44 +000084
85
86def resolve_patterns_path(patterns_path):
87 """Resolve patterns_path to existing absolute local path or raise.
88
89 As a convenience we allow users to specify a non-absolute patterns_path.
90 However these need to be resolved before allowing them to be passed down
91 to console.py.
92
93 For now we expect non-absolute ones to be in self.monitordir.
94 """
95 if os.path.isabs(patterns_path):
96 if os.path.exists(patterns_path):
97 return patterns_path
98 else:
99 raise InvalidPatternsPathError('Absolute path does not exist.')
100 else:
101 patterns_path = os.path.join(MONITORDIR, patterns_path)
102 if os.path.exists(patterns_path):
103 return patterns_path
104 else:
105 raise InvalidPatternsPathError('Relative path does not exist.')
106
107
108def launch_local_console(
109 input_stream, console_log_path, pattern_paths=None):
110 """Launch console.py locally.
111
112 This will process the output from followfiles and
113 fire warning messages per configuration in pattern_paths.
114 """
115 r, w = os.pipe()
116 local_script_path = os.path.join(MONITORDIR, 'console.py')
117 console_cmd = [sys.executable, local_script_path]
118 if pattern_paths:
119 console_cmd.append('--pattern_paths=%s' % ','.join(pattern_paths))
120
121 console_cmd += [console_log_path, str(w)]
122
123 # Setup warning stream before we actually launch
124 warning_stream = os.fdopen(r, 'r', 0)
125
126 devnull_r = open(os.devnull, 'r')
127 devnull_w = open(os.devnull, 'w')
128 # Launch console.py locally
129 console_proc = subprocess.Popen(
130 console_cmd, stdin=input_stream,
131 stdout=devnull_w, stderr=devnull_w)
132 os.close(w)
133 return console_proc, warning_stream
134
135
136def _log_and_ignore_exceptions(f):
137 """Decorator: automatically log exception during a method call.
138 """
139 def wrapped(self, *args, **dargs):
140 try:
141 return f(self, *args, **dargs)
142 except Exception, e:
143 print "LogfileMonitor.%s failed with exception %s" % (f.__name__, e)
144 print "Exception ignored:"
145 traceback.print_exc(file=sys.stdout)
146 wrapped.__name__ = f.__name__
147 wrapped.__doc__ = f.__doc__
148 wrapped.__dict__.update(f.__dict__)
149 return wrapped
150
151
152class LogfileMonitorMixin(abstract_ssh.AbstractSSHHost):
153 """This can monitor one or more remote files using tail.
154
155 This class and its counterpart script, monitors/followfiles.py,
156 add most functionality one would need to launch and monitor
157 remote tail processes on self.hostname.
158
159 This can be used by subclassing normally or by calling
160 NewLogfileMonitorMixin (below)
161
162 It is configured via two class attributes:
163 follow_paths: Remote paths to monitor
164 pattern_paths: Local paths to alert pattern definition files.
165 """
166 follow_paths = ()
167 pattern_paths = ()
168
169 def _initialize(self, console_log=None, *args, **dargs):
170 super(LogfileMonitorMixin, self)._initialize(*args, **dargs)
171
172 self._lastlines_dirpath = None
173 self._console_proc = None
174 self._console_log = console_log or 'logfile_monitor.log'
mblighe48bcfb2008-11-11 17:09:44 +0000175
176
177 def reboot_followup(self, *args, **dargs):
178 super(LogfileMonitorMixin, self).reboot_followup(*args, **dargs)
179 self.__stop_loggers()
180 self.__start_loggers()
181
182
183 def start_loggers(self):
184 super(LogfileMonitorMixin, self).start_loggers()
185 self.__start_loggers()
186
187
188 def remote_path_exists(self, remote_path):
189 """Return True if remote_path exists, False otherwise."""
190 return not self.run(
191 'ls %s' % remote_path, ignore_status=True).exit_status
192
193
194 def check_remote_paths(self, remote_paths):
195 """Return list of remote_paths that currently exist."""
196 return [
197 path for path in remote_paths if self.remote_path_exists(path)]
198
199
200 @_log_and_ignore_exceptions
201 def __start_loggers(self):
202 """Start multifile monitoring logger.
203
204 Launch monitors/followfiles.py on the target and hook its output
205 to monitors/console.py locally.
206 """
207 # Check if follow_paths exist, in the case that one doesn't
208 # emit a warning and proceed.
209 follow_paths_set = set(self.follow_paths)
210 existing = self.check_remote_paths(follow_paths_set)
211 missing = follow_paths_set.difference(existing)
212 if missing:
213 # Log warning that we are missing expected remote paths.
mblighd0e94982009-07-11 00:15:18 +0000214 logging.warn('Target %s is missing expected remote paths: %s',
showardf1175bb2009-06-17 19:34:36 +0000215 self.hostname, ', '.join(missing))
mblighe48bcfb2008-11-11 17:09:44 +0000216
217 # If none of them exist just return (for now).
218 if not existing:
219 return
220
221 # Create a new lastlines_dirpath on the remote host if not already set.
222 if not self._lastlines_dirpath:
mbligh130a1dc2009-09-10 00:23:58 +0000223 self._lastlines_dirpath = self.get_tmp_dir(parent='/var/tmp')
mblighe48bcfb2008-11-11 17:09:44 +0000224
225 # Launch followfiles on target
mblighb7681262008-11-20 20:11:47 +0000226 try:
227 self._followfiles_proc = launch_remote_followfiles(
228 self, self._lastlines_dirpath, existing)
229 except FollowFilesLaunchError:
230 # We're hosed, there is no point in proceeding.
showardf1175bb2009-06-17 19:34:36 +0000231 logging.fatal('Failed to launch followfiles on target,'
232 ' aborting logfile monitoring: %s', self.hostname)
mblighb7681262008-11-20 20:11:47 +0000233 if self.job:
234 # Put a warning in the status.log
235 self.job.record(
236 'WARN', None, 'logfile.monitor',
237 'followfiles launch failed')
238 return
mblighe48bcfb2008-11-11 17:09:44 +0000239
240 # Ensure we have sane pattern_paths before launching console.py
241 sane_pattern_paths = []
242 for patterns_path in set(self.pattern_paths):
243 try:
244 patterns_path = resolve_patterns_path(patterns_path)
245 except InvalidPatternsPathError, e:
showardf1175bb2009-06-17 19:34:36 +0000246 logging.warn('Specified patterns_path is invalid: %s, %s',
247 patterns_path, str(e))
mblighe48bcfb2008-11-11 17:09:44 +0000248 else:
249 sane_pattern_paths.append(patterns_path)
250
251 # Launch console.py locally, pass in output stream from followfiles.
252 self._console_proc, self._logfile_warning_stream = \
253 launch_local_console(
254 self._followfiles_proc.stdout, self._console_log,
255 sane_pattern_paths)
256
257 if self.job:
258 self.job.warning_loggers.add(self._logfile_warning_stream)
259
260
261 def stop_loggers(self):
262 super(LogfileMonitorMixin, self).stop_loggers()
263 self.__stop_loggers()
264
265
266 @_log_and_ignore_exceptions
267 def __stop_loggers(self):
268 if self._console_proc:
269 utils.nuke_subprocess(self._console_proc)
270 utils.nuke_subprocess(self._followfiles_proc)
271 self._console_proc = self._followfile_proc = None
272 if self.job:
273 self.job.warning_loggers.discard(self._logfile_warning_stream)
274 self._logfile_warning_stream.close()
275
276
277def NewLogfileMonitorMixin(follow_paths, pattern_paths=None):
278 """Create a custom in-memory subclass of LogfileMonitorMixin.
279
280 Args:
281 follow_paths: list; Remote paths to tail.
282 pattern_paths: list; Local alert pattern definition files.
283 """
284 if not follow_paths or (pattern_paths and not follow_paths):
285 raise InvalidConfigurationError
286
287 return type(
288 'LogfileMonitorMixin%d' % id(follow_paths),
289 (LogfileMonitorMixin,),
290 {'follow_paths': follow_paths,
291 'pattern_paths': pattern_paths or ()})