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