blob: fbb70e2c3fe6847b052b125ad5819c09a14b2d56 [file] [log] [blame]
mblighe48bcfb2008-11-11 17:09:44 +00001import os, sys, subprocess, tempfile, traceback
2
3from autotest_lib.client.common_lib import debug, utils
4from autotest_lib.server import utils as server_utils
5from autotest_lib.server.hosts import abstract_ssh, monitors
6
7MONITORDIR = monitors.__path__[0]
8
9
10class Error(Exception):
11 pass
12
13
14class InvalidPatternsPathError(Error):
15 """An invalid patterns_path was specified."""
16
17
18class InvalidConfigurationError(Error):
19 """An invalid configuration was specified."""
20
21
22def run_cmd_on_host(hostname, cmd, stdin, stdout, stderr):
23 base_cmd = abstract_ssh.make_ssh_command()
24 full_cmd = "%s %s \"%s\"" % (base_cmd, hostname,
25 server_utils.sh_escape(cmd))
26
27 return subprocess.Popen(full_cmd, stdin=stdin, stdout=stdout,
28 stderr=stderr, shell=True)
29
30
31def copy_monitordir(host):
32 """Copy over monitordir to a tmpdir on the remote host."""
33 tmp_dir = host.get_tmp_dir()
34 host.send_file(MONITORDIR, tmp_dir)
35 return os.path.join(tmp_dir, 'monitors')
36
37
38def launch_remote_followfiles(host, lastlines_dirpath, follow_paths):
39 """Launch followfiles.py remotely on follow_paths."""
40 remote_monitordir = copy_monitordir(host)
41 remote_script_path = os.path.join(
42 remote_monitordir, 'followfiles.py')
43
44 followfiles_cmd = '%s %s --lastlines_dirpath=%s %s' % (
45 sys.executable, remote_script_path,
46 lastlines_dirpath, ' '.join(follow_paths))
47
48 devnull_r = open(os.devnull, 'r')
49 devnull_w = open(os.devnull, 'w')
50 remote_followfiles_proc = run_cmd_on_host(
51 host.hostname, followfiles_cmd, stdout=subprocess.PIPE,
52 stdin=devnull_r, stderr=devnull_w)
53 return remote_followfiles_proc
54
55
56def resolve_patterns_path(patterns_path):
57 """Resolve patterns_path to existing absolute local path or raise.
58
59 As a convenience we allow users to specify a non-absolute patterns_path.
60 However these need to be resolved before allowing them to be passed down
61 to console.py.
62
63 For now we expect non-absolute ones to be in self.monitordir.
64 """
65 if os.path.isabs(patterns_path):
66 if os.path.exists(patterns_path):
67 return patterns_path
68 else:
69 raise InvalidPatternsPathError('Absolute path does not exist.')
70 else:
71 patterns_path = os.path.join(MONITORDIR, patterns_path)
72 if os.path.exists(patterns_path):
73 return patterns_path
74 else:
75 raise InvalidPatternsPathError('Relative path does not exist.')
76
77
78def launch_local_console(
79 input_stream, console_log_path, pattern_paths=None):
80 """Launch console.py locally.
81
82 This will process the output from followfiles and
83 fire warning messages per configuration in pattern_paths.
84 """
85 r, w = os.pipe()
86 local_script_path = os.path.join(MONITORDIR, 'console.py')
87 console_cmd = [sys.executable, local_script_path]
88 if pattern_paths:
89 console_cmd.append('--pattern_paths=%s' % ','.join(pattern_paths))
90
91 console_cmd += [console_log_path, str(w)]
92
93 # Setup warning stream before we actually launch
94 warning_stream = os.fdopen(r, 'r', 0)
95
96 devnull_r = open(os.devnull, 'r')
97 devnull_w = open(os.devnull, 'w')
98 # Launch console.py locally
99 console_proc = subprocess.Popen(
100 console_cmd, stdin=input_stream,
101 stdout=devnull_w, stderr=devnull_w)
102 os.close(w)
103 return console_proc, warning_stream
104
105
106def _log_and_ignore_exceptions(f):
107 """Decorator: automatically log exception during a method call.
108 """
109 def wrapped(self, *args, **dargs):
110 try:
111 return f(self, *args, **dargs)
112 except Exception, e:
113 print "LogfileMonitor.%s failed with exception %s" % (f.__name__, e)
114 print "Exception ignored:"
115 traceback.print_exc(file=sys.stdout)
116 wrapped.__name__ = f.__name__
117 wrapped.__doc__ = f.__doc__
118 wrapped.__dict__.update(f.__dict__)
119 return wrapped
120
121
122class LogfileMonitorMixin(abstract_ssh.AbstractSSHHost):
123 """This can monitor one or more remote files using tail.
124
125 This class and its counterpart script, monitors/followfiles.py,
126 add most functionality one would need to launch and monitor
127 remote tail processes on self.hostname.
128
129 This can be used by subclassing normally or by calling
130 NewLogfileMonitorMixin (below)
131
132 It is configured via two class attributes:
133 follow_paths: Remote paths to monitor
134 pattern_paths: Local paths to alert pattern definition files.
135 """
136 follow_paths = ()
137 pattern_paths = ()
138
139 def _initialize(self, console_log=None, *args, **dargs):
140 super(LogfileMonitorMixin, self)._initialize(*args, **dargs)
141
142 self._lastlines_dirpath = None
143 self._console_proc = None
144 self._console_log = console_log or 'logfile_monitor.log'
145 self.logfile_monitor_log = debug.get_logger()
146
147
148 def reboot_followup(self, *args, **dargs):
149 super(LogfileMonitorMixin, self).reboot_followup(*args, **dargs)
150 self.__stop_loggers()
151 self.__start_loggers()
152
153
154 def start_loggers(self):
155 super(LogfileMonitorMixin, self).start_loggers()
156 self.__start_loggers()
157
158
159 def remote_path_exists(self, remote_path):
160 """Return True if remote_path exists, False otherwise."""
161 return not self.run(
162 'ls %s' % remote_path, ignore_status=True).exit_status
163
164
165 def check_remote_paths(self, remote_paths):
166 """Return list of remote_paths that currently exist."""
167 return [
168 path for path in remote_paths if self.remote_path_exists(path)]
169
170
171 @_log_and_ignore_exceptions
172 def __start_loggers(self):
173 """Start multifile monitoring logger.
174
175 Launch monitors/followfiles.py on the target and hook its output
176 to monitors/console.py locally.
177 """
178 # Check if follow_paths exist, in the case that one doesn't
179 # emit a warning and proceed.
180 follow_paths_set = set(self.follow_paths)
181 existing = self.check_remote_paths(follow_paths_set)
182 missing = follow_paths_set.difference(existing)
183 if missing:
184 # Log warning that we are missing expected remote paths.
185 self.logfile_monitor_log.warn(
186 'Target(%s) missing expected remote paths: %s',
187 self.hostname, ', '.join(missing))
188
189 # If none of them exist just return (for now).
190 if not existing:
191 return
192
193 # Create a new lastlines_dirpath on the remote host if not already set.
194 if not self._lastlines_dirpath:
195 self._lastlines_dirpath = self.get_tmp_dir(parent='/var/log')
196
197 # Launch followfiles on target
198 self._followfiles_proc = launch_remote_followfiles(
199 self, self._lastlines_dirpath, existing)
200
201 # Ensure we have sane pattern_paths before launching console.py
202 sane_pattern_paths = []
203 for patterns_path in set(self.pattern_paths):
204 try:
205 patterns_path = resolve_patterns_path(patterns_path)
206 except InvalidPatternsPathError, e:
207 self.logfile_monitor_log.warn(
208 'Specified patterns_path is invalid: %s, %s',
209 patterns_path, str(e))
210 else:
211 sane_pattern_paths.append(patterns_path)
212
213 # Launch console.py locally, pass in output stream from followfiles.
214 self._console_proc, self._logfile_warning_stream = \
215 launch_local_console(
216 self._followfiles_proc.stdout, self._console_log,
217 sane_pattern_paths)
218
219 if self.job:
220 self.job.warning_loggers.add(self._logfile_warning_stream)
221
222
223 def stop_loggers(self):
224 super(LogfileMonitorMixin, self).stop_loggers()
225 self.__stop_loggers()
226
227
228 @_log_and_ignore_exceptions
229 def __stop_loggers(self):
230 if self._console_proc:
231 utils.nuke_subprocess(self._console_proc)
232 utils.nuke_subprocess(self._followfiles_proc)
233 self._console_proc = self._followfile_proc = None
234 if self.job:
235 self.job.warning_loggers.discard(self._logfile_warning_stream)
236 self._logfile_warning_stream.close()
237
238
239def NewLogfileMonitorMixin(follow_paths, pattern_paths=None):
240 """Create a custom in-memory subclass of LogfileMonitorMixin.
241
242 Args:
243 follow_paths: list; Remote paths to tail.
244 pattern_paths: list; Local alert pattern definition files.
245 """
246 if not follow_paths or (pattern_paths and not follow_paths):
247 raise InvalidConfigurationError
248
249 return type(
250 'LogfileMonitorMixin%d' % id(follow_paths),
251 (LogfileMonitorMixin,),
252 {'follow_paths': follow_paths,
253 'pattern_paths': pattern_paths or ()})