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