| """The main job wrapper |
| |
| This is the core infrastructure. |
| |
| Copyright Andy Whitcroft, Martin J. Bligh 2006 |
| """ |
| |
| # pylint: disable=missing-docstring |
| |
| import copy |
| from datetime import datetime |
| import getpass |
| import glob |
| import logging |
| import os |
| import re |
| import shutil |
| import sys |
| import time |
| import traceback |
| import types |
| import weakref |
| |
| import common |
| from autotest_lib.client.bin import client_logging_config |
| from autotest_lib.client.bin import harness |
| from autotest_lib.client.bin import local_host |
| from autotest_lib.client.bin import parallel |
| from autotest_lib.client.bin import partition as partition_lib |
| from autotest_lib.client.bin import profilers |
| from autotest_lib.client.bin import sysinfo |
| from autotest_lib.client.bin import test |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import barrier |
| from autotest_lib.client.common_lib import base_job |
| from autotest_lib.client.common_lib import control_data |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import global_config |
| from autotest_lib.client.common_lib import logging_manager |
| from autotest_lib.client.common_lib import packages |
| from autotest_lib.client.cros import cros_logging |
| from autotest_lib.client.tools import html_report |
| |
| GLOBAL_CONFIG = global_config.global_config |
| |
| LAST_BOOT_TAG = object() |
| JOB_PREAMBLE = """ |
| from autotest_lib.client.common_lib.error import * |
| from autotest_lib.client.bin.utils import * |
| """ |
| |
| |
| class StepError(error.AutotestError): |
| pass |
| |
| class NotAvailableError(error.AutotestError): |
| pass |
| |
| |
| |
| def _run_test_complete_on_exit(f): |
| """Decorator for job methods that automatically calls |
| self.harness.run_test_complete when the method exits, if appropriate.""" |
| def wrapped(self, *args, **dargs): |
| try: |
| return f(self, *args, **dargs) |
| finally: |
| if self._logger.global_filename == 'status': |
| self.harness.run_test_complete() |
| if self.drop_caches: |
| utils.drop_caches() |
| wrapped.__name__ = f.__name__ |
| wrapped.__doc__ = f.__doc__ |
| wrapped.__dict__.update(f.__dict__) |
| return wrapped |
| |
| |
| class status_indenter(base_job.status_indenter): |
| """Provide a status indenter that is backed by job._record_prefix.""" |
| def __init__(self, job_): |
| self._job = weakref.proxy(job_) # avoid a circular reference |
| |
| |
| @property |
| def indent(self): |
| return self._job._record_indent |
| |
| |
| def increment(self): |
| self._job._record_indent += 1 |
| |
| |
| def decrement(self): |
| self._job._record_indent -= 1 |
| |
| |
| class base_client_job(base_job.base_job): |
| """The client-side concrete implementation of base_job. |
| |
| Optional properties provided by this implementation: |
| control |
| harness |
| """ |
| |
| _WARNING_DISABLE_DELAY = 5 |
| |
| # _record_indent is a persistent property, but only on the client |
| _job_state = base_job.base_job._job_state |
| _record_indent = _job_state.property_factory( |
| '_state', '_record_indent', 0, namespace='client') |
| _max_disk_usage_rate = _job_state.property_factory( |
| '_state', '_max_disk_usage_rate', 0.0, namespace='client') |
| |
| |
| def __init__(self, control, options, drop_caches=True): |
| """ |
| Prepare a client side job object. |
| |
| @param control: The control file (pathname of). |
| @param options: an object which includes: |
| jobtag: The job tag string (eg "default"). |
| cont: If this is the continuation of this job. |
| harness_type: An alternative server harness. [None] |
| use_external_logging: If true, the enable_external_logging |
| method will be called during construction. [False] |
| @param drop_caches: If true, utils.drop_caches() is called before and |
| between all tests. [True] |
| """ |
| super(base_client_job, self).__init__(options=options) |
| self._pre_record_init(control, options) |
| try: |
| self._post_record_init(control, options, drop_caches) |
| except Exception, err: |
| self.record( |
| 'ABORT', None, None,'client.bin.job.__init__ failed: %s' % |
| str(err)) |
| raise |
| |
| |
| @classmethod |
| def _get_environ_autodir(cls): |
| return os.environ['AUTODIR'] |
| |
| |
| @classmethod |
| def _find_base_directories(cls): |
| """ |
| Determine locations of autodir and clientdir (which are the same) |
| using os.environ. Serverdir does not exist in this context. |
| """ |
| autodir = clientdir = cls._get_environ_autodir() |
| return autodir, clientdir, None |
| |
| |
| @classmethod |
| def _parse_args(cls, args): |
| return re.findall("[^\s]*?['|\"].*?['|\"]|[^\s]+", args) |
| |
| |
| def _find_resultdir(self, options): |
| """ |
| Determine the directory for storing results. On a client this is |
| always <autodir>/results/<tag>, where tag is passed in on the command |
| line as an option. |
| """ |
| output_dir_config = GLOBAL_CONFIG.get_config_value('CLIENT', |
| 'output_dir', |
| default="") |
| if options.output_dir: |
| basedir = options.output_dir |
| elif output_dir_config: |
| basedir = output_dir_config |
| else: |
| basedir = self.autodir |
| |
| return os.path.join(basedir, 'results', options.tag) |
| |
| |
| def _get_status_logger(self): |
| """Return a reference to the status logger.""" |
| return self._logger |
| |
| |
| def _pre_record_init(self, control, options): |
| """ |
| Initialization function that should peform ONLY the required |
| setup so that the self.record() method works. |
| |
| As of now self.record() needs self.resultdir, self._group_level, |
| self.harness and of course self._logger. |
| """ |
| if not options.cont: |
| self._cleanup_debugdir_files() |
| self._cleanup_results_dir() |
| |
| logging_manager.configure_logging( |
| client_logging_config.ClientLoggingConfig(), |
| results_dir=self.resultdir, |
| verbose=options.verbose) |
| logging.info('Writing results to %s', self.resultdir) |
| |
| # init_group_level needs the state |
| self.control = os.path.realpath(control) |
| self._is_continuation = options.cont |
| self._current_step_ancestry = [] |
| self._next_step_index = 0 |
| self._load_state() |
| |
| _harness = self.handle_persistent_option(options, 'harness') |
| _harness_args = self.handle_persistent_option(options, 'harness_args') |
| |
| self.harness = harness.select(_harness, self, _harness_args) |
| |
| if self.control: |
| parsed_control = control_data.parse_control( |
| self.control, raise_warnings=False) |
| self.fast = parsed_control.fast |
| |
| # set up the status logger |
| def client_job_record_hook(entry): |
| msg_tag = '' |
| if '.' in self._logger.global_filename: |
| msg_tag = self._logger.global_filename.split('.', 1)[1] |
| # send the entry to the job harness |
| message = '\n'.join([entry.message] + entry.extra_message_lines) |
| rendered_entry = self._logger.render_entry(entry) |
| self.harness.test_status_detail(entry.status_code, entry.subdir, |
| entry.operation, message, msg_tag, |
| entry.fields) |
| self.harness.test_status(rendered_entry, msg_tag) |
| # send the entry to stdout, if it's enabled |
| logging.info(rendered_entry) |
| self._logger = base_job.status_logger( |
| self, status_indenter(self), record_hook=client_job_record_hook) |
| |
| |
| def _post_record_init(self, control, options, drop_caches): |
| """ |
| Perform job initialization not required by self.record(). |
| """ |
| self._init_drop_caches(drop_caches) |
| |
| self._init_packages() |
| |
| self.sysinfo = sysinfo.sysinfo(self.resultdir) |
| self._load_sysinfo_state() |
| |
| if not options.cont: |
| download = os.path.join(self.testdir, 'download') |
| if not os.path.exists(download): |
| os.mkdir(download) |
| |
| shutil.copyfile(self.control, |
| os.path.join(self.resultdir, 'control')) |
| |
| self.control = control |
| |
| self.logging = logging_manager.get_logging_manager( |
| manage_stdout_and_stderr=True, redirect_fds=True) |
| self.logging.start_logging() |
| |
| self.profilers = profilers.profilers(self) |
| |
| self.machines = [options.hostname] |
| self.machine_dict_list = [{'hostname' : options.hostname}] |
| # Client side tests should always run the same whether or not they are |
| # running in the lab. |
| self.in_lab = False |
| self.hosts = set([local_host.LocalHost(hostname=options.hostname)]) |
| |
| self.args = [] |
| if options.args: |
| self.args = self._parse_args(options.args) |
| |
| if options.user: |
| self.user = options.user |
| else: |
| self.user = getpass.getuser() |
| |
| self.sysinfo.log_per_reboot_data() |
| |
| if not options.cont: |
| self.record('START', None, None) |
| |
| self.harness.run_start() |
| |
| if options.log: |
| self.enable_external_logging() |
| |
| self.num_tests_run = None |
| self.num_tests_failed = None |
| |
| self.warning_loggers = None |
| self.warning_manager = None |
| |
| |
| def _init_drop_caches(self, drop_caches): |
| """ |
| Perform the drop caches initialization. |
| """ |
| self.drop_caches_between_iterations = ( |
| GLOBAL_CONFIG.get_config_value('CLIENT', |
| 'drop_caches_between_iterations', |
| type=bool, default=True)) |
| self.drop_caches = drop_caches |
| if self.drop_caches: |
| utils.drop_caches() |
| |
| |
| def _init_packages(self): |
| """ |
| Perform the packages support initialization. |
| """ |
| self.pkgmgr = packages.PackageManager( |
| self.autodir, run_function_dargs={'timeout':3600}) |
| |
| |
| def _cleanup_results_dir(self): |
| """Delete everything in resultsdir""" |
| assert os.path.exists(self.resultdir) |
| list_files = glob.glob('%s/*' % self.resultdir) |
| for f in list_files: |
| if os.path.isdir(f): |
| shutil.rmtree(f) |
| elif os.path.isfile(f): |
| os.remove(f) |
| |
| |
| def _cleanup_debugdir_files(self): |
| """ |
| Delete any leftover debugdir files |
| """ |
| list_files = glob.glob("/tmp/autotest_results_dir.*") |
| for f in list_files: |
| os.remove(f) |
| |
| |
| def disable_warnings(self, warning_type): |
| self.record("INFO", None, None, |
| "disabling %s warnings" % warning_type, |
| {"warnings.disable": warning_type}) |
| time.sleep(self._WARNING_DISABLE_DELAY) |
| |
| |
| def enable_warnings(self, warning_type): |
| time.sleep(self._WARNING_DISABLE_DELAY) |
| self.record("INFO", None, None, |
| "enabling %s warnings" % warning_type, |
| {"warnings.enable": warning_type}) |
| |
| |
| def monitor_disk_usage(self, max_rate): |
| """\ |
| Signal that the job should monitor disk space usage on / |
| and generate a warning if a test uses up disk space at a |
| rate exceeding 'max_rate'. |
| |
| Parameters: |
| max_rate - the maximium allowed rate of disk consumption |
| during a test, in MB/hour, or 0 to indicate |
| no limit. |
| """ |
| self._max_disk_usage_rate = max_rate |
| |
| |
| def control_get(self): |
| return self.control |
| |
| |
| def control_set(self, control): |
| self.control = os.path.abspath(control) |
| |
| |
| def harness_select(self, which, harness_args): |
| self.harness = harness.select(which, self, harness_args) |
| |
| |
| def setup_dirs(self, results_dir, tmp_dir): |
| if not tmp_dir: |
| tmp_dir = os.path.join(self.tmpdir, 'build') |
| if not os.path.exists(tmp_dir): |
| os.mkdir(tmp_dir) |
| if not os.path.isdir(tmp_dir): |
| e_msg = "Temp dir (%s) is not a dir - args backwards?" % self.tmpdir |
| raise ValueError(e_msg) |
| |
| # We label the first build "build" and then subsequent ones |
| # as "build.2", "build.3", etc. Whilst this is a little bit |
| # inconsistent, 99.9% of jobs will only have one build |
| # (that's not done as kernbench, sparse, or buildtest), |
| # so it works out much cleaner. One of life's compromises. |
| if not results_dir: |
| results_dir = os.path.join(self.resultdir, 'build') |
| i = 2 |
| while os.path.exists(results_dir): |
| results_dir = os.path.join(self.resultdir, 'build.%d' % i) |
| i += 1 |
| if not os.path.exists(results_dir): |
| os.mkdir(results_dir) |
| |
| return (results_dir, tmp_dir) |
| |
| |
| def barrier(self, *args, **kwds): |
| """Create a barrier object""" |
| return barrier.barrier(*args, **kwds) |
| |
| |
| def install_pkg(self, name, pkg_type, install_dir): |
| ''' |
| This method is a simple wrapper around the actual package |
| installation method in the Packager class. This is used |
| internally by the profilers, deps and tests code. |
| name : name of the package (ex: sleeptest, dbench etc.) |
| pkg_type : Type of the package (ex: test, dep etc.) |
| install_dir : The directory in which the source is actually |
| untarred into. (ex: client/profilers/<name> for profilers) |
| ''' |
| if self.pkgmgr.repositories: |
| self.pkgmgr.install_pkg(name, pkg_type, self.pkgdir, install_dir) |
| |
| |
| def add_repository(self, repo_urls): |
| ''' |
| Adds the repository locations to the job so that packages |
| can be fetched from them when needed. The repository list |
| needs to be a string list |
| Ex: job.add_repository(['http://blah1','http://blah2']) |
| ''' |
| for repo_url in repo_urls: |
| self.pkgmgr.add_repository(repo_url) |
| |
| # Fetch the packages' checksum file that contains the checksums |
| # of all the packages if it is not already fetched. The checksum |
| # is always fetched whenever a job is first started. This |
| # is not done in the job's constructor as we don't have the list of |
| # the repositories there (and obviously don't care about this file |
| # if we are not using the repos) |
| try: |
| checksum_file_path = os.path.join(self.pkgmgr.pkgmgr_dir, |
| packages.CHECKSUM_FILE) |
| self.pkgmgr.fetch_pkg(packages.CHECKSUM_FILE, |
| checksum_file_path, use_checksum=False) |
| except error.PackageFetchError: |
| # packaging system might not be working in this case |
| # Silently fall back to the normal case |
| pass |
| |
| |
| def require_gcc(self): |
| """ |
| Test whether gcc is installed on the machine. |
| """ |
| # check if gcc is installed on the system. |
| try: |
| utils.system('which gcc') |
| except error.CmdError: |
| raise NotAvailableError('gcc is required by this job and is ' |
| 'not available on the system') |
| |
| |
| def setup_dep(self, deps): |
| """Set up the dependencies for this test. |
| deps is a list of libraries required for this test. |
| """ |
| # Fetch the deps from the repositories and set them up. |
| for dep in deps: |
| dep_dir = os.path.join(self.autodir, 'deps', dep) |
| # Search for the dependency in the repositories if specified, |
| # else check locally. |
| try: |
| self.install_pkg(dep, 'dep', dep_dir) |
| except error.PackageInstallError: |
| # see if the dep is there locally |
| pass |
| |
| # dep_dir might not exist if it is not fetched from the repos |
| if not os.path.exists(dep_dir): |
| raise error.TestError("Dependency %s does not exist" % dep) |
| |
| os.chdir(dep_dir) |
| if execfile('%s.py' % dep, {}) is None: |
| logging.info('Dependency %s successfuly built', dep) |
| |
| |
| def _runtest(self, url, tag, timeout, args, dargs): |
| try: |
| l = lambda : test.runtest(self, url, tag, args, dargs) |
| pid = parallel.fork_start(self.resultdir, l) |
| |
| self._forkwait(pid, timeout) |
| |
| except error.TestBaseException: |
| # These are already classified with an error type (exit_status) |
| raise |
| except error.JobError: |
| raise # Caught further up and turned into an ABORT. |
| except Exception, e: |
| # Converts all other exceptions thrown by the test regardless |
| # of phase into a TestError(TestBaseException) subclass that |
| # reports them with their full stack trace. |
| raise error.UnhandledTestError(e) |
| |
| def _forkwait(self, pid, timeout=None): |
| """Wait for the given pid to complete |
| |
| @param pid (int) process id to wait for |
| @param timeout (int) seconds to wait before timing out the process""" |
| if timeout: |
| logging.debug('Waiting for pid %d for %d seconds', pid, timeout) |
| parallel.fork_waitfor_timed(self.resultdir, pid, timeout) |
| else: |
| logging.debug('Waiting for pid %d', pid) |
| parallel.fork_waitfor(self.resultdir, pid) |
| logging.info('pid %d completed', pid) |
| |
| |
| def _run_test_base(self, url, *args, **dargs): |
| """ |
| Prepares arguments and run functions to run_test and run_test_detail. |
| |
| @param url A url that identifies the test to run. |
| @param tag An optional keyword argument that will be added to the |
| test and subdir name. |
| @param subdir_tag An optional keyword argument that will be added |
| to the subdir name. |
| |
| @returns: |
| subdir: Test subdirectory |
| testname: Test name |
| group_func: Actual test run function |
| timeout: Test timeout |
| """ |
| _group, testname = self.pkgmgr.get_package_name(url, 'test') |
| testname, subdir, tag = self._build_tagged_test_name(testname, dargs) |
| self._make_test_outputdir(subdir) |
| |
| timeout = dargs.pop('timeout', None) |
| if timeout: |
| logging.debug('Test has timeout: %d sec.', timeout) |
| |
| def log_warning(reason): |
| self.record("WARN", subdir, testname, reason) |
| @disk_usage_monitor.watch(log_warning, "/", self._max_disk_usage_rate) |
| def group_func(): |
| try: |
| self._runtest(url, tag, timeout, args, dargs) |
| except error.TestBaseException, detail: |
| # The error is already classified, record it properly. |
| self.record(detail.exit_status, subdir, testname, str(detail)) |
| raise |
| else: |
| self.record('GOOD', subdir, testname, 'completed successfully') |
| |
| return (subdir, testname, group_func, timeout) |
| |
| |
| @_run_test_complete_on_exit |
| def run_test(self, url, *args, **dargs): |
| """ |
| Summon a test object and run it. |
| |
| @param url A url that identifies the test to run. |
| @param tag An optional keyword argument that will be added to the |
| test and subdir name. |
| @param subdir_tag An optional keyword argument that will be added |
| to the subdir name. |
| |
| @returns True if the test passes, False otherwise. |
| """ |
| (subdir, testname, group_func, timeout) = self._run_test_base(url, |
| *args, |
| **dargs) |
| try: |
| self._rungroup(subdir, testname, group_func, timeout) |
| return True |
| except error.TestBaseException: |
| return False |
| # Any other exception here will be given to the caller |
| # |
| # NOTE: The only exception possible from the control file here |
| # is error.JobError as _runtest() turns all others into an |
| # UnhandledTestError that is caught above. |
| |
| |
| def stage_control_file(self, url): |
| """ |
| Install the test package and return the control file path. |
| |
| @param url The name of the test, e.g. dummy_Pass. This is the |
| string passed to run_test in the client test control file: |
| job.run_test('dummy_Pass') |
| This name can also be something like 'camera_HAL3.jea', |
| which corresponds to a test package containing multiple |
| control files, each with calls to: |
| job.run_test('camera_HAL3', **opts) |
| |
| @returns Absolute path to the control file for the test. |
| """ |
| testname, _, _tag = url.partition('.') |
| bindir = os.path.join(self.testdir, testname) |
| self.install_pkg(testname, 'test', bindir) |
| return _locate_test_control_file(bindir, url) |
| |
| |
| @_run_test_complete_on_exit |
| def run_test_detail(self, url, *args, **dargs): |
| """ |
| Summon a test object and run it, returning test status. |
| |
| @param url A url that identifies the test to run. |
| @param tag An optional keyword argument that will be added to the |
| test and subdir name. |
| @param subdir_tag An optional keyword argument that will be added |
| to the subdir name. |
| |
| @returns Test status |
| @see: client/common_lib/error.py, exit_status |
| """ |
| (subdir, testname, group_func, timeout) = self._run_test_base(url, |
| *args, |
| **dargs) |
| try: |
| self._rungroup(subdir, testname, group_func, timeout) |
| return 'GOOD' |
| except error.TestBaseException, detail: |
| return detail.exit_status |
| |
| |
| def _rungroup(self, subdir, testname, function, timeout, *args, **dargs): |
| """\ |
| subdir: |
| name of the group |
| testname: |
| name of the test to run, or support step |
| function: |
| subroutine to run |
| *args: |
| arguments for the function |
| |
| Returns the result of the passed in function |
| """ |
| |
| try: |
| optional_fields = None |
| if timeout: |
| optional_fields = {} |
| optional_fields['timeout'] = timeout |
| self.record('START', subdir, testname, |
| optional_fields=optional_fields) |
| |
| self._state.set('client', 'unexpected_reboot', (subdir, testname)) |
| try: |
| result = function(*args, **dargs) |
| self.record('END GOOD', subdir, testname) |
| return result |
| except error.TestBaseException, e: |
| self.record('END %s' % e.exit_status, subdir, testname) |
| raise |
| except error.JobError, e: |
| self.record('END ABORT', subdir, testname) |
| raise |
| except Exception, e: |
| # This should only ever happen due to a bug in the given |
| # function's code. The common case of being called by |
| # run_test() will never reach this. If a control file called |
| # run_group() itself, bugs in its function will be caught |
| # here. |
| err_msg = str(e) + '\n' + traceback.format_exc() |
| self.record('END ERROR', subdir, testname, err_msg) |
| raise |
| finally: |
| self._state.discard('client', 'unexpected_reboot') |
| |
| |
| def run_group(self, function, tag=None, **dargs): |
| """ |
| Run a function nested within a group level. |
| |
| function: |
| Callable to run. |
| tag: |
| An optional tag name for the group. If None (default) |
| function.__name__ will be used. |
| **dargs: |
| Named arguments for the function. |
| """ |
| if tag: |
| name = tag |
| else: |
| name = function.__name__ |
| |
| try: |
| return self._rungroup(subdir=None, testname=name, |
| function=function, timeout=None, **dargs) |
| except (SystemExit, error.TestBaseException): |
| raise |
| # If there was a different exception, turn it into a TestError. |
| # It will be caught by step_engine or _run_step_fn. |
| except Exception, e: |
| raise error.UnhandledTestError(e) |
| |
| |
| def cpu_count(self): |
| return utils.count_cpus() # use total system count |
| |
| |
| def start_reboot(self): |
| self.record('START', None, 'reboot') |
| self.record('GOOD', None, 'reboot.start') |
| |
| |
| def _record_reboot_failure(self, subdir, operation, status, |
| running_id=None): |
| self.record("ABORT", subdir, operation, status) |
| if not running_id: |
| running_id = utils.running_os_ident() |
| kernel = {"kernel": running_id.split("::")[0]} |
| self.record("END ABORT", subdir, 'reboot', optional_fields=kernel) |
| |
| |
| def _check_post_reboot(self, subdir, running_id=None): |
| """ |
| Function to perform post boot checks such as if the system configuration |
| has changed across reboots (specifically, CPUs and partitions). |
| |
| @param subdir: The subdir to use in the job.record call. |
| @param running_id: An optional running_id to include in the reboot |
| failure log message |
| |
| @raise JobError: Raised if the current configuration does not match the |
| pre-reboot configuration. |
| """ |
| # check to see if any partitions have changed |
| partition_list = partition_lib.get_partition_list(self, |
| exclude_swap=False) |
| mount_info = partition_lib.get_mount_info(partition_list) |
| old_mount_info = self._state.get('client', 'mount_info') |
| if mount_info != old_mount_info: |
| new_entries = mount_info - old_mount_info |
| old_entries = old_mount_info - mount_info |
| description = ("mounted partitions are different after reboot " |
| "(old entries: %s, new entries: %s)" % |
| (old_entries, new_entries)) |
| self._record_reboot_failure(subdir, "reboot.verify_config", |
| description, running_id=running_id) |
| raise error.JobError("Reboot failed: %s" % description) |
| |
| # check to see if any CPUs have changed |
| cpu_count = utils.count_cpus() |
| old_count = self._state.get('client', 'cpu_count') |
| if cpu_count != old_count: |
| description = ('Number of CPUs changed after reboot ' |
| '(old count: %d, new count: %d)' % |
| (old_count, cpu_count)) |
| self._record_reboot_failure(subdir, 'reboot.verify_config', |
| description, running_id=running_id) |
| raise error.JobError('Reboot failed: %s' % description) |
| |
| |
| def partition(self, device, loop_size=0, mountpoint=None): |
| """ |
| Work with a machine partition |
| |
| @param device: e.g. /dev/sda2, /dev/sdb1 etc... |
| @param mountpoint: Specify a directory to mount to. If not specified |
| autotest tmp directory will be used. |
| @param loop_size: Size of loopback device (in MB). Defaults to 0. |
| |
| @return: A L{client.bin.partition.partition} object |
| """ |
| |
| if not mountpoint: |
| mountpoint = self.tmpdir |
| return partition_lib.partition(self, device, loop_size, mountpoint) |
| |
| @utils.deprecated |
| def filesystem(self, device, mountpoint=None, loop_size=0): |
| """ Same as partition |
| |
| @deprecated: Use partition method instead |
| """ |
| return self.partition(device, loop_size, mountpoint) |
| |
| |
| def enable_external_logging(self): |
| pass |
| |
| |
| def disable_external_logging(self): |
| pass |
| |
| |
| def reboot_setup(self): |
| # save the partition list and mount points, as well as the cpu count |
| partition_list = partition_lib.get_partition_list(self, |
| exclude_swap=False) |
| mount_info = partition_lib.get_mount_info(partition_list) |
| self._state.set('client', 'mount_info', mount_info) |
| self._state.set('client', 'cpu_count', utils.count_cpus()) |
| |
| |
| def reboot(self): |
| self.reboot_setup() |
| self.harness.run_reboot() |
| |
| # HACK: using this as a module sometimes hangs shutdown, so if it's |
| # installed unload it first |
| utils.system("modprobe -r netconsole", ignore_status=True) |
| |
| # sync first, so that a sync during shutdown doesn't time out |
| utils.system("sync; sync", ignore_status=True) |
| |
| utils.system("(sleep 5; reboot) </dev/null >/dev/null 2>&1 &") |
| self.quit() |
| |
| |
| def noop(self, text): |
| logging.info("job: noop: " + text) |
| |
| |
| @_run_test_complete_on_exit |
| def parallel(self, *tasklist, **kwargs): |
| """Run tasks in parallel""" |
| |
| pids = [] |
| old_log_filename = self._logger.global_filename |
| for i, task in enumerate(tasklist): |
| assert isinstance(task, (tuple, list)) |
| self._logger.global_filename = old_log_filename + (".%d" % i) |
| def task_func(): |
| # stub out _record_indent with a process-local one |
| base_record_indent = self._record_indent |
| proc_local = self._job_state.property_factory( |
| '_state', '_record_indent.%d' % os.getpid(), |
| base_record_indent, namespace='client') |
| self.__class__._record_indent = proc_local |
| task[0](*task[1:]) |
| forked_pid = parallel.fork_start(self.resultdir, task_func) |
| logging.info('Just forked pid %d', forked_pid) |
| pids.append(forked_pid) |
| |
| old_log_path = os.path.join(self.resultdir, old_log_filename) |
| old_log = open(old_log_path, "a") |
| exceptions = [] |
| for i, pid in enumerate(pids): |
| # wait for the task to finish |
| try: |
| self._forkwait(pid, kwargs.get('timeout')) |
| except Exception, e: |
| logging.info('pid %d completed with error', pid) |
| exceptions.append(e) |
| # copy the logs from the subtask into the main log |
| new_log_path = old_log_path + (".%d" % i) |
| if os.path.exists(new_log_path): |
| new_log = open(new_log_path) |
| old_log.write(new_log.read()) |
| new_log.close() |
| old_log.flush() |
| os.remove(new_log_path) |
| old_log.close() |
| |
| self._logger.global_filename = old_log_filename |
| |
| # handle any exceptions raised by the parallel tasks |
| if exceptions: |
| msg = "%d task(s) failed in job.parallel" % len(exceptions) |
| raise error.JobError(msg) |
| |
| |
| def quit(self): |
| # XXX: should have a better name. |
| self.harness.run_pause() |
| raise error.JobContinue("more to come") |
| |
| |
| def complete(self, status): |
| """Write pending reports, clean up, and exit""" |
| # write out a job HTML report |
| try: |
| html_report.create_report(self.resultdir) |
| except Exception, e: |
| logging.error("Error writing job HTML report: %s", e) |
| |
| # We are about to exit 'complete' so clean up the control file. |
| dest = os.path.join(self.resultdir, os.path.basename(self._state_file)) |
| shutil.move(self._state_file, dest) |
| |
| self.harness.run_complete() |
| self.disable_external_logging() |
| sys.exit(status) |
| |
| |
| def _load_state(self): |
| # grab any initial state and set up $CONTROL.state as the backing file |
| init_state_file = self.control + '.init.state' |
| self._state_file = self.control + '.state' |
| if os.path.exists(init_state_file): |
| shutil.move(init_state_file, self._state_file) |
| self._state.set_backing_file(self._state_file) |
| |
| # initialize the state engine, if necessary |
| has_steps = self._state.has('client', 'steps') |
| if not self._is_continuation and has_steps: |
| raise RuntimeError('Loaded state can only contain client.steps if ' |
| 'this is a continuation') |
| |
| if not has_steps: |
| logging.debug('Initializing the state engine') |
| self._state.set('client', 'steps', []) |
| |
| |
| def handle_persistent_option(self, options, option_name): |
| """ |
| Select option from command line or persistent state. |
| Store selected option to allow standalone client to continue |
| after reboot with previously selected options. |
| Priority: |
| 1. explicitly specified via command line |
| 2. stored in state file (if continuing job '-c') |
| 3. default == None |
| """ |
| option = None |
| cmd_line_option = getattr(options, option_name) |
| if cmd_line_option: |
| option = cmd_line_option |
| self._state.set('client', option_name, option) |
| else: |
| stored_option = self._state.get('client', option_name, None) |
| if stored_option: |
| option = stored_option |
| logging.debug('Persistent option %s now set to %s', option_name, option) |
| return option |
| |
| |
| def __create_step_tuple(self, fn, args, dargs): |
| # Legacy code passes in an array where the first arg is |
| # the function or its name. |
| if isinstance(fn, list): |
| assert(len(args) == 0) |
| assert(len(dargs) == 0) |
| args = fn[1:] |
| fn = fn[0] |
| # Pickling actual functions is hairy, thus we have to call |
| # them by name. Unfortunately, this means only functions |
| # defined globally can be used as a next step. |
| if callable(fn): |
| fn = fn.__name__ |
| if not isinstance(fn, types.StringTypes): |
| raise StepError("Next steps must be functions or " |
| "strings containing the function name") |
| ancestry = copy.copy(self._current_step_ancestry) |
| return (ancestry, fn, args, dargs) |
| |
| |
| def next_step_append(self, fn, *args, **dargs): |
| """Define the next step and place it at the end""" |
| steps = self._state.get('client', 'steps') |
| steps.append(self.__create_step_tuple(fn, args, dargs)) |
| self._state.set('client', 'steps', steps) |
| |
| |
| def next_step(self, fn, *args, **dargs): |
| """Create a new step and place it after any steps added |
| while running the current step but before any steps added in |
| previous steps""" |
| steps = self._state.get('client', 'steps') |
| steps.insert(self._next_step_index, |
| self.__create_step_tuple(fn, args, dargs)) |
| self._next_step_index += 1 |
| self._state.set('client', 'steps', steps) |
| |
| |
| def next_step_prepend(self, fn, *args, **dargs): |
| """Insert a new step, executing first""" |
| steps = self._state.get('client', 'steps') |
| steps.insert(0, self.__create_step_tuple(fn, args, dargs)) |
| self._next_step_index += 1 |
| self._state.set('client', 'steps', steps) |
| |
| |
| |
| def _run_step_fn(self, local_vars, fn, args, dargs): |
| """Run a (step) function within the given context""" |
| |
| local_vars['__args'] = args |
| local_vars['__dargs'] = dargs |
| try: |
| exec('__ret = %s(*__args, **__dargs)' % fn, local_vars, local_vars) |
| return local_vars['__ret'] |
| except SystemExit: |
| raise # Send error.JobContinue and JobComplete on up to runjob. |
| except error.TestNAError, detail: |
| self.record(detail.exit_status, None, fn, str(detail)) |
| except Exception, detail: |
| raise error.UnhandledJobError(detail) |
| |
| |
| def _create_frame(self, global_vars, ancestry, fn_name): |
| """Set up the environment like it would have been when this |
| function was first defined. |
| |
| Child step engine 'implementations' must have 'return locals()' |
| at end end of their steps. Because of this, we can call the |
| parent function and get back all child functions (i.e. those |
| defined within it). |
| |
| Unfortunately, the call stack of the function calling |
| job.next_step might have been deeper than the function it |
| added. In order to make sure that the environment is what it |
| should be, we need to then pop off the frames we built until |
| we find the frame where the function was first defined.""" |
| |
| # The copies ensure that the parent frames are not modified |
| # while building child frames. This matters if we then |
| # pop some frames in the next part of this function. |
| current_frame = copy.copy(global_vars) |
| frames = [current_frame] |
| for steps_fn_name in ancestry: |
| ret = self._run_step_fn(current_frame, steps_fn_name, [], {}) |
| current_frame = copy.copy(ret) |
| frames.append(current_frame) |
| |
| # Walk up the stack frames until we find the place fn_name was defined. |
| while len(frames) > 2: |
| if fn_name not in frames[-2]: |
| break |
| if frames[-2][fn_name] != frames[-1][fn_name]: |
| break |
| frames.pop() |
| ancestry.pop() |
| |
| return (frames[-1], ancestry) |
| |
| |
| def _add_step_init(self, local_vars, current_function): |
| """If the function returned a dictionary that includes a |
| function named 'step_init', prepend it to our list of steps. |
| This will only get run the first time a function with a nested |
| use of the step engine is run.""" |
| |
| if (isinstance(local_vars, dict) and |
| 'step_init' in local_vars and |
| callable(local_vars['step_init'])): |
| # The init step is a child of the function |
| # we were just running. |
| self._current_step_ancestry.append(current_function) |
| self.next_step_prepend('step_init') |
| |
| |
| def step_engine(self): |
| """The multi-run engine used when the control file defines step_init. |
| |
| Does the next step. |
| """ |
| |
| # Set up the environment and then interpret the control file. |
| # Some control files will have code outside of functions, |
| # which means we need to have our state engine initialized |
| # before reading in the file. |
| global_control_vars = {'job': self, |
| 'args': self.args} |
| exec(JOB_PREAMBLE, global_control_vars, global_control_vars) |
| try: |
| execfile(self.control, global_control_vars, global_control_vars) |
| except error.TestNAError, detail: |
| self.record(detail.exit_status, None, self.control, str(detail)) |
| except SystemExit: |
| raise # Send error.JobContinue and JobComplete on up to runjob. |
| except Exception, detail: |
| # Syntax errors or other general Python exceptions coming out of |
| # the top level of the control file itself go through here. |
| raise error.UnhandledJobError(detail) |
| |
| # If we loaded in a mid-job state file, then we presumably |
| # know what steps we have yet to run. |
| if not self._is_continuation: |
| if 'step_init' in global_control_vars: |
| self.next_step(global_control_vars['step_init']) |
| else: |
| # if last job failed due to unexpected reboot, record it as fail |
| # so harness gets called |
| last_job = self._state.get('client', 'unexpected_reboot', None) |
| if last_job: |
| subdir, testname = last_job |
| self.record('FAIL', subdir, testname, 'unexpected reboot') |
| self.record('END FAIL', subdir, testname) |
| |
| # Iterate through the steps. If we reboot, we'll simply |
| # continue iterating on the next step. |
| while len(self._state.get('client', 'steps')) > 0: |
| steps = self._state.get('client', 'steps') |
| (ancestry, fn_name, args, dargs) = steps.pop(0) |
| self._state.set('client', 'steps', steps) |
| |
| self._next_step_index = 0 |
| ret = self._create_frame(global_control_vars, ancestry, fn_name) |
| local_vars, self._current_step_ancestry = ret |
| local_vars = self._run_step_fn(local_vars, fn_name, args, dargs) |
| self._add_step_init(local_vars, fn_name) |
| |
| |
| def add_sysinfo_command(self, command, logfile=None, on_every_test=False): |
| self._add_sysinfo_loggable(sysinfo.command(command, logf=logfile), |
| on_every_test) |
| |
| |
| def add_sysinfo_logfile(self, file, on_every_test=False): |
| self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test) |
| |
| |
| def _add_sysinfo_loggable(self, loggable, on_every_test): |
| if on_every_test: |
| self.sysinfo.test_loggables.add(loggable) |
| else: |
| self.sysinfo.boot_loggables.add(loggable) |
| self._save_sysinfo_state() |
| |
| |
| def _load_sysinfo_state(self): |
| state = self._state.get('client', 'sysinfo', None) |
| if state: |
| self.sysinfo.deserialize(state) |
| |
| |
| def _save_sysinfo_state(self): |
| state = self.sysinfo.serialize() |
| self._state.set('client', 'sysinfo', state) |
| |
| |
| class disk_usage_monitor: |
| def __init__(self, logging_func, device, max_mb_per_hour): |
| self.func = logging_func |
| self.device = device |
| self.max_mb_per_hour = max_mb_per_hour |
| |
| |
| def start(self): |
| self.initial_space = utils.freespace(self.device) |
| self.start_time = time.time() |
| |
| |
| def stop(self): |
| # if no maximum usage rate was set, we don't need to |
| # generate any warnings |
| if not self.max_mb_per_hour: |
| return |
| |
| final_space = utils.freespace(self.device) |
| used_space = self.initial_space - final_space |
| stop_time = time.time() |
| total_time = stop_time - self.start_time |
| # round up the time to one minute, to keep extremely short |
| # tests from generating false positives due to short, badly |
| # timed bursts of activity |
| total_time = max(total_time, 60.0) |
| |
| # determine the usage rate |
| bytes_per_sec = used_space / total_time |
| mb_per_sec = bytes_per_sec / 1024**2 |
| mb_per_hour = mb_per_sec * 60 * 60 |
| |
| if mb_per_hour > self.max_mb_per_hour: |
| msg = ("disk space on %s was consumed at a rate of %.2f MB/hour") |
| msg %= (self.device, mb_per_hour) |
| self.func(msg) |
| |
| |
| @classmethod |
| def watch(cls, *monitor_args, **monitor_dargs): |
| """ Generic decorator to wrap a function call with the |
| standard create-monitor -> start -> call -> stop idiom.""" |
| def decorator(func): |
| def watched_func(*args, **dargs): |
| monitor = cls(*monitor_args, **monitor_dargs) |
| monitor.start() |
| try: |
| func(*args, **dargs) |
| finally: |
| monitor.stop() |
| return watched_func |
| return decorator |
| |
| |
| def runjob(control, drop_caches, options): |
| """ |
| Run a job using the given control file. |
| |
| This is the main interface to this module. |
| |
| @see base_job.__init__ for parameter info. |
| """ |
| control = os.path.abspath(control) |
| state = control + '.state' |
| # Ensure state file is cleaned up before the job starts to run if autotest |
| # is not running with the --continue flag |
| if not options.cont and os.path.isfile(state): |
| logging.debug('Cleaning up previously found state file') |
| os.remove(state) |
| |
| # instantiate the job object ready for the control file. |
| myjob = None |
| try: |
| # Check that the control file is valid |
| if not os.path.exists(control): |
| raise error.JobError(control + ": control file not found") |
| |
| # When continuing, the job is complete when there is no |
| # state file, ensure we don't try and continue. |
| if options.cont and not os.path.exists(state): |
| raise error.JobComplete("all done") |
| |
| myjob = job(control=control, drop_caches=drop_caches, options=options) |
| |
| # Load in the users control file, may do any one of: |
| # 1) execute in toto |
| # 2) define steps, and select the first via next_step() |
| myjob.step_engine() |
| |
| except error.JobContinue: |
| sys.exit(5) |
| |
| except error.JobComplete: |
| sys.exit(1) |
| |
| except error.JobError, instance: |
| logging.error("JOB ERROR: " + str(instance)) |
| if myjob: |
| command = None |
| if len(instance.args) > 1: |
| command = instance.args[1] |
| myjob.record('ABORT', None, command, str(instance)) |
| myjob.record('END ABORT', None, None, str(instance)) |
| assert myjob._record_indent == 0 |
| myjob.complete(1) |
| else: |
| sys.exit(1) |
| |
| except Exception, e: |
| # NOTE: job._run_step_fn and job.step_engine will turn things into |
| # a JobError for us. If we get here, its likely an autotest bug. |
| msg = str(e) + '\n' + traceback.format_exc() |
| logging.critical("JOB ERROR (autotest bug?): " + msg) |
| if myjob: |
| myjob.record('END ABORT', None, None, msg) |
| assert myjob._record_indent == 0 |
| myjob.complete(1) |
| else: |
| sys.exit(1) |
| |
| # If we get here, then we assume the job is complete and good. |
| myjob.record('END GOOD', None, None) |
| assert myjob._record_indent == 0 |
| |
| myjob.complete(0) |
| |
| |
| class job(base_client_job): |
| |
| def __init__(self, *args, **kwargs): |
| base_client_job.__init__(self, *args, **kwargs) |
| |
| |
| def run_test(self, url, *args, **dargs): |
| log_pauser = cros_logging.LogRotationPauser() |
| passed = False |
| try: |
| log_pauser.begin() |
| passed = base_client_job.run_test(self, url, *args, **dargs) |
| if not passed: |
| # Save the VM state immediately after the test failure. |
| # This is a NOOP if the the test isn't running in a VM or |
| # if the VM is not properly configured to save state. |
| _group, testname = self.pkgmgr.get_package_name(url, 'test') |
| now = datetime.now().strftime('%I:%M:%S.%f') |
| checkpoint_name = '%s-%s' % (testname, now) |
| utils.save_vm_state(checkpoint_name) |
| finally: |
| log_pauser.end() |
| return passed |
| |
| |
| def reboot(self): |
| self.reboot_setup() |
| self.harness.run_reboot() |
| |
| # sync first, so that a sync during shutdown doesn't time out |
| utils.system('sync; sync', ignore_status=True) |
| |
| utils.system('reboot </dev/null >/dev/null 2>&1 &') |
| self.quit() |
| |
| |
| def require_gcc(self): |
| return False |
| |
| |
| # TODO(ayatane): This logic should be deduplicated with |
| # server/cros/dynamic_suite/control_file_getter.py, but the server |
| # libraries are not available on clients. |
| def _locate_test_control_file(dirpath, testname): |
| """ |
| Locate the control file for the given test. |
| |
| @param dirpath Root directory to search. |
| @param testname Name of test. |
| |
| @returns Absolute path to the control file. |
| @raise JobError: Raised if control file not found. |
| """ |
| for dirpath, _dirnames, filenames in os.walk(dirpath): |
| for filename in filenames: |
| if 'control' not in filename: |
| continue |
| path = os.path.join(dirpath, filename) |
| if _is_control_file_for_test(path, testname): |
| return os.path.abspath(path) |
| raise error.JobError( |
| 'could not find client test control file', |
| dirpath, testname) |
| |
| |
| _NAME_PATTERN = "NAME *= *['\"]([^'\"]+)['\"]" |
| |
| |
| def _is_control_file_for_test(path, testname): |
| with open(path) as f: |
| for line in f: |
| match = re.match(_NAME_PATTERN, line) |
| if match is not None: |
| return match.group(1) == testname |