1# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import logging 6import os 7import stat 8 9from autotest_lib.client.common_lib import log 10from autotest_lib.client.common_lib import error, utils, global_config 11from autotest_lib.client.bin import base_sysinfo, utils 12from autotest_lib.client.cros import constants 13 14get_value = global_config.global_config.get_config_value 15collect_corefiles = get_value('CLIENT', 'collect_corefiles', 16 type=bool, default=False) 17 18 19logfile = base_sysinfo.logfile 20command = base_sysinfo.command 21 22 23class logdir(base_sysinfo.loggable): 24 """Represents a log directory.""" 25 26 DEFAULT_EXCLUDES = ("**autoserv*", "**.journal",) 27 28 def __init__(self, directory, excludes=DEFAULT_EXCLUDES): 29 super(logdir, self).__init__(directory, log_in_keyval=False) 30 self.dir = directory 31 self._excludes = excludes 32 self._infer_old_attributes() 33 34 35 def __setstate__(self, state): 36 """Unpickle handler 37 38 When client tests are run without SSP, we pickle this object on the 39 server-side (using the version of the class deployed in the lab) and 40 unpickle it on the DUT (using the version of the class from the build). 41 This means that when adding a new attribute to this class, for a while 42 the server-side code does not populate that attribute. So, deal with 43 missing attributes in a sane way. 44 """ 45 self.__dict__ = state 46 if '_excludes' not in state: 47 self._excludes = self.DEFAULT_EXCLUDES 48 if self.additional_exclude: 49 self._excludes += tuple(self.additional_exclude) 50 51 52 def __repr__(self): 53 return "site_sysinfo.logdir(%r, %s)" % (self.dir, 54 self._excludes) 55 56 57 def __eq__(self, other): 58 if isinstance(other, logdir): 59 return (self.dir == other.dir and self._excludes == other._excludes) 60 elif isinstance(other, base_sysinfo.loggable): 61 return False 62 return NotImplemented 63 64 65 def __ne__(self, other): 66 result = self.__eq__(other) 67 if result is NotImplemented: 68 return result 69 return not result 70 71 72 def __hash__(self): 73 return hash(self.dir) + hash(self._excludes) 74 75 76 def run(self, log_dir): 77 """Copies this log directory to the specified directory. 78 79 @param log_dir: The destination log directory. 80 """ 81 from_dir = os.path.realpath(self.dir) 82 if os.path.exists(from_dir): 83 parent_dir = os.path.dirname(from_dir) 84 utils.system("mkdir -p %s%s" % (log_dir, parent_dir)) 85 86 excludes = [ 87 "--exclude=%s" % self._anchored_exclude_pattern(from_dir, x) 88 for x in self._excludes] 89 # Take source permissions and add ugo+r so files are accessible via 90 # archive server. 91 utils.system( 92 "rsync --no-perms --chmod=ugo+r -a --safe-links %s %s %s%s" 93 % (" ".join(excludes), from_dir, log_dir, parent_dir)) 94 95 96 def _anchored_exclude_pattern(self, from_dir, pattern): 97 return '/%s/%s' % (os.path.basename(from_dir), pattern) 98 99 100 def _infer_old_attributes(self): 101 """ Backwards compatibility attributes. 102 103 YOU MUST NEVER DROP / REINTERPRET THESE. 104 A logdir object is pickled on server-side and unpickled on 105 client-side. This means that, when running aginst client-side code 106 from an older build, we need to be able to unpickle an instance of 107 logdir pickled from a newer version of the class. 108 109 Some old attributes are not sanely handled via __setstate__, so we can't 110 drop them without breaking compatibility. 111 """ 112 additional_excludes = list(set(self._excludes) - 113 set(self.DEFAULT_EXCLUDES)) 114 if additional_excludes: 115 # Old API only allowed a single additional exclude. 116 # Best effort, keep the first one, throw the rest. 117 self.additional_exclude = additional_excludes[0] 118 else: 119 self.additional_exclude = None 120 121 122class file_stat(object): 123 """Store the file size and inode, used for retrieving new data in file.""" 124 def __init__(self, file_path): 125 """Collect the size and inode information of a file. 126 127 @param file_path: full path to the file. 128 129 """ 130 stat = os.stat(file_path) 131 # Start size of the file, skip that amount of bytes when do diff. 132 self.st_size = stat.st_size 133 # inode of the file. If inode is changed, treat this as a new file and 134 # copy the whole file. 135 self.st_ino = stat.st_ino 136 137 138class diffable_logdir(logdir): 139 """Represents a log directory that only new content will be copied. 140 141 An instance of this class should be added in both 142 before_iteration_loggables and after_iteration_loggables. This is to 143 guarantee the file status information is collected when run method is 144 called in before_iteration_loggables, and diff is executed when run 145 method is called in after_iteration_loggables. 146 147 """ 148 def __init__(self, directory, excludes=logdir.DEFAULT_EXCLUDES, 149 keep_file_hierarchy=True, append_diff_in_name=True): 150 """ 151 Constructor of a diffable_logdir instance. 152 153 @param directory: directory to be diffed after an iteration finished. 154 @param excludes: path patterns to exclude for rsync. 155 @param keep_file_hierarchy: True if need to preserve full path, e.g., 156 sysinfo/var/log/sysstat, v.s. sysinfo/sysstat if it's False. 157 @param append_diff_in_name: True if you want to append '_diff' to the 158 folder name to indicate it's a diff, e.g., var/log_diff. Option 159 keep_file_hierarchy must be True for this to take effect. 160 161 """ 162 super(diffable_logdir, self).__init__(directory, excludes) 163 self.keep_file_hierarchy = keep_file_hierarchy 164 self.append_diff_in_name = append_diff_in_name 165 # Init dictionary to store all file status for files in the directory. 166 self._log_stats = {} 167 168 169 def _get_init_status_of_src_dir(self, src_dir): 170 """Get initial status of files in src_dir folder. 171 172 @param src_dir: directory to be diff-ed. 173 174 """ 175 # Dictionary used to store the initial status of files in src_dir. 176 for file_path in self._get_all_files(src_dir): 177 self._log_stats[file_path] = file_stat(file_path) 178 self.file_stats_collected = True 179 180 181 def _get_all_files(self, path): 182 """Iterate through files in given path including subdirectories. 183 184 @param path: root directory. 185 @return: an iterator that iterates through all files in given path 186 including subdirectories. 187 188 """ 189 if not os.path.exists(path): 190 yield [] 191 for root, dirs, files in os.walk(path): 192 for f in files: 193 if f.startswith('autoserv'): 194 continue 195 if f.endswith('.journal'): 196 continue 197 full_path = os.path.join(root, f) 198 # Only list regular files or symlinks to those (os.stat follows 199 # symlinks) 200 if stat.S_ISREG(os.stat(full_path).st_mode): 201 yield full_path 202 203 204 def _copy_new_data_in_file(self, file_path, src_dir, dest_dir): 205 """Copy all new data in a file to target directory. 206 207 @param file_path: full path to the file to be copied. 208 @param src_dir: source directory to do the diff. 209 @param dest_dir: target directory to store new data of src_dir. 210 211 """ 212 bytes_to_skip = 0 213 if self._log_stats.has_key(file_path): 214 prev_stat = self._log_stats[file_path] 215 new_stat = os.stat(file_path) 216 if new_stat.st_ino == prev_stat.st_ino: 217 bytes_to_skip = prev_stat.st_size 218 if new_stat.st_size == bytes_to_skip: 219 return 220 elif new_stat.st_size < prev_stat.st_size: 221 # File is modified to a smaller size, copy whole file. 222 bytes_to_skip = 0 223 try: 224 with open(file_path, 'r') as in_log: 225 if bytes_to_skip > 0: 226 in_log.seek(bytes_to_skip) 227 # Skip src_dir in path, e.g., src_dir/[sub_dir]/file_name. 228 target_path = os.path.join(dest_dir, 229 os.path.relpath(file_path, src_dir)) 230 target_dir = os.path.dirname(target_path) 231 if not os.path.exists(target_dir): 232 os.makedirs(target_dir) 233 with open(target_path, "w") as out_log: 234 out_log.write(in_log.read()) 235 except IOError as e: 236 logging.error('Diff %s failed with error: %s', file_path, e) 237 238 239 def _log_diff(self, src_dir, dest_dir): 240 """Log all of the new data in src_dir to dest_dir. 241 242 @param src_dir: source directory to do the diff. 243 @param dest_dir: target directory to store new data of src_dir. 244 245 """ 246 if self.keep_file_hierarchy: 247 dir = src_dir.lstrip('/') 248 if self.append_diff_in_name: 249 dir = dir.rstrip('/') + '_diff' 250 dest_dir = os.path.join(dest_dir, dir) 251 252 if not os.path.exists(dest_dir): 253 os.makedirs(dest_dir) 254 255 for src_file in self._get_all_files(src_dir): 256 self._copy_new_data_in_file(src_file, src_dir, dest_dir) 257 258 259 def run(self, log_dir, collect_init_status=True, collect_all=False): 260 """Copies new content from self.dir to the destination log_dir. 261 262 @param log_dir: The destination log directory. 263 @param collect_init_status: Set to True if run method is called to 264 collect the initial status of files. 265 @param collect_all: Set to True to force to collect all files. 266 267 """ 268 if collect_init_status: 269 self._get_init_status_of_src_dir(self.dir) 270 elif os.path.exists(self.dir): 271 # Always create a copy of the new logs to help debugging. 272 self._log_diff(self.dir, log_dir) 273 if collect_all: 274 logdir_temp = logdir(self.dir) 275 logdir_temp.run(log_dir) 276 277 278class purgeable_logdir(logdir): 279 """Represents a log directory that will be purged.""" 280 def __init__(self, directory, excludes=logdir.DEFAULT_EXCLUDES): 281 super(purgeable_logdir, self).__init__(directory, excludes) 282 283 def run(self, log_dir): 284 """Copies this log dir to the destination dir, then purges the source. 285 286 @param log_dir: The destination log directory. 287 """ 288 super(purgeable_logdir, self).run(log_dir) 289 290 if os.path.exists(self.dir): 291 utils.system("rm -rf %s/*" % (self.dir)) 292 293 294class site_sysinfo(base_sysinfo.base_sysinfo): 295 """Represents site system info.""" 296 def __init__(self, job_resultsdir): 297 super(site_sysinfo, self).__init__(job_resultsdir) 298 crash_exclude_string = None 299 if not collect_corefiles: 300 crash_exclude_string = "*.core" 301 302 # This is added in before and after_iteration_loggables. When run is 303 # called in before_iteration_loggables, it collects file status in 304 # the directory. When run is called in after_iteration_loggables, diff 305 # is executed. 306 # self.diffable_loggables is only initialized if the instance does not 307 # have this attribute yet. The sysinfo instance could be loaded 308 # from an earlier pickle dump, which has already initialized attribute 309 # self.diffable_loggables. 310 if not hasattr(self, 'diffable_loggables'): 311 diffable_log = diffable_logdir(constants.LOG_DIR) 312 self.diffable_loggables = set() 313 self.diffable_loggables.add(diffable_log) 314 315 # add in some extra command logging 316 self.boot_loggables.add(command("ls -l /boot", 317 "boot_file_list")) 318 self.before_iteration_loggables.add( 319 command(constants.CHROME_VERSION_COMMAND, "chrome_version")) 320 self.boot_loggables.add(command("crossystem", "crossystem")) 321 self.test_loggables.add( 322 purgeable_logdir( 323 os.path.join(constants.CRYPTOHOME_MOUNT_PT, "log"))) 324 # We only want to gather and purge crash reports after the client test 325 # runs in case a client test is checking that a crash found at boot 326 # (such as a kernel crash) is handled. 327 self.after_iteration_loggables.add( 328 purgeable_logdir( 329 os.path.join(constants.CRYPTOHOME_MOUNT_PT, "crash"), 330 excludes=logdir.DEFAULT_EXCLUDES + (crash_exclude_string,))) 331 self.after_iteration_loggables.add( 332 purgeable_logdir( 333 constants.CRASH_DIR, 334 excludes=logdir.DEFAULT_EXCLUDES + (crash_exclude_string,))) 335 self.test_loggables.add( 336 logfile(os.path.join(constants.USER_DATA_DIR, 337 ".Google/Google Talk Plugin/gtbplugin.log"))) 338 self.test_loggables.add(purgeable_logdir( 339 constants.CRASH_DIR, 340 excludes=logdir.DEFAULT_EXCLUDES + (crash_exclude_string,))) 341 # Collect files under /tmp/crash_reporter, which contain the procfs 342 # copy of those crashed processes whose core file didn't get converted 343 # into minidump. We need these additional files for post-mortem analysis 344 # of the conversion failure. 345 self.test_loggables.add( 346 purgeable_logdir(constants.CRASH_REPORTER_RESIDUE_DIR)) 347 348 349 @log.log_and_ignore_errors("pre-test sysinfo error:") 350 def log_before_each_test(self, test): 351 """Logging hook called before a test starts. 352 353 @param test: A test object. 354 """ 355 super(site_sysinfo, self).log_before_each_test(test) 356 357 for log in self.diffable_loggables: 358 log.run(log_dir=None, collect_init_status=True) 359 360 361 @log.log_and_ignore_errors("post-test sysinfo error:") 362 def log_after_each_test(self, test): 363 """Logging hook called after a test finishs. 364 365 @param test: A test object. 366 """ 367 super(site_sysinfo, self).log_after_each_test(test) 368 369 test_sysinfodir = self._get_sysinfodir(test.outputdir) 370 371 for log in self.diffable_loggables: 372 log.run(log_dir=test_sysinfodir, collect_init_status=False, 373 collect_all=not test.success) 374 375 376 def _get_chrome_version(self): 377 """Gets the Chrome version number and milestone as strings. 378 379 Invokes "chrome --version" to get the version number and milestone. 380 381 @return A tuple (chrome_ver, milestone) where "chrome_ver" is the 382 current Chrome version number as a string (in the form "W.X.Y.Z") 383 and "milestone" is the first component of the version number 384 (the "W" from "W.X.Y.Z"). If the version number cannot be parsed 385 in the "W.X.Y.Z" format, the "chrome_ver" will be the full output 386 of "chrome --version" and the milestone will be the empty string. 387 388 """ 389 version_string = utils.system_output(constants.CHROME_VERSION_COMMAND, 390 ignore_status=True) 391 return utils.parse_chrome_version(version_string) 392 393 394 def log_test_keyvals(self, test_sysinfodir): 395 """Generate keyval for the sysinfo. 396 397 Collects keyval entries to be written in the test keyval. 398 399 @param test_sysinfodir: The test's system info directory. 400 """ 401 keyval = super(site_sysinfo, self).log_test_keyvals(test_sysinfodir) 402 403 lsb_lines = utils.system_output( 404 "cat /etc/lsb-release", 405 ignore_status=True).splitlines() 406 lsb_dict = dict(item.split("=") for item in lsb_lines) 407 408 for lsb_key in lsb_dict.keys(): 409 # Special handling for build number 410 if lsb_key == "CHROMEOS_RELEASE_DESCRIPTION": 411 keyval["CHROMEOS_BUILD"] = ( 412 lsb_dict[lsb_key].rstrip(")").split(" ")[3]) 413 keyval[lsb_key] = lsb_dict[lsb_key] 414 415 # Get the hwid (hardware ID), if applicable. 416 try: 417 keyval["hwid"] = utils.system_output('crossystem hwid') 418 except error.CmdError: 419 # The hwid may not be available (e.g, when running on a VM). 420 # If the output of 'crossystem mainfw_type' is 'nonchrome', then 421 # we expect the hwid to not be avilable, and we can proceed in this 422 # case. Otherwise, the hwid is missing unexpectedly. 423 mainfw_type = utils.system_output('crossystem mainfw_type') 424 if mainfw_type == 'nonchrome': 425 logging.info( 426 'HWID not available; not logging it as a test keyval.') 427 else: 428 logging.exception('HWID expected but could not be identified; ' 429 'output of "crossystem mainfw_type" is "%s"', 430 mainfw_type) 431 raise 432 433 # Get the chrome version and milestone numbers. 434 keyval["CHROME_VERSION"], keyval["MILESTONE"] = ( 435 self._get_chrome_version()) 436 437 # TODO(kinaba): crbug.com/707448 Import at the head of this file. 438 # Currently a server-side script server/server_job.py is indirectly 439 # importing this file, so we cannot globaly import cryptohome that 440 # has dependency to a client-only library. 441 from autotest_lib.client.cros import cryptohome 442 # Get the dictionary attack counter. 443 keyval["TPM_DICTIONARY_ATTACK_COUNTER"] = ( 444 cryptohome.get_tpm_more_status().get( 445 'dictionary_attack_counter', 'Failed to query cryptohome')) 446 447 # Return the updated keyvals. 448 return keyval 449 450 451 def add_logdir(self, loggable): 452 """Collect files in log_path to sysinfo folder. 453 454 This method can be called from a control file for test to collect files 455 in a specified folder. autotest creates a folder [test result 456 dir]/sysinfo folder with the full path of log_path and copy all files in 457 log_path to that folder. 458 459 @param loggable: A logdir instance corresponding to the logs to collect. 460 """ 461 self.test_loggables.add(loggable) 462