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