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