provision.py revision 0723bf5ed7ff506e15fea180547cb6a8ae9102eb
1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import abc
7import logging
8
9import common
10from autotest_lib.frontend.afe.json_rpc import proxy
11from autotest_lib.server import frontend
12from autotest_lib.server.cros import provision_actionables as actionables
13
14
15### Constants for label prefixes
16CROS_VERSION_PREFIX = 'cros-version'
17FW_RW_VERSION_PREFIX = 'fwrw-version'
18FW_RO_VERSION_PREFIX = 'fwro-version'
19
20# Default number of provisions attempts to try if we believe the devserver is
21# flaky.
22FLAKY_DEVSERVER_ATTEMPTS = 2
23
24
25### Helpers to convert value to label
26def cros_version_to_label(image):
27    """
28    Returns the proper label name for a ChromeOS build of |image|.
29
30    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
31    @returns: A string that is the appropriate label name.
32
33    """
34    return CROS_VERSION_PREFIX + ':' + image
35
36
37def fw_version_to_label(image):
38    """
39    Returns the proper label name for a firmware build of |image|.
40
41    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
42    @returns: A string that is the appropriate label name.
43
44    """
45    return FW_RW_VERSION_PREFIX + ':' + image
46
47
48class _SpecialTaskAction(object):
49    """
50    Base class to give a template for mapping labels to tests.
51    """
52
53    __metaclass__ = abc.ABCMeta
54
55
56    # One cannot do
57    #     @abc.abstractproperty
58    #     _actions = {}
59    # so this is the next best thing
60    @abc.abstractproperty
61    def _actions(self):
62        """A dictionary mapping labels to test names."""
63        pass
64
65
66    @abc.abstractproperty
67    def name(self):
68        """The name of this special task to be used in output."""
69        pass
70
71
72    @classmethod
73    def acts_on(cls, label):
74        """
75        Returns True if the label is a label that we recognize as something we
76        know how to act on, given our _actions.
77
78        @param label: The label as a string.
79        @returns: True if there exists a test to run for this label.
80
81        """
82        return label.split(':')[0] in cls._actions
83
84
85    @classmethod
86    def test_for(cls, label):
87        """
88        Returns the test associated with the given (string) label name.
89
90        @param label: The label for which the action is being requested.
91        @returns: The string name of the test that should be run.
92        @raises KeyError: If the name was not recognized as one we care about.
93
94        """
95        return cls._actions[label]
96
97
98    @classmethod
99    def partition(cls, labels):
100        """
101        Filter a list of labels into two sets: those labels that we know how to
102        act on and those that we don't know how to act on.
103
104        @param labels: A list of strings of labels.
105        @returns: A tuple where the first element is a set of unactionable
106                  labels, and the second element is a set of the actionable
107                  labels.
108        """
109        capabilities = set()
110        configurations = set()
111
112        for label in labels:
113            if cls.acts_on(label):
114                configurations.add(label)
115            else:
116                capabilities.add(label)
117
118        return capabilities, configurations
119
120
121class Verify(_SpecialTaskAction):
122    """
123    Tests to verify that the DUT is in a sane, known good state that we can run
124    tests on.  Failure to verify leads to running Repair.
125    """
126
127    _actions = {
128        'modem_repair': actionables.TestActionable('cellular_StaleModemReboot'),
129        # TODO(crbug.com/404421): set rpm action to power_RPMTest after the RPM
130        # is stable in lab (destiny). The power_RPMTest failure led to reset job
131        # failure and that left dut in Repair Failed. Since the test will fail
132        # anyway due to the destiny lab issue, and test retry will retry the
133        # test in another DUT.
134        # This change temporarily disable the RPM check in reset job.
135        # Another way to do this is to remove rpm dependency from tests' control
136        # file. That will involve changes on multiple control files. This one
137        # line change here is a simple temporary fix.
138        'rpm': actionables.TestActionable('dummy_PassServer'),
139    }
140
141    name = 'verify'
142
143
144class Provision(_SpecialTaskAction):
145    """
146    Provisioning runs to change the configuration of the DUT from one state to
147    another.  It will only be run on verified DUTs.
148    """
149
150    # TODO(milleral): http://crbug.com/249555
151    # Create some way to discover and register provisioning tests so that we
152    # don't need to hand-maintain a list of all of them.
153    _actions = {
154        CROS_VERSION_PREFIX: actionables.TestActionable(
155                'provision_AutoUpdate',
156                extra_kwargs={'disable_sysinfo': False,
157                              'disable_before_test_sysinfo': False,
158                              'disable_before_iteration_sysinfo': True,
159                              'disable_after_test_sysinfo': True,
160                              'disable_after_iteration_sysinfo': True}),
161        FW_RW_VERSION_PREFIX: actionables.TestActionable(
162                'provision_FirmwareUpdate'),
163    }
164
165    name = 'provision'
166
167
168class Cleanup(_SpecialTaskAction):
169    """
170    Cleanup runs after a test fails to try and remove artifacts of tests and
171    ensure the DUT will be in a sane state for the next test run.
172    """
173
174    _actions = {
175        'cleanup-reboot': actionables.RebootActionable(),
176    }
177
178    name = 'cleanup'
179
180
181class Repair(_SpecialTaskAction):
182    """
183    Repair runs when one of the other special tasks fails.  It should be able
184    to take a component of the DUT that's in an unknown state and restore it to
185    a good state.
186    """
187
188    _actions = {
189    }
190
191    name = 'repair'
192
193
194
195# TODO(milleral): crbug.com/364273
196# Label doesn't really mean label in this context.  We're putting things into
197# DEPENDENCIES that really aren't DEPENDENCIES, and we should probably stop
198# doing that.
199def is_for_special_action(label):
200    """
201    If any special task handles the label specially, then we're using the label
202    to communicate that we want an action, and not as an actual dependency that
203    the test has.
204
205    @param label: A string label name.
206    @return True if any special task handles this label specially,
207            False if no special task handles this label.
208    """
209    return (Verify.acts_on(label) or
210            Provision.acts_on(label) or
211            Cleanup.acts_on(label) or
212            Repair.acts_on(label))
213
214
215def filter_labels(labels):
216    """
217    Filter a list of labels into two sets: those labels that we know how to
218    change and those that we don't.  For the ones we know how to change, split
219    them apart into the name of configuration type and its value.
220
221    @param labels: A list of strings of labels.
222    @returns: A tuple where the first element is a set of unprovisionable
223              labels, and the second element is a set of the provisionable
224              labels.
225
226    >>> filter_labels(['bluetooth', 'cros-version:lumpy-release/R28-3993.0.0'])
227    (set(['bluetooth']), set(['cros-version:lumpy-release/R28-3993.0.0']))
228
229    """
230    return Provision.partition(labels)
231
232
233def split_labels(labels):
234    """
235    Split a list of labels into a dict mapping name to value.  All labels must
236    be provisionable labels, or else a ValueError
237
238    @param labels: list of strings of label names
239    @returns: A dict of where the key is the configuration name, and the value
240              is the configuration value.
241    @raises: ValueError if a label is not a provisionable label.
242
243    >>> split_labels(['cros-version:lumpy-release/R28-3993.0.0'])
244    {'cros-version': 'lumpy-release/R28-3993.0.0'}
245    >>> split_labels(['bluetooth'])
246    Traceback (most recent call last):
247    ...
248    ValueError: Unprovisionable label bluetooth
249
250    """
251    configurations = dict()
252
253    for label in labels:
254        if Provision.acts_on(label):
255            name, value = label.split(':', 1)
256            configurations[name] = value
257        else:
258            raise ValueError('Unprovisionable label %s' % label)
259
260    return configurations
261
262
263def join(provision_type, provision_value):
264    """
265    Combine the provision type and value into the label name.
266
267    @param provision_type: One of the constants that are the label prefixes.
268    @param provision_value: A string of the value for this provision type.
269    @returns: A string that is the label name for this (type, value) pair.
270
271    >>> join(CROS_VERSION_PREFIX, 'lumpy-release/R27-3773.0.0')
272    'cros-version:lumpy-release/R27-3773.0.0'
273
274    """
275    return '%s:%s' % (provision_type, provision_value)
276
277
278class SpecialTaskActionException(Exception):
279    """
280    Exception raised when a special task fails to successfully run a test that
281    is required.
282
283    This is also a literally meaningless exception.  It's always just discarded.
284    """
285
286
287def run_special_task_actions(job, host, labels, task):
288    """
289    Iterate through all `label`s and run any tests on `host` that `task` has
290    corresponding to the passed in labels.
291
292    Emits status lines for each run test, and INFO lines for each skipped label.
293
294    @param job: A job object from a control file.
295    @param host: The host to run actions on.
296    @param labels: The list of job labels to work on.
297    @param task: An instance of _SpecialTaskAction.
298    @returns: None
299    @raises: SpecialTaskActionException if a test fails.
300
301    """
302    capabilities, configuration = filter_labels(labels)
303
304    for label in capabilities:
305        if task.acts_on(label):
306            action_item = task.test_for(label)
307            success = action_item.execute(job=job, host=host)
308            if not success:
309                raise SpecialTaskActionException()
310        else:
311            job.record('INFO', None, task.name,
312                       "Can't %s label '%s'." % (task.name, label))
313
314    for name, value in split_labels(configuration).items():
315        if task.acts_on(name):
316            action_item = task.test_for(name)
317            success = action_item.execute(job=job, host=host, value=value)
318            if not success:
319                raise SpecialTaskActionException()
320        else:
321            job.record('INFO', None, task.name,
322                       "Can't %s label '%s:%s'." % (task.name, name, value))
323
324
325# This has been copied out of dynamic_suite's reimager.py, which no longer
326# exists.  I'd prefer if this would go away by doing http://crbug.com/249424,
327# so that labels are just automatically made when we try to add them to a host.
328def ensure_label_exists(name):
329    """
330    Ensure that a label called |name| exists in the autotest DB.
331
332    @param name: the label to check for/create.
333    @raises ValidationError: There was an error in the response that was
334                             not because the label already existed.
335
336    """
337    afe = frontend.AFE()
338    try:
339        afe.create_label(name=name)
340    except proxy.ValidationError as ve:
341        if ('name' in ve.problem_keys and
342            'This value must be unique' in ve.problem_keys['name']):
343            logging.debug('Version label %s already exists', name)
344        else:
345            raise ve
346