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 | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 29 | def list_remote_pythons(host): |
| 30 | """List out installed pythons on host.""" |
mbligh | dc60375 | 2010-03-04 21:08:44 +0000 | [diff] [blame] | 31 | result = host.run('ls /usr/bin/python[0-9]*') |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 32 | return result.stdout.splitlines() |
| 33 | |
| 34 | |
| 35 | def 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 | |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 42 | def 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 | |
| 49 | def launch_remote_followfiles(host, lastlines_dirpath, follow_paths): |
| 50 | """Launch followfiles.py remotely on follow_paths.""" |
mbligh | d0e9498 | 2009-07-11 00:15:18 +0000 | [diff] [blame] | 51 | logging.info('Launching followfiles on target: %s, %s, %s', |
showard | f1175bb | 2009-06-17 19:34:36 +0000 | [diff] [blame] | 52 | host.hostname, lastlines_dirpath, str(follow_paths)) |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 53 | |
| 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: |
mbligh | d0e9498 | 2009-07-11 00:15:18 +0000 | [diff] [blame] | 59 | logging.info('No versioned Python binary found, ' |
| 60 | 'defaulting to: %s', DEFAULT_PYTHON) |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 61 | supported_python = DEFAULT_PYTHON |
| 62 | else: |
| 63 | raise FollowFilesLaunchError('No supported Python on host.') |
| 64 | |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 65 | remote_monitordir = copy_monitordir(host) |
Eric Li | 7edb304 | 2011-01-06 17:57:17 -0800 | [diff] [blame^] | 66 | remote_script_path = os.path.join(remote_monitordir, 'followfiles.py') |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 67 | |
| 68 | followfiles_cmd = '%s %s --lastlines_dirpath=%s %s' % ( |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 69 | supported_python, remote_script_path, |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 70 | lastlines_dirpath, ' '.join(follow_paths)) |
| 71 | |
Eric Li | 7edb304 | 2011-01-06 17:57:17 -0800 | [diff] [blame^] | 72 | 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 | |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 77 | # Give it enough time to crash if it's going to (it shouldn't). |
| 78 | time.sleep(5) |
Eric Li | 7edb304 | 2011-01-06 17:57:17 -0800 | [diff] [blame^] | 79 | doa = remote_ff_proc.poll() |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 80 | if doa: |
mbligh | d0e9498 | 2009-07-11 00:15:18 +0000 | [diff] [blame] | 81 | raise FollowFilesLaunchError('ssh command crashed.') |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 82 | |
Eric Li | 7edb304 | 2011-01-06 17:57:17 -0800 | [diff] [blame^] | 83 | return remote_ff_proc |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 84 | |
| 85 | |
| 86 | def 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 | |
| 108 | def 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 | |
| 136 | def _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 | |
| 152 | class 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' |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 175 | |
| 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. |
mbligh | d0e9498 | 2009-07-11 00:15:18 +0000 | [diff] [blame] | 214 | logging.warn('Target %s is missing expected remote paths: %s', |
showard | f1175bb | 2009-06-17 19:34:36 +0000 | [diff] [blame] | 215 | self.hostname, ', '.join(missing)) |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 216 | |
| 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: |
mbligh | 130a1dc | 2009-09-10 00:23:58 +0000 | [diff] [blame] | 223 | self._lastlines_dirpath = self.get_tmp_dir(parent='/var/tmp') |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 224 | |
| 225 | # Launch followfiles on target |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 226 | 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. |
showard | f1175bb | 2009-06-17 19:34:36 +0000 | [diff] [blame] | 231 | logging.fatal('Failed to launch followfiles on target,' |
| 232 | ' aborting logfile monitoring: %s', self.hostname) |
mbligh | b768126 | 2008-11-20 20:11:47 +0000 | [diff] [blame] | 233 | 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 |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 239 | |
| 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: |
showard | f1175bb | 2009-06-17 19:34:36 +0000 | [diff] [blame] | 246 | logging.warn('Specified patterns_path is invalid: %s, %s', |
| 247 | patterns_path, str(e)) |
mbligh | e48bcfb | 2008-11-11 17:09:44 +0000 | [diff] [blame] | 248 | 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 | |
| 277 | def 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 ()}) |