mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame^] | 1 | import os, sys, subprocess, tempfile, traceback |
| 2 | |
| 3 | from autotest_lib.client.common_lib import debug, utils |
| 4 | from autotest_lib.server import utils as server_utils |
| 5 | from autotest_lib.server.hosts import abstract_ssh, monitors |
| 6 | |
| 7 | MONITORDIR = monitors.__path__[0] |
| 8 | |
| 9 | |
| 10 | class Error(Exception): |
| 11 | pass |
| 12 | |
| 13 | |
| 14 | class InvalidPatternsPathError(Error): |
| 15 | """An invalid patterns_path was specified.""" |
| 16 | |
| 17 | |
| 18 | class InvalidConfigurationError(Error): |
| 19 | """An invalid configuration was specified.""" |
| 20 | |
| 21 | |
| 22 | def 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 | |
| 31 | def 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 | |
| 38 | def 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 | |
| 56 | def 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 | |
| 78 | def 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 | |
| 106 | def _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 | |
| 122 | class 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 | |
| 239 | def 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 ()}) |