base_sysinfo.py revision c64fe51989a605e53bb26cc03b0b62e3b2090ec7
1import os, shutil, re, glob, subprocess, logging
2
3from autotest_lib.client.common_lib import log
4from autotest_lib.client.bin import utils, package
5
6
7_DEFAULT_COMMANDS_TO_LOG_PER_TEST = []
8_DEFAULT_COMMANDS_TO_LOG_PER_BOOT = [
9    "lspci -vvn", "gcc --version", "ld --version", "mount", "hostname",
10    "uptime",
11    ]
12_DEFAULT_COMMANDS_TO_LOG_BEFORE_ITERATION = []
13_DEFAULT_COMMANDS_TO_LOG_AFTER_ITERATION = []
14
15_DEFAULT_FILES_TO_LOG_PER_TEST = []
16_DEFAULT_FILES_TO_LOG_PER_BOOT = [
17    "/proc/pci", "/proc/meminfo", "/proc/slabinfo", "/proc/version",
18    "/proc/cpuinfo", "/proc/modules", "/proc/interrupts", "/proc/partitions",
19    ]
20_DEFAULT_FILES_TO_LOG_BEFORE_ITERATION = [
21    "/proc/schedstat", "/proc/meminfo", "/proc/slabinfo", "/proc/interrupts"
22    ]
23_DEFAULT_FILES_TO_LOG_AFTER_ITERATION = [
24    "/proc/schedstat", "/proc/meminfo", "/proc/slabinfo", "/proc/interrupts"
25    ]
26
27
28class loggable(object):
29    """ Abstract class for representing all things "loggable" by sysinfo. """
30    def __init__(self, logf, log_in_keyval):
31        self.logf = logf
32        self.log_in_keyval = log_in_keyval
33
34
35    def readline(self, logdir):
36        path = os.path.join(logdir, self.logf)
37        if os.path.exists(path):
38            return utils.read_one_line(path)
39        else:
40            return ""
41
42
43class logfile(loggable):
44    def __init__(self, path, logf=None, log_in_keyval=False):
45        if not logf:
46            logf = os.path.basename(path)
47        super(logfile, self).__init__(logf, log_in_keyval)
48        self.path = path
49
50
51    def __repr__(self):
52        r = "sysinfo.logfile(%r, %r, %r)"
53        r %= (self.path, self.logf, self.log_in_keyval)
54        return r
55
56
57    def __eq__(self, other):
58        if isinstance(other, logfile):
59            return (self.path, self.logf) == (other.path, other.logf)
60        elif isinstance(other, 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.path, self.logf))
74
75
76    def run(self, logdir):
77        if os.path.exists(self.path):
78            shutil.copyfile(self.path, os.path.join(logdir, self.logf))
79
80
81class command(loggable):
82    def __init__(self, cmd, logf=None, log_in_keyval=False, compress_log=False):
83        if not logf:
84            logf = cmd.replace(" ", "_")
85        super(command, self).__init__(logf, log_in_keyval)
86        self.cmd = cmd
87        self._compress_log = compress_log
88
89
90    def __repr__(self):
91        r = "sysinfo.command(%r, %r, %r)"
92        r %= (self.cmd, self.logf, self.log_in_keyval)
93        return r
94
95
96    def __eq__(self, other):
97        if isinstance(other, command):
98            return (self.cmd, self.logf) == (other.cmd, other.logf)
99        elif isinstance(other, loggable):
100            return False
101        return NotImplemented
102
103
104    def __ne__(self, other):
105        result = self.__eq__(other)
106        if result is NotImplemented:
107            return result
108        return not result
109
110
111    def __hash__(self):
112        return hash((self.cmd, self.logf))
113
114
115    def run(self, logdir):
116        env = os.environ.copy()
117        if "PATH" not in env:
118            env["PATH"] = "/usr/bin:/bin"
119        logf_path = os.path.join(logdir, self.logf)
120        stdin = open(os.devnull, "r")
121        stderr = open(os.devnull, "w")
122        stdout = open(logf_path, "w")
123        try:
124            subprocess.call(self.cmd, stdin=stdin, stdout=stdout, stderr=stderr,
125                            shell=True, env=env)
126        finally:
127            for f in (stdin, stdout, stderr):
128                f.close()
129            if self._compress_log and os.path.exists(logf_path):
130                utils.system('gzip -9 "%s"' % logf_path, ignore_status=True)
131
132
133class base_sysinfo(object):
134    def __init__(self, job_resultsdir):
135        self.sysinfodir = self._get_sysinfodir(job_resultsdir)
136
137        # pull in the post-test logs to collect
138        self.test_loggables = set()
139        for cmd in _DEFAULT_COMMANDS_TO_LOG_PER_TEST:
140            self.test_loggables.add(command(cmd))
141        for filename in _DEFAULT_FILES_TO_LOG_PER_TEST:
142            self.test_loggables.add(logfile(filename))
143
144        # pull in the EXTRA post-boot logs to collect
145        self.boot_loggables = set()
146        for cmd in _DEFAULT_COMMANDS_TO_LOG_PER_BOOT:
147            self.boot_loggables.add(command(cmd))
148        for filename in _DEFAULT_FILES_TO_LOG_PER_BOOT:
149            self.boot_loggables.add(logfile(filename))
150
151        # pull in the pre test iteration logs to collect
152        self.before_iteration_loggables = set()
153        for cmd in _DEFAULT_COMMANDS_TO_LOG_BEFORE_ITERATION:
154            self.before_iteration_loggables.add(
155                command(cmd, logf=cmd.replace(" ", "_") + '.before'))
156        for fname in _DEFAULT_FILES_TO_LOG_BEFORE_ITERATION:
157            self.before_iteration_loggables.add(
158                logfile(fname, logf=os.path.basename(fname) + '.before'))
159
160        # pull in the post test iteration logs to collect
161        self.after_iteration_loggables = set()
162        for cmd in _DEFAULT_COMMANDS_TO_LOG_AFTER_ITERATION:
163            self.after_iteration_loggables.add(
164                command(cmd, logf=cmd.replace(" ", "_") + '.after'))
165        for fname in _DEFAULT_FILES_TO_LOG_AFTER_ITERATION:
166            self.after_iteration_loggables.add(
167                logfile(fname, logf=os.path.basename(fname) + '.after'))
168
169        # add in a couple of extra files and commands we want to grab
170        self.test_loggables.add(command("df -mP", logf="df"))
171        # We compress the dmesg because it can get large when kernels are
172        # configured with a large buffer and some tests trigger OOMs or
173        # other large "spam" that fill it up...
174        self.test_loggables.add(command("dmesg -c", logf="dmesg",
175                                        compress_log=True))
176        self.boot_loggables.add(logfile("/proc/cmdline",
177                                             log_in_keyval=True))
178        # log /proc/mounts but with custom filename since we already
179        # log the output of the "mount" command as the filename "mount"
180        self.boot_loggables.add(logfile('/proc/mounts', logf='proc_mounts'))
181        self.boot_loggables.add(command("uname -a", logf="uname",
182                                             log_in_keyval=True))
183        self._installed_packages = None
184
185
186    def serialize(self):
187        return {"boot": self.boot_loggables, "test": self.test_loggables}
188
189
190    def deserialize(self, serialized):
191        self.boot_loggables = serialized["boot"]
192        self.test_loggables = serialized["test"]
193
194
195    @staticmethod
196    def _get_sysinfodir(resultsdir):
197        sysinfodir = os.path.join(resultsdir, "sysinfo")
198        if not os.path.exists(sysinfodir):
199            os.makedirs(sysinfodir)
200        return sysinfodir
201
202
203    def _get_reboot_count(self):
204        if not glob.glob(os.path.join(self.sysinfodir, "*")):
205            return -1
206        else:
207            return len(glob.glob(os.path.join(self.sysinfodir, "boot.*")))
208
209
210    def _get_boot_subdir(self, next=False):
211        reboot_count = self._get_reboot_count()
212        if next:
213            reboot_count += 1
214        if reboot_count < 1:
215            return self.sysinfodir
216        else:
217            boot_dir = "boot.%d" % (reboot_count - 1)
218            return os.path.join(self.sysinfodir, boot_dir)
219
220
221    def _get_iteration_subdir(self, test, iteration):
222        iter_dir = "iteration.%d" % iteration
223
224        logdir = os.path.join(self._get_sysinfodir(test.outputdir), iter_dir)
225        if not os.path.exists(logdir):
226            os.mkdir(logdir)
227        return logdir
228
229
230    @log.log_and_ignore_errors("post-reboot sysinfo error:")
231    def log_per_reboot_data(self):
232        """ Logging hook called whenever a job starts, and again after
233        any reboot. """
234        logdir = self._get_boot_subdir(next=True)
235        if not os.path.exists(logdir):
236            os.mkdir(logdir)
237
238        for log in (self.test_loggables | self.boot_loggables):
239            log.run(logdir)
240
241        # also log any installed packages
242        installed_path = os.path.join(logdir, "installed_packages")
243        installed_packages = "\n".join(package.list_all()) + "\n"
244        utils.open_write_close(installed_path, installed_packages)
245
246
247    @log.log_and_ignore_errors("pre-test sysinfo error:")
248    def log_before_each_test(self, test):
249        """ Logging hook called before a test starts. """
250        self._installed_packages = package.list_all()
251        if os.path.exists("/var/log/messages"):
252            stat = os.stat("/var/log/messages")
253            self._messages_size = stat.st_size
254            self._messages_inode = stat.st_ino
255
256
257    @log.log_and_ignore_errors("post-test sysinfo error:")
258    def log_after_each_test(self, test):
259        """ Logging hook called after a test finishs. """
260        test_sysinfodir = self._get_sysinfodir(test.outputdir)
261
262        # create a symlink in the test sysinfo dir to the current boot
263        reboot_dir = self._get_boot_subdir()
264        assert os.path.exists(reboot_dir)
265        symlink_dest = os.path.join(test_sysinfodir, "reboot_current")
266        symlink_src = utils.get_relative_path(reboot_dir,
267                                              os.path.dirname(symlink_dest))
268        try:
269            os.symlink(symlink_src, symlink_dest)
270        except Exception, e:
271            raise Exception, '%s: whilst linking %s to %s' % (e, symlink_src,
272                                                              symlink_dest)
273
274        # run all the standard logging commands
275        for log in self.test_loggables:
276            log.run(test_sysinfodir)
277
278        # grab any new data from /var/log/messages
279        self._log_messages(test_sysinfodir)
280
281        # log some sysinfo data into the test keyval file
282        keyval = self.log_test_keyvals(test_sysinfodir)
283        test.write_test_keyval(keyval)
284
285        # log any changes to installed packages
286        old_packages = set(self._installed_packages)
287        new_packages = set(package.list_all())
288        added_path = os.path.join(test_sysinfodir, "added_packages")
289        added_packages = "\n".join(new_packages - old_packages) + "\n"
290        utils.open_write_close(added_path, added_packages)
291        removed_path = os.path.join(test_sysinfodir, "removed_packages")
292        removed_packages = "\n".join(old_packages - new_packages) + "\n"
293        utils.open_write_close(removed_path, removed_packages)
294
295
296    @log.log_and_ignore_errors("pre-test siteration sysinfo error:")
297    def log_before_each_iteration(self, test, iteration=None):
298        """ Logging hook called before a test iteration."""
299        if not iteration:
300            iteration = test.iteration
301        logdir = self._get_iteration_subdir(test, iteration)
302
303        for log in self.before_iteration_loggables:
304            log.run(logdir)
305
306
307    @log.log_and_ignore_errors("post-test siteration sysinfo error:")
308    def log_after_each_iteration(self, test, iteration=None):
309        """ Logging hook called after a test iteration."""
310        if not iteration:
311            iteration = test.iteration
312        logdir = self._get_iteration_subdir(test, iteration)
313
314        for log in self.after_iteration_loggables:
315            log.run(logdir)
316
317
318    def _log_messages(self, logdir):
319        """ Log all of the new data in /var/log/messages. """
320        try:
321            # log all of the new data in /var/log/messages
322            bytes_to_skip = 0
323            if hasattr(self, "_messages_size"):
324                current_inode = os.stat("/var/log/messages").st_ino
325                if current_inode == self._messages_inode:
326                    bytes_to_skip = self._messages_size
327            in_messages = open("/var/log/messages")
328            in_messages.seek(bytes_to_skip)
329            out_messages = open(os.path.join(logdir, "messages"), "w")
330            out_messages.write(in_messages.read())
331            in_messages.close()
332            out_messages.close()
333        except Exception, e:
334            logging.error("/var/log/messages collection failed with %s", e)
335
336
337    @staticmethod
338    def _read_sysinfo_keyvals(loggables, logdir):
339        keyval = {}
340        for log in loggables:
341            if log.log_in_keyval:
342                keyval["sysinfo-" + log.logf] = log.readline(logdir)
343        return keyval
344
345
346    def log_test_keyvals(self, test_sysinfodir):
347        """ Logging hook called by log_after_each_test to collect keyval
348        entries to be written in the test keyval. """
349        keyval = {}
350
351        # grab any loggables that should be in the keyval
352        keyval.update(self._read_sysinfo_keyvals(
353            self.test_loggables, test_sysinfodir))
354        keyval.update(self._read_sysinfo_keyvals(
355            self.boot_loggables,
356            os.path.join(test_sysinfodir, "reboot_current")))
357
358        # remove hostname from uname info
359        #   Linux lpt36 2.6.18-smp-230.1 #1 [4069269] SMP Fri Oct 24 11:30:...
360        if "sysinfo-uname" in keyval:
361            kernel_vers = " ".join(keyval["sysinfo-uname"].split()[2:])
362            keyval["sysinfo-uname"] = kernel_vers
363
364        # grab the total avail memory, not used by sys tables
365        path = os.path.join(test_sysinfodir, "reboot_current", "meminfo")
366        if os.path.exists(path):
367            mem_data = open(path).read()
368            match = re.search(r"^MemTotal:\s+(\d+) kB$", mem_data,
369                              re.MULTILINE)
370            if match:
371                keyval["sysinfo-memtotal-in-kb"] = match.group(1)
372
373        # guess the system's total physical memory, including sys tables
374        keyval["sysinfo-phys-mbytes"] = utils.rounded_memtotal()//1024
375
376        # return what we collected
377        return keyval
378