| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging |
| import re |
| import sys |
| import time |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import global_config |
| from autotest_lib.client.common_lib.cros import dev_server |
| from autotest_lib.server import afe_utils |
| from autotest_lib.server import test |
| from autotest_lib.server import utils |
| from autotest_lib.server.cros import provision |
| from autotest_lib.server.cros import provisioner |
| |
| try: |
| from chromite.lib import metrics |
| except ImportError: |
| metrics = utils.metrics_mock |
| |
| _CONFIG = global_config.global_config |
| # pylint: disable-msg=E1120 |
| _IMAGE_URL_PATTERN = _CONFIG.get_config_value('CROS', |
| 'image_url_pattern', |
| type=str) |
| |
| |
| def _metric_name(base_name): |
| return 'chromeos/autotest/provision/' + base_name |
| |
| |
| def _get_build_metrics_fields(build_name): |
| try: |
| return utils.ParseBuildName(build_name)[0:2] |
| except utils.ParseBuildNameException: |
| logging.warning( |
| 'Unable to parse build name %s for metrics. ' |
| 'Continuing anyway.', build_name) |
| return ('', '') |
| |
| |
| def _emit_updater_metrics(name_prefix, build_name, failure_reason, duration, |
| fields): |
| # reset_after=True is required for String gauges events to ensure that |
| # the metrics are not repeatedly emitted until the server restarts. |
| metrics.String(_metric_name(name_prefix + '_build_by_devserver_dut'), |
| reset_after=True).set(build_name, fields=fields) |
| if failure_reason: |
| metrics.String(_metric_name(name_prefix + |
| '_failure_reason_by_devserver_dut'), |
| reset_after=True).set(failure_reason, fields=fields) |
| metrics.SecondsDistribution( |
| _metric_name(name_prefix + '_duration_by_devserver_dut')).add( |
| duration, fields=fields) |
| |
| |
| def _emit_provision_metrics(update_url, dut_host_name, exception, duration): |
| # The following is high cardinality, but sparse. |
| # Each DUT is of a single board type, and likely build type. |
| # |
| # TODO(jrbarnette) The devserver-triggered provisioning code |
| # includes retries in certain cases. For that reason, the metrics |
| # distinguish 'provision' metrics which summarizes across all |
| # retries, and 'auto_update' which summarizes an individual update |
| # attempt. ChromiumOSProvisioner doesn't do retries, so we just report |
| # the same information twice. We should replace the metrics with |
| # something better tailored to the current implementation. |
| build_name = provisioner.url_to_image_name(update_url) |
| board, build_type = _get_build_metrics_fields(build_name) |
| fields = { |
| 'board': board, |
| 'build_type': build_type, |
| 'dut_host_name': dut_host_name, |
| 'dev_server': dev_server.get_resolved_hostname(update_url), |
| 'success': not exception, |
| } |
| failure_reason = provisioner.get_update_failure_reason(exception) |
| _emit_updater_metrics('provision', build_name, failure_reason, duration, |
| fields) |
| fields['attempt'] = 1 |
| _emit_updater_metrics('auto_update', build_name, failure_reason, duration, |
| fields) |
| |
| |
| class provision_QuickProvision(test.test): |
| """A test that can provision a machine to the correct ChromeOS version.""" |
| version = 1 |
| |
| def initialize(self, host, value, is_test_na=False): |
| """Initialize. |
| |
| @param host: The host object to update to |value|. |
| @param value: The build type and version to install on the host. |
| @param is_test_na: boolean, if True, will simply skip the test |
| and emit TestNAError. The control file |
| determines whether the test should be skipped |
| and passes the decision via this argument. Note |
| we can't raise TestNAError in control file as it won't |
| be caught and handled properly. |
| """ |
| if is_test_na: |
| raise error.TestNAError( |
| 'Test not available for test_that. chroot detected, ' |
| 'you are probably using test_that.') |
| # We check value in initialize so that it fails faster. |
| if not value: |
| raise error.TestFail('No build version specified.') |
| |
| def run_once(self, host, value): |
| """The method called by the control file to start the test. |
| |
| @param host: The host object to update to |value|. |
| @param value: The host object to provision with a build corresponding |
| to |value|. |
| """ |
| with_cheets = False |
| logging.debug('Start provisioning %s to %s.', host, value) |
| if value.endswith(provision.CHEETS_SUFFIX): |
| image = re.sub(provision.CHEETS_SUFFIX + '$', '', value) |
| with_cheets = True |
| else: |
| image = value |
| |
| # If the host is already on the correct build, we have nothing to do. |
| # Note that this means we're not doing any sort of stateful-only |
| # update, and that we're relying more on cleanup to do cleanup. |
| info = host.host_info_store.get() |
| if info.build == value: |
| # We can't raise a TestNA, as would make sense, as that makes |
| # job.run_test return False as if the job failed. However, it'd |
| # still be nice to get this into the status.log, so we manually |
| # emit an INFO line instead. |
| self.job.record('INFO', None, None, |
| 'Host already running %s' % value) |
| return |
| |
| try: |
| ds = dev_server.ImageServer.resolve(image, host.hostname) |
| except dev_server.DevServerException as e: |
| raise error.TestFail, str(e), sys.exc_info()[2] |
| |
| url = _IMAGE_URL_PATTERN % (ds.url(), image) |
| |
| logging.debug('Installing image from URL: %s', url) |
| start_time = time.time() |
| failure = None |
| try: |
| afe_utils.machine_install_and_update_labels(host, |
| url, |
| with_cheets, |
| staging_server=ds) |
| except BaseException as e: |
| failure = e |
| raise |
| finally: |
| _emit_provision_metrics(url, host.hostname, failure, |
| time.time() - start_time) |
| logging.debug('Finished provisioning %s to %s', host, value) |