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