You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
491 lines
18 KiB
491 lines
18 KiB
# 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
|