Home | History | Annotate | Download | only in parsers
      1 import os
      2 import re
      3 
      4 import common
      5 from autotest_lib.tko import models
      6 from autotest_lib.tko import status_lib
      7 from autotest_lib.tko import utils as tko_utils
      8 from autotest_lib.tko.parsers import base
      9 
     10 class NoHostnameError(Exception):
     11     pass
     12 
     13 
     14 class BoardLabelError(Exception):
     15     pass
     16 
     17 
     18 class job(models.job):
     19     def __init__(self, dir):
     20         job_dict = job.load_from_dir(dir)
     21         super(job, self).__init__(dir, **job_dict)
     22 
     23 
     24     @classmethod
     25     def load_from_dir(cls, dir):
     26         keyval = cls.read_keyval(dir)
     27         tko_utils.dprint(str(keyval))
     28 
     29         user = keyval.get("user", None)
     30         label = keyval.get("label", None)
     31         queued_time = tko_utils.get_timestamp(keyval, "job_queued")
     32         started_time = tko_utils.get_timestamp(keyval, "job_started")
     33         finished_time = tko_utils.get_timestamp(keyval, "job_finished")
     34         machine = cls.determine_hostname(keyval, dir)
     35         machine_group = cls.determine_machine_group(machine, dir)
     36         machine_owner = keyval.get("owner", None)
     37 
     38         aborted_by = keyval.get("aborted_by", None)
     39         aborted_at = tko_utils.get_timestamp(keyval, "aborted_on")
     40 
     41         return {"user": user, "label": label, "machine": machine,
     42                 "queued_time": queued_time, "started_time": started_time,
     43                 "finished_time": finished_time, "machine_owner": machine_owner,
     44                 "machine_group": machine_group, "aborted_by": aborted_by,
     45                 "aborted_on": aborted_at, "keyval_dict": keyval}
     46 
     47 
     48     @classmethod
     49     def determine_hostname(cls, keyval, job_dir):
     50         host_group_name = keyval.get("host_group_name", None)
     51         machine = keyval.get("hostname", "")
     52         is_multimachine = "," in machine
     53 
     54         # determine what hostname to use
     55         if host_group_name:
     56             if is_multimachine or not machine:
     57                 tko_utils.dprint("Using host_group_name %r instead of "
     58                                  "machine name." % host_group_name)
     59                 machine = host_group_name
     60         elif is_multimachine:
     61             try:
     62                 machine = job.find_hostname(job_dir) # find a unique hostname
     63             except NoHostnameError:
     64                 pass  # just use the comma-separated name
     65 
     66         tko_utils.dprint("MACHINE NAME: %s" % machine)
     67         return machine
     68 
     69 
     70     @classmethod
     71     def determine_machine_group(cls, hostname, job_dir):
     72         machine_groups = set()
     73         for individual_hostname in hostname.split(","):
     74             host_keyval = models.test.parse_host_keyval(job_dir,
     75                                                         individual_hostname)
     76             if not host_keyval:
     77                 tko_utils.dprint('Unable to parse host keyval for %s'
     78                                  % individual_hostname)
     79             elif 'labels' in host_keyval:
     80                 # Use `model` label as machine group. This is to avoid the
     81                 # confusion of multiple boards mapping to the same platform in
     82                 # wmatrix. With this change, wmatrix will group tests with the
     83                 # same model, rather than the same platform.
     84                 labels = host_keyval['labels'].split(',')
     85                 board_labels = [l[8:] for l in labels
     86                                if l.startswith('model%3A')]
     87                 # If the host doesn't have `model:` label, fall back to `board:`
     88                 # label.
     89                 if not board_labels:
     90                     board_labels = [l[8:] for l in labels
     91                                if l.startswith('board%3A')]
     92                 if board_labels:
     93                     # Testbeds have multiple boards so concat them into a
     94                     # single string then add it to the machine_groups list.
     95                     machine_groups.add(','.join(board_labels))
     96                 else:
     97                     error = ('Failed to retrieve board label from host labels: '
     98                              '%s' % host_keyval['labels'])
     99                     tko_utils.dprint(error)
    100                     raise BoardLabelError(error)
    101             elif "platform" in host_keyval:
    102                 machine_groups.add(host_keyval["platform"])
    103         machine_group = ",".join(sorted(machine_groups))
    104         tko_utils.dprint("MACHINE GROUP: %s" % machine_group)
    105         return machine_group
    106 
    107 
    108     @staticmethod
    109     def find_hostname(path):
    110         hostname = os.path.join(path, "sysinfo", "hostname")
    111         try:
    112             machine = open(hostname).readline().rstrip()
    113             return machine
    114         except Exception:
    115             tko_utils.dprint("Could not read a hostname from "
    116                              "sysinfo/hostname")
    117 
    118         uname = os.path.join(path, "sysinfo", "uname_-a")
    119         try:
    120             machine = open(uname).readline().split()[1]
    121             return machine
    122         except Exception:
    123             tko_utils.dprint("Could not read a hostname from "
    124                              "sysinfo/uname_-a")
    125 
    126         raise NoHostnameError("Unable to find a machine name")
    127 
    128 
    129 class kernel(models.kernel):
    130     def __init__(self, job, verify_ident=None):
    131         kernel_dict = kernel.load_from_dir(job.dir, verify_ident)
    132         super(kernel, self).__init__(**kernel_dict)
    133 
    134 
    135     @staticmethod
    136     def load_from_dir(dir, verify_ident=None):
    137         # try and load the booted kernel version
    138         attributes = False
    139         i = 1
    140         build_dir = os.path.join(dir, "build")
    141         while True:
    142             if not os.path.exists(build_dir):
    143                 break
    144             build_log = os.path.join(build_dir, "debug", "build_log")
    145             attributes = kernel.load_from_build_log(build_log)
    146             if attributes:
    147                 break
    148             i += 1
    149             build_dir = os.path.join(dir, "build.%d" % (i))
    150 
    151         if not attributes:
    152             if verify_ident:
    153                 base = verify_ident
    154             else:
    155                 base = kernel.load_from_sysinfo(dir)
    156             patches = []
    157             hashes = []
    158         else:
    159             base, patches, hashes = attributes
    160         tko_utils.dprint("kernel.__init__() found kernel version %s"
    161                          % base)
    162 
    163         # compute the kernel hash
    164         if base == "UNKNOWN":
    165             kernel_hash = "UNKNOWN"
    166         else:
    167             kernel_hash = kernel.compute_hash(base, hashes)
    168 
    169         return {"base": base, "patches": patches,
    170                 "kernel_hash": kernel_hash}
    171 
    172 
    173     @staticmethod
    174     def load_from_sysinfo(path):
    175         for subdir in ("reboot1", ""):
    176             uname_path = os.path.join(path, "sysinfo", subdir,
    177                                       "uname_-a")
    178             if not os.path.exists(uname_path):
    179                 continue
    180             uname = open(uname_path).readline().split()
    181             return re.sub("-autotest$", "", uname[2])
    182         return "UNKNOWN"
    183 
    184 
    185     @staticmethod
    186     def load_from_build_log(path):
    187         if not os.path.exists(path):
    188             return None
    189 
    190         base, patches, hashes = "UNKNOWN", [], []
    191         for line in file(path):
    192             head, rest = line.split(": ", 1)
    193             rest = rest.split()
    194             if head == "BASE":
    195                 base = rest[0]
    196             elif head == "PATCH":
    197                 patches.append(patch(*rest))
    198                 hashes.append(rest[2])
    199         return base, patches, hashes
    200 
    201 
    202 class test(models.test):
    203     def __init__(self, subdir, testname, status, reason, test_kernel,
    204                  machine, started_time, finished_time, iterations,
    205                  attributes, labels):
    206         # for backwards compatibility with the original parser
    207         # implementation, if there is no test version we need a NULL
    208         # value to be used; also, if there is a version it should
    209         # be terminated by a newline
    210         if "version" in attributes:
    211             attributes["version"] = str(attributes["version"])
    212         else:
    213             attributes["version"] = None
    214 
    215         super(test, self).__init__(subdir, testname, status, reason,
    216                                    test_kernel, machine, started_time,
    217                                    finished_time, iterations,
    218                                    attributes, labels)
    219 
    220 
    221     @staticmethod
    222     def load_iterations(keyval_path):
    223         return iteration.load_from_keyval(keyval_path)
    224 
    225 
    226 class patch(models.patch):
    227     def __init__(self, spec, reference, hash):
    228         tko_utils.dprint("PATCH::%s %s %s" % (spec, reference, hash))
    229         super(patch, self).__init__(spec, reference, hash)
    230         self.spec = spec
    231         self.reference = reference
    232         self.hash = hash
    233 
    234 
    235 class iteration(models.iteration):
    236     @staticmethod
    237     def parse_line_into_dicts(line, attr_dict, perf_dict):
    238         key, value = line.split("=", 1)
    239         perf_dict[key] = value
    240 
    241 
    242 class status_line(object):
    243     def __init__(self, indent, status, subdir, testname, reason,
    244                  optional_fields):
    245         # pull out the type & status of the line
    246         if status == "START":
    247             self.type = "START"
    248             self.status = None
    249         elif status.startswith("END "):
    250             self.type = "END"
    251             self.status = status[4:]
    252         else:
    253             self.type = "STATUS"
    254             self.status = status
    255         assert (self.status is None or
    256                 self.status in status_lib.statuses)
    257 
    258         # save all the other parameters
    259         self.indent = indent
    260         self.subdir = self.parse_name(subdir)
    261         self.testname = self.parse_name(testname)
    262         self.reason = reason
    263         self.optional_fields = optional_fields
    264 
    265 
    266     @staticmethod
    267     def parse_name(name):
    268         if name == "----":
    269             return None
    270         return name
    271 
    272 
    273     @staticmethod
    274     def is_status_line(line):
    275         return re.search(r"^\t*(\S[^\t]*\t){3}", line) is not None
    276 
    277 
    278     @classmethod
    279     def parse_line(cls, line):
    280         if not status_line.is_status_line(line):
    281             return None
    282         match = re.search(r"^(\t*)(.*)$", line, flags=re.DOTALL)
    283         if not match:
    284             # A more useful error message than:
    285             #  AttributeError: 'NoneType' object has no attribute 'groups'
    286             # to help us debug WTF happens on occasion here.
    287             raise RuntimeError("line %r could not be parsed." % line)
    288         indent, line = match.groups()
    289         indent = len(indent)
    290 
    291         # split the line into the fixed and optional fields
    292         parts = line.rstrip("\n").split("\t")
    293 
    294         part_index = 3
    295         status, subdir, testname = parts[0:part_index]
    296 
    297         # all optional parts should be of the form "key=value". once we've found
    298         # a non-matching part, treat it and the rest of the parts as the reason.
    299         optional_fields = {}
    300         while part_index < len(parts):
    301             kv = re.search(r"^(\w+)=(.+)", parts[part_index])
    302             if not kv:
    303                 break
    304 
    305             optional_fields[kv.group(1)] = kv.group(2)
    306             part_index += 1
    307 
    308         reason = "\t".join(parts[part_index:])
    309 
    310         # build up a new status_line and return it
    311         return cls(indent, status, subdir, testname, reason,
    312                    optional_fields)
    313 
    314 
    315 class parser(base.parser):
    316     @staticmethod
    317     def make_job(dir):
    318         return job(dir)
    319 
    320 
    321     def state_iterator(self, buffer):
    322         new_tests = []
    323         boot_count = 0
    324         group_subdir = None
    325         sought_level = 0
    326         stack = status_lib.status_stack()
    327         current_kernel = kernel(self.job)
    328         boot_in_progress = False
    329         alert_pending = None
    330         started_time = None
    331 
    332         while not self.finished or buffer.size():
    333             # stop processing once the buffer is empty
    334             if buffer.size() == 0:
    335                 yield new_tests
    336                 new_tests = []
    337                 continue
    338 
    339             # parse the next line
    340             line = buffer.get()
    341             tko_utils.dprint('\nSTATUS: ' + line.strip())
    342             line = status_line.parse_line(line)
    343             if line is None:
    344                 tko_utils.dprint('non-status line, ignoring')
    345                 continue # ignore non-status lines
    346 
    347             # have we hit the job start line?
    348             if (line.type == "START" and not line.subdir and
    349                 not line.testname):
    350                 sought_level = 1
    351                 tko_utils.dprint("found job level start "
    352                                  "marker, looking for level "
    353                                  "1 groups now")
    354                 continue
    355 
    356             # have we hit the job end line?
    357             if (line.type == "END" and not line.subdir and
    358                 not line.testname):
    359                 tko_utils.dprint("found job level end "
    360                                  "marker, looking for level "
    361                                  "0 lines now")
    362                 sought_level = 0
    363 
    364             # START line, just push another layer on to the stack
    365             # and grab the start time if this is at the job level
    366             # we're currently seeking
    367             if line.type == "START":
    368                 group_subdir = None
    369                 stack.start()
    370                 if line.indent == sought_level:
    371                     started_time = \
    372                                  tko_utils.get_timestamp(
    373                         line.optional_fields, "timestamp")
    374                 tko_utils.dprint("start line, ignoring")
    375                 continue
    376             # otherwise, update the status on the stack
    377             else:
    378                 tko_utils.dprint("GROPE_STATUS: %s" %
    379                                  [stack.current_status(),
    380                                   line.status, line.subdir,
    381                                   line.testname, line.reason])
    382                 stack.update(line.status)
    383 
    384             if line.status == "ALERT":
    385                 tko_utils.dprint("job level alert, recording")
    386                 alert_pending = line.reason
    387                 continue
    388 
    389             # ignore Autotest.install => GOOD lines
    390             if (line.testname == "Autotest.install" and
    391                 line.status == "GOOD"):
    392                 tko_utils.dprint("Successful Autotest "
    393                                  "install, ignoring")
    394                 continue
    395 
    396             # ignore END lines for a reboot group
    397             if (line.testname == "reboot" and line.type == "END"):
    398                 tko_utils.dprint("reboot group, ignoring")
    399                 continue
    400 
    401             # convert job-level ABORTs into a 'CLIENT_JOB' test, and
    402             # ignore other job-level events
    403             if line.testname is None:
    404                 if (line.status == "ABORT" and
    405                     line.type != "END"):
    406                     line.testname = "CLIENT_JOB"
    407                 else:
    408                     tko_utils.dprint("job level event, "
    409                                     "ignoring")
    410                     continue
    411 
    412             # use the group subdir for END lines
    413             if line.type == "END":
    414                 line.subdir = group_subdir
    415 
    416             # are we inside a block group?
    417             if (line.indent != sought_level and
    418                 line.status != "ABORT" and
    419                 not line.testname.startswith('reboot.')):
    420                 if line.subdir:
    421                     tko_utils.dprint("set group_subdir: "
    422                                      + line.subdir)
    423                     group_subdir = line.subdir
    424                 tko_utils.dprint("ignoring incorrect indent "
    425                                  "level %d != %d," %
    426                                  (line.indent, sought_level))
    427                 continue
    428 
    429             # use the subdir as the testname, except for
    430             # boot.* and kernel.* tests
    431             if (line.testname is None or
    432                 not re.search(r"^(boot(\.\d+)?$|kernel\.)",
    433                               line.testname)):
    434                 if line.subdir and '.' in line.subdir:
    435                     line.testname = line.subdir
    436 
    437             # has a reboot started?
    438             if line.testname == "reboot.start":
    439                 started_time = tko_utils.get_timestamp(
    440                     line.optional_fields, "timestamp")
    441                 tko_utils.dprint("reboot start event, "
    442                                  "ignoring")
    443                 boot_in_progress = True
    444                 continue
    445 
    446             # has a reboot finished?
    447             if line.testname == "reboot.verify":
    448                 line.testname = "boot.%d" % boot_count
    449                 tko_utils.dprint("reboot verified")
    450                 boot_in_progress = False
    451                 verify_ident = line.reason.strip()
    452                 current_kernel = kernel(self.job, verify_ident)
    453                 boot_count += 1
    454 
    455             if alert_pending:
    456                 line.status = "ALERT"
    457                 line.reason = alert_pending
    458                 alert_pending = None
    459 
    460             # create the actual test object
    461             finished_time = tko_utils.get_timestamp(
    462                 line.optional_fields, "timestamp")
    463             final_status = stack.end()
    464             tko_utils.dprint("Adding: "
    465                              "%s\nSubdir:%s\nTestname:%s\n%s" %
    466                              (final_status, line.subdir,
    467                               line.testname, line.reason))
    468             new_test = test.parse_test(self.job, line.subdir,
    469                                        line.testname,
    470                                        final_status, line.reason,
    471                                        current_kernel,
    472                                        started_time,
    473                                        finished_time)
    474             started_time = None
    475             new_tests.append(new_test)
    476 
    477         # the job is finished, but we never came back from reboot
    478         if boot_in_progress:
    479             testname = "boot.%d" % boot_count
    480             reason = "machine did not return from reboot"
    481             tko_utils.dprint(("Adding: ABORT\nSubdir:----\n"
    482                               "Testname:%s\n%s")
    483                              % (testname, reason))
    484             new_test = test.parse_test(self.job, None, testname,
    485                                        "ABORT", reason,
    486                                        current_kernel, None, None)
    487             new_tests.append(new_test)
    488         yield new_tests
    489