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