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