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