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