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