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