1 import os, shutil, re, glob, subprocess, logging 2 3 from distutils import dir_util 4 5 from autotest_lib.client.common_lib import log 6 from autotest_lib.client.cros import constants 7 from autotest_lib.client.bin import utils, package 8 9 _DEFAULT_COMMANDS_TO_LOG_PER_TEST = [] 10 _DEFAULT_COMMANDS_TO_LOG_PER_BOOT = [ 11 'lspci -vvn', 12 'gcc --version', 13 'ld --version', 14 'mount', 15 'hostname', 16 'uptime', 17 ] 18 _DEFAULT_COMMANDS_TO_LOG_BEFORE_ITERATION = [] 19 _DEFAULT_COMMANDS_TO_LOG_AFTER_ITERATION = [] 20 21 _DEFAULT_FILES_TO_LOG_PER_TEST = [] 22 _DEFAULT_FILES_TO_LOG_PER_BOOT = [ 23 '/proc/pci', 24 '/proc/meminfo', 25 '/proc/slabinfo', 26 '/proc/version', 27 '/proc/cpuinfo', 28 '/proc/modules', 29 '/proc/interrupts', 30 '/proc/partitions', 31 constants.LOG_CONSOLE_RAMOOPS, 32 '/var/log/', 33 ] 34 _DEFAULT_FILES_TO_LOG_BEFORE_ITERATION = [ 35 '/proc/schedstat', '/proc/meminfo', '/proc/slabinfo', '/proc/interrupts' 36 ] 37 _DEFAULT_FILES_TO_LOG_AFTER_ITERATION = [ 38 '/proc/schedstat', '/proc/meminfo', '/proc/slabinfo', '/proc/interrupts' 39 ] 40 41 42 class loggable(object): 43 """ Abstract class for representing all things "loggable" by sysinfo. """ 44 45 def __init__(self, logf, log_in_keyval): 46 self.logf = logf 47 self.log_in_keyval = log_in_keyval 48 49 def readline(self, logdir): 50 """Reads one line from the log. 51 52 @param logdir: The log directory. 53 @return A line from the log, or the empty string if the log doesn't 54 exist. 55 """ 56 path = os.path.join(logdir, self.logf) 57 if os.path.exists(path): 58 return utils.read_one_line(path) 59 else: 60 return "" 61 62 63 class logfile(loggable): 64 """Represents a log file.""" 65 def __init__(self, path, logf=None, log_in_keyval=False): 66 if not logf: 67 logf = os.path.basename(path) 68 super(logfile, self).__init__(logf, log_in_keyval) 69 self.path = path 70 71 72 def __repr__(self): 73 r = "sysinfo.logfile(%r, %r, %r)" 74 r %= (self.path, self.logf, self.log_in_keyval) 75 return r 76 77 78 def __eq__(self, other): 79 if isinstance(other, logfile): 80 return (self.path, self.logf) == (other.path, other.logf) 81 elif isinstance(other, loggable): 82 return False 83 return NotImplemented 84 85 86 def __ne__(self, other): 87 result = self.__eq__(other) 88 if result is NotImplemented: 89 return result 90 return not result 91 92 93 def __hash__(self): 94 return hash((self.path, self.logf)) 95 96 97 def run(self, logdir): 98 """Copies the log file or directory to the specified directory. 99 100 @param logdir: The log directory. 101 """ 102 if os.path.exists(self.path): 103 if os.path.isdir(self.path): 104 dst = os.path.join(logdir, self.logf, self.path.lstrip('/')) 105 dir_util.copy_tree(self.path, dst) 106 else: 107 dst = os.path.join(logdir, self.logf) 108 shutil.copyfile(self.path, dst) 109 110 111 class command(loggable): 112 """Represents a command.""" 113 def __init__(self, cmd, logf=None, log_in_keyval=False, compress_log=False): 114 if not logf: 115 logf = cmd.replace(" ", "_") 116 super(command, self).__init__(logf, log_in_keyval) 117 self.cmd = cmd 118 self._compress_log = compress_log 119 120 121 def __repr__(self): 122 r = "sysinfo.command(%r, %r, %r)" 123 r %= (self.cmd, self.logf, self.log_in_keyval) 124 return r 125 126 127 def __eq__(self, other): 128 if isinstance(other, command): 129 return (self.cmd, self.logf) == (other.cmd, other.logf) 130 elif isinstance(other, loggable): 131 return False 132 return NotImplemented 133 134 135 def __ne__(self, other): 136 result = self.__eq__(other) 137 if result is NotImplemented: 138 return result 139 return not result 140 141 142 def __hash__(self): 143 return hash((self.cmd, self.logf)) 144 145 146 def run(self, logdir): 147 """Runs the command. 148 149 @param logdir: The log directory. 150 """ 151 env = os.environ.copy() 152 if "PATH" not in env: 153 env["PATH"] = "/usr/bin:/bin" 154 logf_path = os.path.join(logdir, self.logf) 155 stdin = open(os.devnull, "r") 156 stderr = open(os.devnull, "w") 157 stdout = open(logf_path, "w") 158 try: 159 subprocess.call(self.cmd, 160 stdin=stdin, 161 stdout=stdout, 162 stderr=stderr, 163 shell=True, 164 env=env) 165 finally: 166 for f in (stdin, stdout, stderr): 167 f.close() 168 if self._compress_log and os.path.exists(logf_path): 169 utils.system('gzip -9 "%s"' % logf_path, ignore_status=True) 170 171 172 class base_sysinfo(object): 173 """Represents system info.""" 174 def __init__(self, job_resultsdir): 175 self.sysinfodir = self._get_sysinfodir(job_resultsdir) 176 177 # pull in the post-test logs to collect 178 self.test_loggables = set() 179 for cmd in _DEFAULT_COMMANDS_TO_LOG_PER_TEST: 180 self.test_loggables.add(command(cmd)) 181 for filename in _DEFAULT_FILES_TO_LOG_PER_TEST: 182 self.test_loggables.add(logfile(filename)) 183 184 # pull in the EXTRA post-boot logs to collect 185 self.boot_loggables = set() 186 for cmd in _DEFAULT_COMMANDS_TO_LOG_PER_BOOT: 187 self.boot_loggables.add(command(cmd)) 188 for filename in _DEFAULT_FILES_TO_LOG_PER_BOOT: 189 self.boot_loggables.add(logfile(filename)) 190 191 # pull in the pre test iteration logs to collect 192 self.before_iteration_loggables = set() 193 for cmd in _DEFAULT_COMMANDS_TO_LOG_BEFORE_ITERATION: 194 self.before_iteration_loggables.add( 195 command(cmd, logf=cmd.replace(" ", "_") + '.before')) 196 for fname in _DEFAULT_FILES_TO_LOG_BEFORE_ITERATION: 197 self.before_iteration_loggables.add( 198 logfile(fname, logf=os.path.basename(fname) + '.before')) 199 200 # pull in the post test iteration logs to collect 201 self.after_iteration_loggables = set() 202 for cmd in _DEFAULT_COMMANDS_TO_LOG_AFTER_ITERATION: 203 self.after_iteration_loggables.add( 204 command(cmd, logf=cmd.replace(" ", "_") + '.after')) 205 for fname in _DEFAULT_FILES_TO_LOG_AFTER_ITERATION: 206 self.after_iteration_loggables.add( 207 logfile(fname, logf=os.path.basename(fname) + '.after')) 208 209 # add in a couple of extra files and commands we want to grab 210 self.test_loggables.add(command("df -mP", logf="df")) 211 # We compress the dmesg because it can get large when kernels are 212 # configured with a large buffer and some tests trigger OOMs or 213 # other large "spam" that fill it up... 214 self.test_loggables.add(command('dmesg -c', 215 logf='dmesg', 216 compress_log=True)) 217 self.test_loggables.add(command('journalctl -o export', 218 logf='journal', 219 compress_log=True)) 220 self.boot_loggables.add(logfile('/proc/cmdline', log_in_keyval=True)) 221 # log /proc/mounts but with custom filename since we already 222 # log the output of the "mount" command as the filename "mount" 223 self.boot_loggables.add(logfile('/proc/mounts', logf='proc_mounts')) 224 self.boot_loggables.add(command('uname -a', 225 logf='uname', 226 log_in_keyval=True)) 227 self._installed_packages = [] 228 229 230 def serialize(self): 231 """Returns parameters in a dictionary.""" 232 return {"boot": self.boot_loggables, "test": self.test_loggables} 233 234 235 def deserialize(self, serialized): 236 """Stores parameters from a dictionary into instance variables. 237 238 @param serialized: A dictionary containing parameters to store as 239 instance variables. 240 """ 241 self.boot_loggables = serialized["boot"] 242 self.test_loggables = serialized["test"] 243 244 245 @staticmethod 246 def _get_sysinfodir(resultsdir): 247 sysinfodir = os.path.join(resultsdir, "sysinfo") 248 if not os.path.exists(sysinfodir): 249 os.makedirs(sysinfodir) 250 return sysinfodir 251 252 253 def _get_reboot_count(self): 254 if not glob.glob(os.path.join(self.sysinfodir, "*")): 255 return -1 256 else: 257 return len(glob.glob(os.path.join(self.sysinfodir, "boot.*"))) 258 259 260 def _get_boot_subdir(self, next=False): 261 reboot_count = self._get_reboot_count() 262 if next: 263 reboot_count += 1 264 if reboot_count < 1: 265 return self.sysinfodir 266 else: 267 boot_dir = "boot.%d" % (reboot_count - 1) 268 return os.path.join(self.sysinfodir, boot_dir) 269 270 271 def _get_iteration_subdir(self, test, iteration): 272 iter_dir = "iteration.%d" % iteration 273 274 logdir = os.path.join(self._get_sysinfodir(test.outputdir), iter_dir) 275 if not os.path.exists(logdir): 276 os.mkdir(logdir) 277 return logdir 278 279 280 @log.log_and_ignore_errors("post-reboot sysinfo error:") 281 def log_per_reboot_data(self): 282 """Logging hook called when a job starts, and again after any reboot.""" 283 logdir = self._get_boot_subdir(next=True) 284 if not os.path.exists(logdir): 285 os.mkdir(logdir) 286 287 for log in (self.test_loggables | self.boot_loggables): 288 log.run(logdir) 289 290 # also log any installed packages 291 installed_path = os.path.join(logdir, "installed_packages") 292 installed_packages = "\n".join(package.list_all()) + "\n" 293 utils.open_write_close(installed_path, installed_packages) 294 295 296 @log.log_and_ignore_errors("pre-test sysinfo error:") 297 def log_before_each_test(self, test): 298 """Logging hook called before a test starts. 299 300 @param test: A test object. 301 """ 302 self._installed_packages = package.list_all() 303 if os.path.exists("/var/log/messages"): 304 stat = os.stat("/var/log/messages") 305 self._messages_size = stat.st_size 306 self._messages_inode = stat.st_ino 307 308 if os.path.exists("/var/log/journal"): 309 # Find the current journal cursor so we later can save new messages. 310 cmd = "/usr/bin/journalctl -n0 --show-cursor -q" 311 try: 312 cursor = utils.system_output(cmd) 313 prefix = "-- cursor: " 314 pos = cursor.find(prefix) + len(prefix) 315 self._journal_cursor = cursor[pos:] 316 except Exception, e: 317 logging.error("error running journalctl --show-cursor: %s", e) 318 319 320 @log.log_and_ignore_errors("post-test sysinfo error:") 321 def log_after_each_test(self, test): 322 """Logging hook called after a test finishs. 323 324 @param test: A test object. 325 """ 326 test_sysinfodir = self._get_sysinfodir(test.outputdir) 327 328 # create a symlink in the test sysinfo dir to the current boot 329 reboot_dir = self._get_boot_subdir() 330 assert os.path.exists(reboot_dir) 331 symlink_dest = os.path.join(test_sysinfodir, "reboot_current") 332 symlink_src = utils.get_relative_path(reboot_dir, 333 os.path.dirname(symlink_dest)) 334 try: 335 os.symlink(symlink_src, symlink_dest) 336 except Exception, e: 337 raise Exception, '%s: whilst linking %s to %s' % (e, symlink_src, 338 symlink_dest) 339 340 # run all the standard logging commands 341 for log in self.test_loggables: 342 log.run(test_sysinfodir) 343 344 # grab any new data from /var/log/messages 345 self._log_messages(test_sysinfodir) 346 347 # grab any new data from systemd journal 348 self._log_journal(test_sysinfodir) 349 350 # log some sysinfo data into the test keyval file 351 keyval = self.log_test_keyvals(test_sysinfodir) 352 test.write_test_keyval(keyval) 353 354 # log any changes to installed packages 355 old_packages = set(self._installed_packages) 356 new_packages = set(package.list_all()) 357 added_path = os.path.join(test_sysinfodir, "added_packages") 358 added_packages = "\n".join(new_packages - old_packages) + "\n" 359 utils.open_write_close(added_path, added_packages) 360 removed_path = os.path.join(test_sysinfodir, "removed_packages") 361 removed_packages = "\n".join(old_packages - new_packages) + "\n" 362 utils.open_write_close(removed_path, removed_packages) 363 364 365 @log.log_and_ignore_errors("pre-test siteration sysinfo error:") 366 def log_before_each_iteration(self, test, iteration=None): 367 """Logging hook called before a test iteration. 368 369 @param test: A test object. 370 @param iteration: A test iteration. 371 """ 372 if not iteration: 373 iteration = test.iteration 374 logdir = self._get_iteration_subdir(test, iteration) 375 376 for log in self.before_iteration_loggables: 377 log.run(logdir) 378 # Start each log with the board name for orientation. 379 board = utils.get_board_with_frequency_and_memory() 380 logging.info('ChromeOS BOARD = %s', board) 381 # Leave some autotest bread crumbs in the system logs. 382 utils.system('logger "autotest starting iteration %s on %s"' % (logdir, 383 board), 384 ignore_status=True) 385 386 387 @log.log_and_ignore_errors("post-test siteration sysinfo error:") 388 def log_after_each_iteration(self, test, iteration=None): 389 """Logging hook called after a test iteration. 390 391 @param test: A test object. 392 @param iteration: A test iteration. 393 """ 394 if not iteration: 395 iteration = test.iteration 396 logdir = self._get_iteration_subdir(test, iteration) 397 398 for log in self.after_iteration_loggables: 399 log.run(logdir) 400 utils.system('logger "autotest finished iteration %s"' % logdir, 401 ignore_status=True) 402 403 404 def _log_messages(self, logdir): 405 """Log all of the new data in /var/log/messages.""" 406 try: 407 # log all of the new data in /var/log/messages 408 bytes_to_skip = 0 409 if hasattr(self, "_messages_size"): 410 current_inode = os.stat("/var/log/messages").st_ino 411 if current_inode == self._messages_inode: 412 bytes_to_skip = self._messages_size 413 in_messages = open("/var/log/messages") 414 in_messages.seek(bytes_to_skip) 415 out_messages = open(os.path.join(logdir, "messages"), "w") 416 out_messages.write(in_messages.read()) 417 in_messages.close() 418 out_messages.close() 419 except Exception, e: 420 logging.error("/var/log/messages collection failed with %s", e) 421 422 423 def _log_journal(self, logdir): 424 """Log all of the new data in systemd journal.""" 425 if not hasattr(self, "_journal_cursor"): 426 return 427 428 cmd = "/usr/bin/journalctl --after-cursor \"%s\"" % ( 429 self._journal_cursor) 430 try: 431 with open(os.path.join(logdir, "journal"), "w") as journal: 432 journal.write(utils.system_output(cmd)) 433 except Exception, e: 434 logging.error("journal collection failed with %s", e) 435 436 437 @staticmethod 438 def _read_sysinfo_keyvals(loggables, logdir): 439 keyval = {} 440 for log in loggables: 441 if log.log_in_keyval: 442 keyval["sysinfo-" + log.logf] = log.readline(logdir) 443 return keyval 444 445 446 def log_test_keyvals(self, test_sysinfodir): 447 """Logging hook called by log_after_each_test. 448 449 Collects keyval entries to be written in the test keyval. 450 451 @param test_sysinfodir: The test's system info directory. 452 """ 453 keyval = {} 454 455 # grab any loggables that should be in the keyval 456 keyval.update(self._read_sysinfo_keyvals(self.test_loggables, 457 test_sysinfodir)) 458 keyval.update(self._read_sysinfo_keyvals( 459 self.boot_loggables, os.path.join(test_sysinfodir, 460 'reboot_current'))) 461 462 # remove hostname from uname info 463 # Linux lpt36 2.6.18-smp-230.1 #1 [4069269] SMP Fri Oct 24 11:30:... 464 if "sysinfo-uname" in keyval: 465 kernel_vers = " ".join(keyval["sysinfo-uname"].split()[2:]) 466 keyval["sysinfo-uname"] = kernel_vers 467 468 # grab the total avail memory, not used by sys tables 469 path = os.path.join(test_sysinfodir, "reboot_current", "meminfo") 470 if os.path.exists(path): 471 mem_data = open(path).read() 472 match = re.search(r'^MemTotal:\s+(\d+) kB$', mem_data, re.MULTILINE) 473 if match: 474 keyval["sysinfo-memtotal-in-kb"] = match.group(1) 475 476 # guess the system's total physical memory, including sys tables 477 keyval["sysinfo-phys-mbytes"] = utils.rounded_memtotal()//1024 478 479 # return what we collected 480 return keyval 481