1import os, copy, logging, errno, fcntl, time, re, weakref, traceback 2import tarfile 3import cPickle as pickle 4import tempfile 5from autotest_lib.client.common_lib import autotemp, error, log 6 7 8class job_directory(object): 9 """Represents a job.*dir directory.""" 10 11 12 class JobDirectoryException(error.AutotestError): 13 """Generic job_directory exception superclass.""" 14 15 16 class MissingDirectoryException(JobDirectoryException): 17 """Raised when a directory required by the job does not exist.""" 18 def __init__(self, path): 19 Exception.__init__(self, 'Directory %s does not exist' % path) 20 21 22 class UncreatableDirectoryException(JobDirectoryException): 23 """Raised when a directory required by the job is missing and cannot 24 be created.""" 25 def __init__(self, path, error): 26 msg = 'Creation of directory %s failed with exception %s' 27 msg %= (path, error) 28 Exception.__init__(self, msg) 29 30 31 class UnwritableDirectoryException(JobDirectoryException): 32 """Raised when a writable directory required by the job exists 33 but is not writable.""" 34 def __init__(self, path): 35 msg = 'Directory %s exists but is not writable' % path 36 Exception.__init__(self, msg) 37 38 39 def __init__(self, path, is_writable=False): 40 """ 41 Instantiate a job directory. 42 43 @param path: The path of the directory. If None a temporary directory 44 will be created instead. 45 @param is_writable: If True, expect the directory to be writable. 46 47 @raise MissingDirectoryException: raised if is_writable=False and the 48 directory does not exist. 49 @raise UnwritableDirectoryException: raised if is_writable=True and 50 the directory exists but is not writable. 51 @raise UncreatableDirectoryException: raised if is_writable=True, the 52 directory does not exist and it cannot be created. 53 """ 54 if path is None: 55 if is_writable: 56 self._tempdir = autotemp.tempdir(unique_id='autotest') 57 self.path = self._tempdir.name 58 else: 59 raise self.MissingDirectoryException(path) 60 else: 61 self._tempdir = None 62 self.path = path 63 self._ensure_valid(is_writable) 64 65 66 def _ensure_valid(self, is_writable): 67 """ 68 Ensure that this is a valid directory. 69 70 Will check if a directory exists, can optionally also enforce that 71 it be writable. It can optionally create it if necessary. Creation 72 will still fail if the path is rooted in a non-writable directory, or 73 if a file already exists at the given location. 74 75 @param dir_path A path where a directory should be located 76 @param is_writable A boolean indicating that the directory should 77 not only exist, but also be writable. 78 79 @raises MissingDirectoryException raised if is_writable=False and the 80 directory does not exist. 81 @raises UnwritableDirectoryException raised if is_writable=True and 82 the directory is not wrtiable. 83 @raises UncreatableDirectoryException raised if is_writable=True, the 84 directory does not exist and it cannot be created 85 """ 86 # ensure the directory exists 87 if is_writable: 88 try: 89 os.makedirs(self.path) 90 except OSError, e: 91 if e.errno != errno.EEXIST or not os.path.isdir(self.path): 92 raise self.UncreatableDirectoryException(self.path, e) 93 elif not os.path.isdir(self.path): 94 raise self.MissingDirectoryException(self.path) 95 96 # if is_writable=True, also check that the directory is writable 97 if is_writable and not os.access(self.path, os.W_OK): 98 raise self.UnwritableDirectoryException(self.path) 99 100 101 @staticmethod 102 def property_factory(attribute): 103 """ 104 Create a job.*dir -> job._*dir.path property accessor. 105 106 @param attribute A string with the name of the attribute this is 107 exposed as. '_'+attribute must then be attribute that holds 108 either None or a job_directory-like object. 109 110 @returns A read-only property object that exposes a job_directory path 111 """ 112 @property 113 def dir_property(self): 114 underlying_attribute = getattr(self, '_' + attribute) 115 if underlying_attribute is None: 116 return None 117 else: 118 return underlying_attribute.path 119 return dir_property 120 121 122# decorator for use with job_state methods 123def with_backing_lock(method): 124 """A decorator to perform a lock-*-unlock cycle. 125 126 When applied to a method, this decorator will automatically wrap 127 calls to the method in a backing file lock and before the call 128 followed by a backing file unlock. 129 """ 130 def wrapped_method(self, *args, **dargs): 131 already_have_lock = self._backing_file_lock is not None 132 if not already_have_lock: 133 self._lock_backing_file() 134 try: 135 return method(self, *args, **dargs) 136 finally: 137 if not already_have_lock: 138 self._unlock_backing_file() 139 wrapped_method.__name__ = method.__name__ 140 wrapped_method.__doc__ = method.__doc__ 141 return wrapped_method 142 143 144# decorator for use with job_state methods 145def with_backing_file(method): 146 """A decorator to perform a lock-read-*-write-unlock cycle. 147 148 When applied to a method, this decorator will automatically wrap 149 calls to the method in a lock-and-read before the call followed by a 150 write-and-unlock. Any operation that is reading or writing state 151 should be decorated with this method to ensure that backing file 152 state is consistently maintained. 153 """ 154 @with_backing_lock 155 def wrapped_method(self, *args, **dargs): 156 self._read_from_backing_file() 157 try: 158 return method(self, *args, **dargs) 159 finally: 160 self._write_to_backing_file() 161 wrapped_method.__name__ = method.__name__ 162 wrapped_method.__doc__ = method.__doc__ 163 return wrapped_method 164 165 166 167class job_state(object): 168 """A class for managing explicit job and user state, optionally persistent. 169 170 The class allows you to save state by name (like a dictionary). Any state 171 stored in this class should be picklable and deep copyable. While this is 172 not enforced it is recommended that only valid python identifiers be used 173 as names. Additionally, the namespace 'stateful_property' is used for 174 storing the valued associated with properties constructed using the 175 property_factory method. 176 """ 177 178 NO_DEFAULT = object() 179 PICKLE_PROTOCOL = 2 # highest protocol available in python 2.4 180 181 182 def __init__(self): 183 """Initialize the job state.""" 184 self._state = {} 185 self._backing_file = None 186 self._backing_file_initialized = False 187 self._backing_file_lock = None 188 189 190 def _lock_backing_file(self): 191 """Acquire a lock on the backing file.""" 192 if self._backing_file: 193 self._backing_file_lock = open(self._backing_file, 'a') 194 fcntl.flock(self._backing_file_lock, fcntl.LOCK_EX) 195 196 197 def _unlock_backing_file(self): 198 """Release a lock on the backing file.""" 199 if self._backing_file_lock: 200 fcntl.flock(self._backing_file_lock, fcntl.LOCK_UN) 201 self._backing_file_lock.close() 202 self._backing_file_lock = None 203 204 205 def read_from_file(self, file_path, merge=True): 206 """Read in any state from the file at file_path. 207 208 When merge=True, any state specified only in-memory will be preserved. 209 Any state specified on-disk will be set in-memory, even if an in-memory 210 setting already exists. 211 212 @param file_path: The path where the state should be read from. It must 213 exist but it can be empty. 214 @param merge: If true, merge the on-disk state with the in-memory 215 state. If false, replace the in-memory state with the on-disk 216 state. 217 218 @warning: This method is intentionally concurrency-unsafe. It makes no 219 attempt to control concurrent access to the file at file_path. 220 """ 221 222 # we can assume that the file exists 223 if os.path.getsize(file_path) == 0: 224 on_disk_state = {} 225 else: 226 on_disk_state = pickle.load(open(file_path)) 227 228 if merge: 229 # merge the on-disk state with the in-memory state 230 for namespace, namespace_dict in on_disk_state.iteritems(): 231 in_memory_namespace = self._state.setdefault(namespace, {}) 232 for name, value in namespace_dict.iteritems(): 233 if name in in_memory_namespace: 234 if in_memory_namespace[name] != value: 235 logging.info('Persistent value of %s.%s from %s ' 236 'overridding existing in-memory ' 237 'value', namespace, name, file_path) 238 in_memory_namespace[name] = value 239 else: 240 logging.debug('Value of %s.%s is unchanged, ' 241 'skipping import', namespace, name) 242 else: 243 logging.debug('Importing %s.%s from state file %s', 244 namespace, name, file_path) 245 in_memory_namespace[name] = value 246 else: 247 # just replace the in-memory state with the on-disk state 248 self._state = on_disk_state 249 250 # lock the backing file before we refresh it 251 with_backing_lock(self.__class__._write_to_backing_file)(self) 252 253 254 def write_to_file(self, file_path): 255 """Write out the current state to the given path. 256 257 @param file_path: The path where the state should be written out to. 258 Must be writable. 259 260 @warning: This method is intentionally concurrency-unsafe. It makes no 261 attempt to control concurrent access to the file at file_path. 262 """ 263 outfile = open(file_path, 'w') 264 try: 265 pickle.dump(self._state, outfile, self.PICKLE_PROTOCOL) 266 finally: 267 outfile.close() 268 269 270 def _read_from_backing_file(self): 271 """Refresh the current state from the backing file. 272 273 If the backing file has never been read before (indicated by checking 274 self._backing_file_initialized) it will merge the file with the 275 in-memory state, rather than overwriting it. 276 """ 277 if self._backing_file: 278 merge_backing_file = not self._backing_file_initialized 279 self.read_from_file(self._backing_file, merge=merge_backing_file) 280 self._backing_file_initialized = True 281 282 283 def _write_to_backing_file(self): 284 """Flush the current state to the backing file.""" 285 if self._backing_file: 286 self.write_to_file(self._backing_file) 287 288 289 @with_backing_file 290 def _synchronize_backing_file(self): 291 """Synchronizes the contents of the in-memory and on-disk state.""" 292 # state is implicitly synchronized in _with_backing_file methods 293 pass 294 295 296 def set_backing_file(self, file_path): 297 """Change the path used as the backing file for the persistent state. 298 299 When a new backing file is specified if a file already exists then 300 its contents will be added into the current state, with conflicts 301 between the file and memory being resolved in favor of the file 302 contents. The file will then be kept in sync with the (combined) 303 in-memory state. The syncing can be disabled by setting this to None. 304 305 @param file_path: A path on the filesystem that can be read from and 306 written to, or None to turn off the backing store. 307 """ 308 self._synchronize_backing_file() 309 self._backing_file = file_path 310 self._backing_file_initialized = False 311 self._synchronize_backing_file() 312 313 314 @with_backing_file 315 def get(self, namespace, name, default=NO_DEFAULT): 316 """Returns the value associated with a particular name. 317 318 @param namespace: The namespace that the property should be stored in. 319 @param name: The name the value was saved with. 320 @param default: A default value to return if no state is currently 321 associated with var. 322 323 @return: A deep copy of the value associated with name. Note that this 324 explicitly returns a deep copy to avoid problems with mutable 325 values; mutations are not persisted or shared. 326 @raise KeyError: raised when no state is associated with var and a 327 default value is not provided. 328 """ 329 if self.has(namespace, name): 330 return copy.deepcopy(self._state[namespace][name]) 331 elif default is self.NO_DEFAULT: 332 raise KeyError('No key %s in namespace %s' % (name, namespace)) 333 else: 334 return default 335 336 337 @with_backing_file 338 def set(self, namespace, name, value): 339 """Saves the value given with the provided name. 340 341 @param namespace: The namespace that the property should be stored in. 342 @param name: The name the value should be saved with. 343 @param value: The value to save. 344 """ 345 namespace_dict = self._state.setdefault(namespace, {}) 346 namespace_dict[name] = copy.deepcopy(value) 347 logging.debug('Persistent state %s.%s now set to %r', namespace, 348 name, value) 349 350 351 @with_backing_file 352 def has(self, namespace, name): 353 """Return a boolean indicating if namespace.name is defined. 354 355 @param namespace: The namespace to check for a definition. 356 @param name: The name to check for a definition. 357 358 @return: True if the given name is defined in the given namespace and 359 False otherwise. 360 """ 361 return namespace in self._state and name in self._state[namespace] 362 363 364 @with_backing_file 365 def discard(self, namespace, name): 366 """If namespace.name is a defined value, deletes it. 367 368 @param namespace: The namespace that the property is stored in. 369 @param name: The name the value is saved with. 370 """ 371 if self.has(namespace, name): 372 del self._state[namespace][name] 373 if len(self._state[namespace]) == 0: 374 del self._state[namespace] 375 logging.debug('Persistent state %s.%s deleted', namespace, name) 376 else: 377 logging.debug( 378 'Persistent state %s.%s not defined so nothing is discarded', 379 namespace, name) 380 381 382 @with_backing_file 383 def discard_namespace(self, namespace): 384 """Delete all defined namespace.* names. 385 386 @param namespace: The namespace to be cleared. 387 """ 388 if namespace in self._state: 389 del self._state[namespace] 390 logging.debug('Persistent state %s.* deleted', namespace) 391 392 393 @staticmethod 394 def property_factory(state_attribute, property_attribute, default, 395 namespace='global_properties'): 396 """ 397 Create a property object for an attribute using self.get and self.set. 398 399 @param state_attribute: A string with the name of the attribute on 400 job that contains the job_state instance. 401 @param property_attribute: A string with the name of the attribute 402 this property is exposed as. 403 @param default: A default value that should be used for this property 404 if it is not set. 405 @param namespace: The namespace to store the attribute value in. 406 407 @return: A read-write property object that performs self.get calls 408 to read the value and self.set calls to set it. 409 """ 410 def getter(job): 411 state = getattr(job, state_attribute) 412 return state.get(namespace, property_attribute, default) 413 def setter(job, value): 414 state = getattr(job, state_attribute) 415 state.set(namespace, property_attribute, value) 416 return property(getter, setter) 417 418 419class status_log_entry(object): 420 """Represents a single status log entry.""" 421 422 RENDERED_NONE_VALUE = '----' 423 TIMESTAMP_FIELD = 'timestamp' 424 LOCALTIME_FIELD = 'localtime' 425 426 # non-space whitespace is forbidden in any fields 427 BAD_CHAR_REGEX = re.compile(r'[\t\n\r\v\f]') 428 429 def __init__(self, status_code, subdir, operation, message, fields, 430 timestamp=None): 431 """Construct a status.log entry. 432 433 @param status_code: A message status code. Must match the codes 434 accepted by autotest_lib.common_lib.log.is_valid_status. 435 @param subdir: A valid job subdirectory, or None. 436 @param operation: Description of the operation, or None. 437 @param message: A printable string describing event to be recorded. 438 @param fields: A dictionary of arbitrary alphanumeric key=value pairs 439 to be included in the log, or None. 440 @param timestamp: An optional integer timestamp, in the same format 441 as a time.time() timestamp. If unspecified, the current time is 442 used. 443 444 @raise ValueError: if any of the parameters are invalid 445 """ 446 447 if not log.is_valid_status(status_code): 448 raise ValueError('status code %r is not valid' % status_code) 449 self.status_code = status_code 450 451 if subdir and self.BAD_CHAR_REGEX.search(subdir): 452 raise ValueError('Invalid character in subdir string') 453 self.subdir = subdir 454 455 if operation and self.BAD_CHAR_REGEX.search(operation): 456 raise ValueError('Invalid character in operation string') 457 self.operation = operation 458 459 # break the message line into a single-line message that goes into the 460 # database, and a block of additional lines that goes into the status 461 # log but will never be parsed 462 message_lines = message.split('\n') 463 self.message = message_lines[0].replace('\t', ' ' * 8) 464 self.extra_message_lines = message_lines[1:] 465 if self.BAD_CHAR_REGEX.search(self.message): 466 raise ValueError('Invalid character in message %r' % self.message) 467 468 if not fields: 469 self.fields = {} 470 else: 471 self.fields = fields.copy() 472 for key, value in self.fields.iteritems(): 473 if type(value) is int: 474 value = str(value) 475 if self.BAD_CHAR_REGEX.search(key + value): 476 raise ValueError('Invalid character in %r=%r field' 477 % (key, value)) 478 479 # build up the timestamp 480 if timestamp is None: 481 timestamp = int(time.time()) 482 self.fields[self.TIMESTAMP_FIELD] = str(timestamp) 483 self.fields[self.LOCALTIME_FIELD] = time.strftime( 484 '%b %d %H:%M:%S', time.localtime(timestamp)) 485 486 487 def is_start(self): 488 """Indicates if this status log is the start of a new nested block. 489 490 @return: A boolean indicating if this entry starts a new nested block. 491 """ 492 return self.status_code == 'START' 493 494 495 def is_end(self): 496 """Indicates if this status log is the end of a nested block. 497 498 @return: A boolean indicating if this entry ends a nested block. 499 """ 500 return self.status_code.startswith('END ') 501 502 503 def render(self): 504 """Render the status log entry into a text string. 505 506 @return: A text string suitable for writing into a status log file. 507 """ 508 # combine all the log line data into a tab-delimited string 509 subdir = self.subdir or self.RENDERED_NONE_VALUE 510 operation = self.operation or self.RENDERED_NONE_VALUE 511 extra_fields = ['%s=%s' % field for field in self.fields.iteritems()] 512 line_items = [self.status_code, subdir, operation] 513 line_items += extra_fields + [self.message] 514 first_line = '\t'.join(line_items) 515 516 # append the extra unparsable lines, two-space indented 517 all_lines = [first_line] 518 all_lines += [' ' + line for line in self.extra_message_lines] 519 return '\n'.join(all_lines) 520 521 522 @classmethod 523 def parse(cls, line): 524 """Parse a status log entry from a text string. 525 526 This method is the inverse of render; it should always be true that 527 parse(entry.render()) produces a new status_log_entry equivalent to 528 entry. 529 530 @return: A new status_log_entry instance with fields extracted from the 531 given status line. If the line is an extra message line then None 532 is returned. 533 """ 534 # extra message lines are always prepended with two spaces 535 if line.startswith(' '): 536 return None 537 538 line = line.lstrip('\t') # ignore indentation 539 entry_parts = line.split('\t') 540 if len(entry_parts) < 4: 541 raise ValueError('%r is not a valid status line' % line) 542 status_code, subdir, operation = entry_parts[:3] 543 if subdir == cls.RENDERED_NONE_VALUE: 544 subdir = None 545 if operation == cls.RENDERED_NONE_VALUE: 546 operation = None 547 message = entry_parts[-1] 548 fields = dict(part.split('=', 1) for part in entry_parts[3:-1]) 549 if cls.TIMESTAMP_FIELD in fields: 550 timestamp = int(fields[cls.TIMESTAMP_FIELD]) 551 else: 552 timestamp = None 553 return cls(status_code, subdir, operation, message, fields, timestamp) 554 555 556class status_indenter(object): 557 """Abstract interface that a status log indenter should use.""" 558 559 @property 560 def indent(self): 561 raise NotImplementedError 562 563 564 def increment(self): 565 """Increase indentation by one level.""" 566 raise NotImplementedError 567 568 569 def decrement(self): 570 """Decrease indentation by one level.""" 571 572 573class status_logger(object): 574 """Represents a status log file. Responsible for translating messages 575 into on-disk status log lines. 576 577 @property global_filename: The filename to write top-level logs to. 578 @property subdir_filename: The filename to write subdir-level logs to. 579 """ 580 def __init__(self, job, indenter, global_filename='status', 581 subdir_filename='status', record_hook=None, 582 tap_writer=None): 583 """Construct a logger instance. 584 585 @param job: A reference to the job object this is logging for. Only a 586 weak reference to the job is held, to avoid a 587 status_logger <-> job circular reference. 588 @param indenter: A status_indenter instance, for tracking the 589 indentation level. 590 @param global_filename: An optional filename to initialize the 591 self.global_filename attribute. 592 @param subdir_filename: An optional filename to initialize the 593 self.subdir_filename attribute. 594 @param record_hook: An optional function to be called before an entry 595 is logged. The function should expect a single parameter, a 596 copy of the status_log_entry object. 597 @param tap_writer: An instance of the class TAPReport for addionally 598 writing TAP files 599 """ 600 self._jobref = weakref.ref(job) 601 self._indenter = indenter 602 self.global_filename = global_filename 603 self.subdir_filename = subdir_filename 604 self._record_hook = record_hook 605 if tap_writer is None: 606 self._tap_writer = TAPReport(None) 607 else: 608 self._tap_writer = tap_writer 609 610 611 def render_entry(self, log_entry): 612 """Render a status_log_entry as it would be written to a log file. 613 614 @param log_entry: A status_log_entry instance to be rendered. 615 616 @return: The status log entry, rendered as it would be written to the 617 logs (including indentation). 618 """ 619 if log_entry.is_end(): 620 indent = self._indenter.indent - 1 621 else: 622 indent = self._indenter.indent 623 return '\t' * indent + log_entry.render().rstrip('\n') 624 625 626 def record_entry(self, log_entry, log_in_subdir=True): 627 """Record a status_log_entry into the appropriate status log files. 628 629 @param log_entry: A status_log_entry instance to be recorded into the 630 status logs. 631 @param log_in_subdir: A boolean that indicates (when true) that subdir 632 logs should be written into the subdirectory status log file. 633 """ 634 # acquire a strong reference for the duration of the method 635 job = self._jobref() 636 if job is None: 637 logging.warning('Something attempted to write a status log entry ' 638 'after its job terminated, ignoring the attempt.') 639 logging.warning(traceback.format_stack()) 640 return 641 642 # call the record hook if one was given 643 if self._record_hook: 644 self._record_hook(log_entry) 645 646 # figure out where we need to log to 647 log_files = [os.path.join(job.resultdir, self.global_filename)] 648 if log_in_subdir and log_entry.subdir: 649 log_files.append(os.path.join(job.resultdir, log_entry.subdir, 650 self.subdir_filename)) 651 652 # write out to entry to the log files 653 log_text = self.render_entry(log_entry) 654 for log_file in log_files: 655 fileobj = open(log_file, 'a') 656 try: 657 print >> fileobj, log_text 658 finally: 659 fileobj.close() 660 661 # write to TAPRecord instance 662 if log_entry.is_end() and self._tap_writer.do_tap_report: 663 self._tap_writer.record(log_entry, self._indenter.indent, log_files) 664 665 # adjust the indentation if this was a START or END entry 666 if log_entry.is_start(): 667 self._indenter.increment() 668 elif log_entry.is_end(): 669 self._indenter.decrement() 670 671 672class TAPReport(object): 673 """ 674 Deal with TAP reporting for the Autotest client. 675 """ 676 677 job_statuses = { 678 "TEST_NA": False, 679 "ABORT": False, 680 "ERROR": False, 681 "FAIL": False, 682 "WARN": False, 683 "GOOD": True, 684 "START": True, 685 "END GOOD": True, 686 "ALERT": False, 687 "RUNNING": False, 688 "NOSTATUS": False 689 } 690 691 692 def __init__(self, enable, resultdir=None, global_filename='status'): 693 """ 694 @param enable: Set self.do_tap_report to trigger TAP reporting. 695 @param resultdir: Path where the TAP report files will be written. 696 @param global_filename: File name of the status files .tap extensions 697 will be appended. 698 """ 699 self.do_tap_report = enable 700 if resultdir is not None: 701 self.resultdir = os.path.abspath(resultdir) 702 self._reports_container = {} 703 self._keyval_container = {} # {'path1': [entries],} 704 self.global_filename = global_filename 705 706 707 @classmethod 708 def tap_ok(self, success, counter, message): 709 """ 710 return a TAP message string. 711 712 @param success: True for positive message string. 713 @param counter: number of TAP line in plan. 714 @param message: additional message to report in TAP line. 715 """ 716 if success: 717 message = "ok %s - %s" % (counter, message) 718 else: 719 message = "not ok %s - %s" % (counter, message) 720 return message 721 722 723 def record(self, log_entry, indent, log_files): 724 """ 725 Append a job-level status event to self._reports_container. All 726 events will be written to TAP log files at the end of the test run. 727 Otherwise, it's impossilble to determine the TAP plan. 728 729 @param log_entry: A string status code describing the type of status 730 entry being recorded. It must pass log.is_valid_status to be 731 considered valid. 732 @param indent: Level of the log_entry to determine the operation if 733 log_entry.operation is not given. 734 @param log_files: List of full path of files the TAP report will be 735 written to at the end of the test. 736 """ 737 for log_file in log_files: 738 log_file_path = os.path.dirname(log_file) 739 key = log_file_path.split(self.resultdir, 1)[1].strip(os.sep) 740 if not key: 741 key = 'root' 742 743 if not self._reports_container.has_key(key): 744 self._reports_container[key] = [] 745 746 if log_entry.operation: 747 operation = log_entry.operation 748 elif indent == 1: 749 operation = "job" 750 else: 751 operation = "unknown" 752 entry = self.tap_ok( 753 self.job_statuses.get(log_entry.status_code, False), 754 len(self._reports_container[key]) + 1, operation + "\n" 755 ) 756 self._reports_container[key].append(entry) 757 758 759 def record_keyval(self, path, dictionary, type_tag=None): 760 """ 761 Append a key-value pairs of dictionary to self._keyval_container in 762 TAP format. Once finished write out the keyval.tap file to the file 763 system. 764 765 If type_tag is None, then the key must be composed of alphanumeric 766 characters (or dashes + underscores). However, if type-tag is not 767 null then the keys must also have "{type_tag}" as a suffix. At 768 the moment the only valid values of type_tag are "attr" and "perf". 769 770 @param path: The full path of the keyval.tap file to be created 771 @param dictionary: The keys and values. 772 @param type_tag: The type of the values 773 """ 774 self._keyval_container.setdefault(path, [0, []]) 775 self._keyval_container[path][0] += 1 776 777 if type_tag is None: 778 key_regex = re.compile(r'^[-\.\w]+$') 779 else: 780 if type_tag not in ('attr', 'perf'): 781 raise ValueError('Invalid type tag: %s' % type_tag) 782 escaped_tag = re.escape(type_tag) 783 key_regex = re.compile(r'^[-\.\w]+\{%s\}$' % escaped_tag) 784 self._keyval_container[path][1].extend([ 785 self.tap_ok(True, self._keyval_container[path][0], "results"), 786 "\n ---\n", 787 ]) 788 try: 789 for key in sorted(dictionary.keys()): 790 if not key_regex.search(key): 791 raise ValueError('Invalid key: %s' % key) 792 self._keyval_container[path][1].append( 793 ' %s: %s\n' % (key.replace('{', '_').rstrip('}'), 794 dictionary[key]) 795 ) 796 finally: 797 self._keyval_container[path][1].append(" ...\n") 798 self._write_keyval() 799 800 801 def _write_reports(self): 802 """ 803 Write TAP reports to file. 804 """ 805 for key in self._reports_container.keys(): 806 if key == 'root': 807 sub_dir = '' 808 else: 809 sub_dir = key 810 tap_fh = open(os.sep.join( 811 [self.resultdir, sub_dir, self.global_filename] 812 ) + ".tap", 'w') 813 tap_fh.write('1..' + str(len(self._reports_container[key])) + '\n') 814 tap_fh.writelines(self._reports_container[key]) 815 tap_fh.close() 816 817 818 def _write_keyval(self): 819 """ 820 Write the self._keyval_container key values to a file. 821 """ 822 for path in self._keyval_container.keys(): 823 tap_fh = open(path + ".tap", 'w') 824 tap_fh.write('1..' + str(self._keyval_container[path][0]) + '\n') 825 tap_fh.writelines(self._keyval_container[path][1]) 826 tap_fh.close() 827 828 829 def write(self): 830 """ 831 Write the TAP reports to files. 832 """ 833 self._write_reports() 834 835 836 def _write_tap_archive(self): 837 """ 838 Write a tar archive containing all the TAP files and 839 a meta.yml containing the file names. 840 """ 841 os.chdir(self.resultdir) 842 tap_files = [] 843 for rel_path, d, files in os.walk('.'): 844 tap_files.extend(["/".join( 845 [rel_path, f]) for f in files if f.endswith('.tap')]) 846 meta_yaml = open('meta.yml', 'w') 847 meta_yaml.write('file_order:\n') 848 tap_tar = tarfile.open(self.resultdir + '/tap.tar.gz', 'w:gz') 849 for f in tap_files: 850 meta_yaml.write(" - " + f.lstrip('./') + "\n") 851 tap_tar.add(f) 852 meta_yaml.close() 853 tap_tar.add('meta.yml') 854 tap_tar.close() 855 856 857class base_job(object): 858 """An abstract base class for the various autotest job classes. 859 860 @property autodir: The top level autotest directory. 861 @property clientdir: The autotest client directory. 862 @property serverdir: The autotest server directory. [OPTIONAL] 863 @property resultdir: The directory where results should be written out. 864 [WRITABLE] 865 866 @property pkgdir: The job packages directory. [WRITABLE] 867 @property tmpdir: The job temporary directory. [WRITABLE] 868 @property testdir: The job test directory. [WRITABLE] 869 @property site_testdir: The job site test directory. [WRITABLE] 870 871 @property bindir: The client bin/ directory. 872 @property profdir: The client profilers/ directory. 873 @property toolsdir: The client tools/ directory. 874 875 @property control: A path to the control file to be executed. [OPTIONAL] 876 @property hosts: A set of all live Host objects currently in use by the 877 job. Code running in the context of a local client can safely assume 878 that this set contains only a single entry. 879 @property machines: A list of the machine names associated with the job. 880 @property user: The user executing the job. 881 @property tag: A tag identifying the job. Often used by the scheduler to 882 give a name of the form NUMBER-USERNAME/HOSTNAME. 883 @property test_retry: The number of times to retry a test if the test did 884 not complete successfully. 885 @property args: A list of addtional miscellaneous command-line arguments 886 provided when starting the job. 887 888 @property last_boot_tag: The label of the kernel from the last reboot. 889 [OPTIONAL,PERSISTENT] 890 @property automatic_test_tag: A string which, if set, will be automatically 891 added to the test name when running tests. 892 893 @property default_profile_only: A boolean indicating the default value of 894 profile_only used by test.execute. [PERSISTENT] 895 @property drop_caches: A boolean indicating if caches should be dropped 896 before each test is executed. 897 @property drop_caches_between_iterations: A boolean indicating if caches 898 should be dropped before each test iteration is executed. 899 @property run_test_cleanup: A boolean indicating if test.cleanup should be 900 run by default after a test completes, if the run_cleanup argument is 901 not specified. [PERSISTENT] 902 903 @property num_tests_run: The number of tests run during the job. [OPTIONAL] 904 @property num_tests_failed: The number of tests failed during the job. 905 [OPTIONAL] 906 907 @property bootloader: An instance of the boottool class. May not be 908 available on job instances where access to the bootloader is not 909 available (e.g. on the server running a server job). [OPTIONAL] 910 @property harness: An instance of the client test harness. Only available 911 in contexts where client test execution happens. [OPTIONAL] 912 @property logging: An instance of the logging manager associated with the 913 job. 914 @property profilers: An instance of the profiler manager associated with 915 the job. 916 @property sysinfo: An instance of the sysinfo object. Only available in 917 contexts where it's possible to collect sysinfo. 918 @property warning_manager: A class for managing which types of WARN 919 messages should be logged and which should be supressed. [OPTIONAL] 920 @property warning_loggers: A set of readable streams that will be monitored 921 for WARN messages to be logged. [OPTIONAL] 922 923 Abstract methods: 924 _find_base_directories [CLASSMETHOD] 925 Returns the location of autodir, clientdir and serverdir 926 927 _find_resultdir 928 Returns the location of resultdir. Gets a copy of any parameters 929 passed into base_job.__init__. Can return None to indicate that 930 no resultdir is to be used. 931 932 _get_status_logger 933 Returns a status_logger instance for recording job status logs. 934 """ 935 936 # capture the dependency on several helper classes with factories 937 _job_directory = job_directory 938 _job_state = job_state 939 940 941 # all the job directory attributes 942 autodir = _job_directory.property_factory('autodir') 943 clientdir = _job_directory.property_factory('clientdir') 944 serverdir = _job_directory.property_factory('serverdir') 945 resultdir = _job_directory.property_factory('resultdir') 946 pkgdir = _job_directory.property_factory('pkgdir') 947 tmpdir = _job_directory.property_factory('tmpdir') 948 testdir = _job_directory.property_factory('testdir') 949 site_testdir = _job_directory.property_factory('site_testdir') 950 bindir = _job_directory.property_factory('bindir') 951 profdir = _job_directory.property_factory('profdir') 952 toolsdir = _job_directory.property_factory('toolsdir') 953 954 955 # all the generic persistent properties 956 tag = _job_state.property_factory('_state', 'tag', '') 957 test_retry = _job_state.property_factory('_state', 'test_retry', 0) 958 default_profile_only = _job_state.property_factory( 959 '_state', 'default_profile_only', False) 960 run_test_cleanup = _job_state.property_factory( 961 '_state', 'run_test_cleanup', True) 962 last_boot_tag = _job_state.property_factory( 963 '_state', 'last_boot_tag', None) 964 automatic_test_tag = _job_state.property_factory( 965 '_state', 'automatic_test_tag', None) 966 967 # the use_sequence_number property 968 _sequence_number = _job_state.property_factory( 969 '_state', '_sequence_number', None) 970 def _get_use_sequence_number(self): 971 return bool(self._sequence_number) 972 def _set_use_sequence_number(self, value): 973 if value: 974 self._sequence_number = 1 975 else: 976 self._sequence_number = None 977 use_sequence_number = property(_get_use_sequence_number, 978 _set_use_sequence_number) 979 980 # parent job id is passed in from autoserv command line. It's only used in 981 # server job. The property is added here for unittest 982 # (base_job_unittest.py) to be consistent on validating public properties of 983 # a base_job object. 984 parent_job_id = None 985 986 def __init__(self, *args, **dargs): 987 # initialize the base directories, all others are relative to these 988 autodir, clientdir, serverdir = self._find_base_directories() 989 self._autodir = self._job_directory(autodir) 990 self._clientdir = self._job_directory(clientdir) 991 # TODO(scottz): crosbug.com/38259, needed to pass unittests for now. 992 self.label = None 993 if serverdir: 994 self._serverdir = self._job_directory(serverdir) 995 else: 996 self._serverdir = None 997 998 # initialize all the other directories relative to the base ones 999 self._initialize_dir_properties() 1000 self._resultdir = self._job_directory( 1001 self._find_resultdir(*args, **dargs), True) 1002 self._execution_contexts = [] 1003 1004 # initialize all the job state 1005 self._state = self._job_state() 1006 1007 # initialize tap reporting 1008 if dargs.has_key('options'): 1009 self._tap = self._tap_init(dargs['options'].tap_report) 1010 else: 1011 self._tap = self._tap_init(False) 1012 1013 @classmethod 1014 def _find_base_directories(cls): 1015 raise NotImplementedError() 1016 1017 1018 def _initialize_dir_properties(self): 1019 """ 1020 Initializes all the secondary self.*dir properties. Requires autodir, 1021 clientdir and serverdir to already be initialized. 1022 """ 1023 # create some stubs for use as shortcuts 1024 def readonly_dir(*args): 1025 return self._job_directory(os.path.join(*args)) 1026 def readwrite_dir(*args): 1027 return self._job_directory(os.path.join(*args), True) 1028 1029 # various client-specific directories 1030 self._bindir = readonly_dir(self.clientdir, 'bin') 1031 self._profdir = readonly_dir(self.clientdir, 'profilers') 1032 self._pkgdir = readwrite_dir(self.clientdir, 'packages') 1033 self._toolsdir = readonly_dir(self.clientdir, 'tools') 1034 1035 # directories which are in serverdir on a server, clientdir on a client 1036 # tmp tests, and site_tests need to be read_write for client, but only 1037 # read for server. 1038 if self.serverdir: 1039 root = self.serverdir 1040 r_or_rw_dir = readonly_dir 1041 else: 1042 root = self.clientdir 1043 r_or_rw_dir = readwrite_dir 1044 self._testdir = r_or_rw_dir(root, 'tests') 1045 self._site_testdir = r_or_rw_dir(root, 'site_tests') 1046 1047 # various server-specific directories 1048 if self.serverdir: 1049 self._tmpdir = readwrite_dir(tempfile.gettempdir()) 1050 else: 1051 self._tmpdir = readwrite_dir(root, 'tmp') 1052 1053 1054 def _find_resultdir(self, *args, **dargs): 1055 raise NotImplementedError() 1056 1057 1058 def push_execution_context(self, resultdir): 1059 """ 1060 Save off the current context of the job and change to the given one. 1061 1062 In practice method just changes the resultdir, but it may become more 1063 extensive in the future. The expected use case is for when a child 1064 job needs to be executed in some sort of nested context (for example 1065 the way parallel_simple does). The original context can be restored 1066 with a pop_execution_context call. 1067 1068 @param resultdir: The new resultdir, relative to the current one. 1069 """ 1070 new_dir = self._job_directory( 1071 os.path.join(self.resultdir, resultdir), True) 1072 self._execution_contexts.append(self._resultdir) 1073 self._resultdir = new_dir 1074 1075 1076 def pop_execution_context(self): 1077 """ 1078 Reverse the effects of the previous push_execution_context call. 1079 1080 @raise IndexError: raised when the stack of contexts is empty. 1081 """ 1082 if not self._execution_contexts: 1083 raise IndexError('No old execution context to restore') 1084 self._resultdir = self._execution_contexts.pop() 1085 1086 1087 def get_state(self, name, default=_job_state.NO_DEFAULT): 1088 """Returns the value associated with a particular name. 1089 1090 @param name: The name the value was saved with. 1091 @param default: A default value to return if no state is currently 1092 associated with var. 1093 1094 @return: A deep copy of the value associated with name. Note that this 1095 explicitly returns a deep copy to avoid problems with mutable 1096 values; mutations are not persisted or shared. 1097 @raise KeyError: raised when no state is associated with var and a 1098 default value is not provided. 1099 """ 1100 try: 1101 return self._state.get('public', name, default=default) 1102 except KeyError: 1103 raise KeyError(name) 1104 1105 1106 def set_state(self, name, value): 1107 """Saves the value given with the provided name. 1108 1109 @param name: The name the value should be saved with. 1110 @param value: The value to save. 1111 """ 1112 self._state.set('public', name, value) 1113 1114 1115 def _build_tagged_test_name(self, testname, dargs): 1116 """Builds the fully tagged testname and subdirectory for job.run_test. 1117 1118 @param testname: The base name of the test 1119 @param dargs: The ** arguments passed to run_test. And arguments 1120 consumed by this method will be removed from the dictionary. 1121 1122 @return: A 3-tuple of the full name of the test, the subdirectory it 1123 should be stored in, and the full tag of the subdir. 1124 """ 1125 tag_parts = [] 1126 1127 # build up the parts of the tag used for the test name 1128 master_testpath = dargs.get('master_testpath', "") 1129 base_tag = dargs.pop('tag', None) 1130 if base_tag: 1131 tag_parts.append(str(base_tag)) 1132 if self.use_sequence_number: 1133 tag_parts.append('_%02d_' % self._sequence_number) 1134 self._sequence_number += 1 1135 if self.automatic_test_tag: 1136 tag_parts.append(self.automatic_test_tag) 1137 full_testname = '.'.join([testname] + tag_parts) 1138 1139 # build up the subdir and tag as well 1140 subdir_tag = dargs.pop('subdir_tag', None) 1141 if subdir_tag: 1142 tag_parts.append(subdir_tag) 1143 subdir = '.'.join([testname] + tag_parts) 1144 subdir = os.path.join(master_testpath, subdir) 1145 tag = '.'.join(tag_parts) 1146 1147 return full_testname, subdir, tag 1148 1149 1150 def _make_test_outputdir(self, subdir): 1151 """Creates an output directory for a test to run it. 1152 1153 @param subdir: The subdirectory of the test. Generally computed by 1154 _build_tagged_test_name. 1155 1156 @return: A job_directory instance corresponding to the outputdir of 1157 the test. 1158 @raise TestError: If the output directory is invalid. 1159 """ 1160 # explicitly check that this subdirectory is new 1161 path = os.path.join(self.resultdir, subdir) 1162 if os.path.exists(path): 1163 msg = ('%s already exists; multiple tests cannot run with the ' 1164 'same subdirectory' % subdir) 1165 raise error.TestError(msg) 1166 1167 # create the outputdir and raise a TestError if it isn't valid 1168 try: 1169 outputdir = self._job_directory(path, True) 1170 return outputdir 1171 except self._job_directory.JobDirectoryException, e: 1172 logging.exception('%s directory creation failed with %s', 1173 subdir, e) 1174 raise error.TestError('%s directory creation failed' % subdir) 1175 1176 def _tap_init(self, enable): 1177 """Initialize TAP reporting 1178 """ 1179 return TAPReport(enable, resultdir=self.resultdir) 1180 1181 1182 def record(self, status_code, subdir, operation, status='', 1183 optional_fields=None): 1184 """Record a job-level status event. 1185 1186 Logs an event noteworthy to the Autotest job as a whole. Messages will 1187 be written into a global status log file, as well as a subdir-local 1188 status log file (if subdir is specified). 1189 1190 @param status_code: A string status code describing the type of status 1191 entry being recorded. It must pass log.is_valid_status to be 1192 considered valid. 1193 @param subdir: A specific results subdirectory this also applies to, or 1194 None. If not None the subdirectory must exist. 1195 @param operation: A string describing the operation that was run. 1196 @param status: An optional human-readable message describing the status 1197 entry, for example an error message or "completed successfully". 1198 @param optional_fields: An optional dictionary of addtional named fields 1199 to be included with the status message. Every time timestamp and 1200 localtime entries are generated with the current time and added 1201 to this dictionary. 1202 """ 1203 entry = status_log_entry(status_code, subdir, operation, status, 1204 optional_fields) 1205 self.record_entry(entry) 1206 1207 1208 def record_entry(self, entry, log_in_subdir=True): 1209 """Record a job-level status event, using a status_log_entry. 1210 1211 This is the same as self.record but using an existing status log 1212 entry object rather than constructing one for you. 1213 1214 @param entry: A status_log_entry object 1215 @param log_in_subdir: A boolean that indicates (when true) that subdir 1216 logs should be written into the subdirectory status log file. 1217 """ 1218 self._get_status_logger().record_entry(entry, log_in_subdir) 1219