mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 1 | import os, sys, subprocess, tempfile, traceback |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 2 | import time |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 3 | |
| 4 | from autotest_lib.client.common_lib import debug, utils |
| 5 | from autotest_lib.server import utils as server_utils |
| 6 | from autotest_lib.server.hosts import abstract_ssh, monitors |
| 7 | |
| 8 | MONITORDIR = monitors.__path__[0] |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 9 | SUPPORTED_PYTHON_VERS = ('2.4', '2.5', '2.6') |
| 10 | DEFAULT_PYTHON = '/usr/bin/python' |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 11 | |
| 12 | |
| 13 | class Error(Exception): |
| 14 | pass |
| 15 | |
| 16 | |
| 17 | class InvalidPatternsPathError(Error): |
| 18 | """An invalid patterns_path was specified.""" |
| 19 | |
| 20 | |
| 21 | class InvalidConfigurationError(Error): |
| 22 | """An invalid configuration was specified.""" |
| 23 | |
| 24 | |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 25 | class FollowFilesLaunchError(Error): |
| 26 | """Error occurred launching followfiles remotely.""" |
| 27 | |
| 28 | |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 29 | def 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 | |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 38 | def 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 | |
| 44 | def 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 | |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 51 | def 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 | |
| 58 | def launch_remote_followfiles(host, lastlines_dirpath, follow_paths): |
| 59 | """Launch followfiles.py remotely on follow_paths.""" |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 60 | 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 | |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 77 | 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' % ( |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 82 | supported_python, remote_script_path, |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 83 | 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) |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 90 | # 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 | |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 96 | return remote_followfiles_proc |
| 97 | |
| 98 | |
| 99 | def 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 | |
| 121 | def 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 | |
| 149 | def _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 | |
| 165 | class 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 |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame^] | 241 | 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 |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 255 | |
| 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 | |
| 294 | def 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 ()}) |