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