| # pylint: disable=missing-docstring |
| import os |
| import re |
| |
| import common |
| from autotest_lib.tko import models |
| from autotest_lib.tko import status_lib |
| from autotest_lib.tko import utils as tko_utils |
| from autotest_lib.tko.parsers import base |
| |
| class NoHostnameError(Exception): |
| pass |
| |
| |
| class BoardLabelError(Exception): |
| pass |
| |
| |
| class job(models.job): |
| def __init__(self, dir): |
| job_dict = job.load_from_dir(dir) |
| super(job, self).__init__(dir, **job_dict) |
| |
| |
| @classmethod |
| def load_from_dir(cls, dir): |
| keyval = cls.read_keyval(dir) |
| tko_utils.dprint(str(keyval)) |
| |
| user = keyval.get("user", None) |
| label = keyval.get("label", None) |
| queued_time = tko_utils.get_timestamp(keyval, "job_queued") |
| started_time = tko_utils.get_timestamp(keyval, "job_started") |
| finished_time = tko_utils.get_timestamp(keyval, "job_finished") |
| machine = cls.determine_hostname(keyval, dir) |
| machine_group = cls.determine_machine_group(machine, dir) |
| machine_owner = keyval.get("owner", None) |
| |
| aborted_by = keyval.get("aborted_by", None) |
| aborted_at = tko_utils.get_timestamp(keyval, "aborted_on") |
| |
| return {"user": user, "label": label, "machine": machine, |
| "queued_time": queued_time, "started_time": started_time, |
| "finished_time": finished_time, "machine_owner": machine_owner, |
| "machine_group": machine_group, "aborted_by": aborted_by, |
| "aborted_on": aborted_at, "keyval_dict": keyval} |
| |
| |
| @classmethod |
| def determine_hostname(cls, keyval, job_dir): |
| host_group_name = keyval.get("host_group_name", None) |
| machine = keyval.get("hostname", "") |
| is_multimachine = "," in machine |
| |
| # determine what hostname to use |
| if host_group_name: |
| if is_multimachine or not machine: |
| tko_utils.dprint("Using host_group_name %r instead of " |
| "machine name." % host_group_name) |
| machine = host_group_name |
| elif is_multimachine: |
| try: |
| machine = job.find_hostname(job_dir) # find a unique hostname |
| except NoHostnameError: |
| pass # just use the comma-separated name |
| |
| tko_utils.dprint("MACHINE NAME: %s" % machine) |
| return machine |
| |
| |
| @classmethod |
| def determine_machine_group(cls, hostname, job_dir): |
| machine_groups = set() |
| for individual_hostname in hostname.split(","): |
| host_keyval = models.test.parse_host_keyval(job_dir, |
| individual_hostname) |
| if not host_keyval: |
| tko_utils.dprint('Unable to parse host keyval for %s' |
| % individual_hostname) |
| elif 'labels' in host_keyval: |
| # Use `model` label as machine group. This is to avoid the |
| # confusion of multiple boards mapping to the same platform in |
| # wmatrix. With this change, wmatrix will group tests with the |
| # same model, rather than the same platform. |
| labels = host_keyval['labels'].split(',') |
| board_labels = [l[8:] for l in labels |
| if l.startswith('model%3A')] |
| # If the host doesn't have `model:` label, fall back to `board:` |
| # label. |
| if not board_labels: |
| board_labels = [l[8:] for l in labels |
| if l.startswith('board%3A')] |
| if board_labels: |
| # Multiple board/model labels aren't supposed to |
| # happen, but let's report something sane rather |
| # than just failing. |
| machine_groups.add(','.join(board_labels)) |
| else: |
| error = ('Failed to retrieve board label from host labels: ' |
| '%s' % host_keyval['labels']) |
| tko_utils.dprint(error) |
| raise BoardLabelError(error) |
| elif "platform" in host_keyval: |
| machine_groups.add(host_keyval["platform"]) |
| machine_group = ",".join(sorted(machine_groups)) |
| tko_utils.dprint("MACHINE GROUP: %s" % machine_group) |
| return machine_group |
| |
| |
| @staticmethod |
| def find_hostname(path): |
| hostname = os.path.join(path, "sysinfo", "hostname") |
| try: |
| machine = open(hostname).readline().rstrip() |
| return machine |
| except Exception: |
| tko_utils.dprint("Could not read a hostname from " |
| "sysinfo/hostname") |
| |
| uname = os.path.join(path, "sysinfo", "uname_-a") |
| try: |
| machine = open(uname).readline().split()[1] |
| return machine |
| except Exception: |
| tko_utils.dprint("Could not read a hostname from " |
| "sysinfo/uname_-a") |
| |
| raise NoHostnameError("Unable to find a machine name") |
| |
| |
| class kernel(models.kernel): |
| def __init__(self, job, verify_ident=None): |
| kernel_dict = kernel.load_from_dir(job.dir, verify_ident) |
| super(kernel, self).__init__(**kernel_dict) |
| |
| |
| @staticmethod |
| def load_from_dir(dir, verify_ident=None): |
| # try and load the booted kernel version |
| attributes = False |
| i = 1 |
| build_dir = os.path.join(dir, "build") |
| while True: |
| if not os.path.exists(build_dir): |
| break |
| build_log = os.path.join(build_dir, "debug", "build_log") |
| attributes = kernel.load_from_build_log(build_log) |
| if attributes: |
| break |
| i += 1 |
| build_dir = os.path.join(dir, "build.%d" % (i)) |
| |
| if not attributes: |
| if verify_ident: |
| base = verify_ident |
| else: |
| base = kernel.load_from_sysinfo(dir) |
| patches = [] |
| hashes = [] |
| else: |
| base, patches, hashes = attributes |
| tko_utils.dprint("kernel.__init__() found kernel version %s" |
| % base) |
| |
| # compute the kernel hash |
| if base == "UNKNOWN": |
| kernel_hash = "UNKNOWN" |
| else: |
| kernel_hash = kernel.compute_hash(base, hashes) |
| |
| return {"base": base, "patches": patches, |
| "kernel_hash": kernel_hash} |
| |
| |
| @staticmethod |
| def load_from_sysinfo(path): |
| for subdir in ("reboot1", ""): |
| uname_path = os.path.join(path, "sysinfo", subdir, |
| "uname_-a") |
| if not os.path.exists(uname_path): |
| continue |
| uname = open(uname_path).readline().split() |
| return re.sub("-autotest$", "", uname[2]) |
| return "UNKNOWN" |
| |
| |
| @staticmethod |
| def load_from_build_log(path): |
| if not os.path.exists(path): |
| return None |
| |
| base, patches, hashes = "UNKNOWN", [], [] |
| for line in file(path): |
| head, rest = line.split(": ", 1) |
| rest = rest.split() |
| if head == "BASE": |
| base = rest[0] |
| elif head == "PATCH": |
| patches.append(patch(*rest)) |
| hashes.append(rest[2]) |
| return base, patches, hashes |
| |
| |
| class test(models.test): |
| def __init__(self, subdir, testname, status, reason, test_kernel, |
| machine, started_time, finished_time, iterations, |
| attributes, labels): |
| # for backwards compatibility with the original parser |
| # implementation, if there is no test version we need a NULL |
| # value to be used; also, if there is a version it should |
| # be terminated by a newline |
| if "version" in attributes: |
| attributes["version"] = str(attributes["version"]) |
| else: |
| attributes["version"] = None |
| |
| super(test, self).__init__(subdir, testname, status, reason, |
| test_kernel, machine, started_time, |
| finished_time, iterations, |
| attributes, labels) |
| |
| |
| @staticmethod |
| def load_iterations(keyval_path): |
| return iteration.load_from_keyval(keyval_path) |
| |
| |
| class patch(models.patch): |
| def __init__(self, spec, reference, hash): |
| tko_utils.dprint("PATCH::%s %s %s" % (spec, reference, hash)) |
| super(patch, self).__init__(spec, reference, hash) |
| self.spec = spec |
| self.reference = reference |
| self.hash = hash |
| |
| |
| class iteration(models.iteration): |
| @staticmethod |
| def parse_line_into_dicts(line, attr_dict, perf_dict): |
| key, value = line.split("=", 1) |
| perf_dict[key] = value |
| |
| |
| class status_line(object): |
| def __init__(self, indent, status, subdir, testname, reason, |
| optional_fields): |
| # pull out the type & status of the line |
| if status == "START": |
| self.type = "START" |
| self.status = None |
| elif status.startswith("END "): |
| self.type = "END" |
| self.status = status[4:] |
| else: |
| self.type = "STATUS" |
| self.status = status |
| assert (self.status is None or |
| self.status in status_lib.statuses) |
| |
| # save all the other parameters |
| self.indent = indent |
| self.subdir = self.parse_name(subdir) |
| self.testname = self.parse_name(testname) |
| self.reason = reason |
| self.optional_fields = optional_fields |
| |
| |
| @staticmethod |
| def parse_name(name): |
| if name == "----": |
| return None |
| return name |
| |
| |
| @staticmethod |
| def is_status_line(line): |
| return re.search(r"^\t*(\S[^\t]*\t){3}", line) is not None |
| |
| |
| @classmethod |
| def parse_line(cls, line): |
| if not status_line.is_status_line(line): |
| return None |
| match = re.search(r"^(\t*)(.*)$", line, flags=re.DOTALL) |
| if not match: |
| # A more useful error message than: |
| # AttributeError: 'NoneType' object has no attribute 'groups' |
| # to help us debug WTF happens on occasion here. |
| raise RuntimeError("line %r could not be parsed." % line) |
| indent, line = match.groups() |
| indent = len(indent) |
| |
| # split the line into the fixed and optional fields |
| parts = line.rstrip("\n").split("\t") |
| |
| part_index = 3 |
| status, subdir, testname = parts[0:part_index] |
| |
| # all optional parts should be of the form "key=value". once we've found |
| # a non-matching part, treat it and the rest of the parts as the reason. |
| optional_fields = {} |
| while part_index < len(parts): |
| kv = re.search(r"^(\w+)=(.+)", parts[part_index]) |
| if not kv: |
| break |
| |
| optional_fields[kv.group(1)] = kv.group(2) |
| part_index += 1 |
| |
| reason = "\t".join(parts[part_index:]) |
| |
| # build up a new status_line and return it |
| return cls(indent, status, subdir, testname, reason, |
| optional_fields) |
| |
| |
| class parser(base.parser): |
| @staticmethod |
| def make_job(dir): |
| return job(dir) |
| |
| |
| def state_iterator(self, buffer): |
| new_tests = [] |
| boot_count = 0 |
| group_subdir = None |
| sought_level = 0 |
| stack = status_lib.status_stack() |
| current_kernel = kernel(self.job) |
| boot_in_progress = False |
| alert_pending = None |
| started_time = None |
| |
| while not self.finished or buffer.size(): |
| # stop processing once the buffer is empty |
| if buffer.size() == 0: |
| yield new_tests |
| new_tests = [] |
| continue |
| |
| # parse the next line |
| line = buffer.get() |
| tko_utils.dprint('\nSTATUS: ' + line.strip()) |
| line = status_line.parse_line(line) |
| if line is None: |
| tko_utils.dprint('non-status line, ignoring') |
| continue # ignore non-status lines |
| |
| # have we hit the job start line? |
| if (line.type == "START" and not line.subdir and |
| not line.testname): |
| sought_level = 1 |
| tko_utils.dprint("found job level start " |
| "marker, looking for level " |
| "1 groups now") |
| continue |
| |
| # have we hit the job end line? |
| if (line.type == "END" and not line.subdir and |
| not line.testname): |
| tko_utils.dprint("found job level end " |
| "marker, looking for level " |
| "0 lines now") |
| sought_level = 0 |
| |
| # START line, just push another layer on to the stack |
| # and grab the start time if this is at the job level |
| # we're currently seeking |
| if line.type == "START": |
| group_subdir = None |
| stack.start() |
| if line.indent == sought_level: |
| started_time = \ |
| tko_utils.get_timestamp( |
| line.optional_fields, "timestamp") |
| tko_utils.dprint("start line, ignoring") |
| continue |
| # otherwise, update the status on the stack |
| else: |
| tko_utils.dprint("GROPE_STATUS: %s" % |
| [stack.current_status(), |
| line.status, line.subdir, |
| line.testname, line.reason]) |
| stack.update(line.status) |
| |
| if line.status == "ALERT": |
| tko_utils.dprint("job level alert, recording") |
| alert_pending = line.reason |
| continue |
| |
| # ignore Autotest.install => GOOD lines |
| if (line.testname == "Autotest.install" and |
| line.status == "GOOD"): |
| tko_utils.dprint("Successful Autotest " |
| "install, ignoring") |
| continue |
| |
| # ignore END lines for a reboot group |
| if (line.testname == "reboot" and line.type == "END"): |
| tko_utils.dprint("reboot group, ignoring") |
| continue |
| |
| # convert job-level ABORTs into a 'CLIENT_JOB' test, and |
| # ignore other job-level events |
| if line.testname is None: |
| if (line.status == "ABORT" and |
| line.type != "END"): |
| line.testname = "CLIENT_JOB" |
| else: |
| tko_utils.dprint("job level event, " |
| "ignoring") |
| continue |
| |
| # use the group subdir for END lines |
| if line.type == "END": |
| line.subdir = group_subdir |
| |
| # are we inside a block group? |
| if (line.indent != sought_level and |
| line.status != "ABORT" and |
| not line.testname.startswith('reboot.')): |
| if line.subdir: |
| tko_utils.dprint("set group_subdir: " |
| + line.subdir) |
| group_subdir = line.subdir |
| tko_utils.dprint("ignoring incorrect indent " |
| "level %d != %d," % |
| (line.indent, sought_level)) |
| continue |
| |
| # use the subdir as the testname, except for |
| # boot.* and kernel.* tests |
| if (line.testname is None or |
| not re.search(r"^(boot(\.\d+)?$|kernel\.)", |
| line.testname)): |
| if line.subdir and '.' in line.subdir: |
| line.testname = line.subdir |
| |
| # has a reboot started? |
| if line.testname == "reboot.start": |
| started_time = tko_utils.get_timestamp( |
| line.optional_fields, "timestamp") |
| tko_utils.dprint("reboot start event, " |
| "ignoring") |
| boot_in_progress = True |
| continue |
| |
| # has a reboot finished? |
| if line.testname == "reboot.verify": |
| line.testname = "boot.%d" % boot_count |
| tko_utils.dprint("reboot verified") |
| boot_in_progress = False |
| verify_ident = line.reason.strip() |
| current_kernel = kernel(self.job, verify_ident) |
| boot_count += 1 |
| |
| if alert_pending: |
| line.status = "ALERT" |
| line.reason = alert_pending |
| alert_pending = None |
| |
| # create the actual test object |
| finished_time = tko_utils.get_timestamp( |
| line.optional_fields, "timestamp") |
| final_status = stack.end() |
| tko_utils.dprint("Adding: " |
| "%s\nSubdir:%s\nTestname:%s\n%s" % |
| (final_status, line.subdir, |
| line.testname, line.reason)) |
| new_test = test.parse_test(self.job, line.subdir, |
| line.testname, |
| final_status, line.reason, |
| current_kernel, |
| started_time, |
| finished_time) |
| started_time = None |
| new_tests.append(new_test) |
| |
| # the job is finished, but we never came back from reboot |
| if boot_in_progress: |
| testname = "boot.%d" % boot_count |
| reason = "machine did not return from reboot" |
| tko_utils.dprint(("Adding: ABORT\nSubdir:----\n" |
| "Testname:%s\n%s") |
| % (testname, reason)) |
| new_test = test.parse_test(self.job, None, testname, |
| "ABORT", reason, |
| current_kernel, None, None) |
| new_tests.append(new_test) |
| yield new_tests |