Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # coding: utf-8 | 1 # coding: utf-8 |
| 2 # Copyright 2014 The LUCI Authors. All rights reserved. | 2 # Copyright 2014 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed by the Apache v2.0 license that can be | 3 # Use of this source code is governed by the Apache v2.0 license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Tasks definition. | 6 """Tasks definition. |
| 7 | 7 |
| 8 Each user request creates a new TaskRequest. The TaskRequest instance saves the | 8 Each user request creates a new TaskRequest. The TaskRequest instance saves the |
| 9 metadata of the request, e.g. who requested it, when why, etc. It links to the | 9 metadata of the request, e.g. who requested it, when why, etc. It links to the |
| 10 actual data of the request in a TaskProperties. The TaskProperties represents | 10 actual data of the request in a TaskProperties. The TaskProperties represents |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 54 import urlparse | 54 import urlparse |
| 55 | 55 |
| 56 from google.appengine.api import datastore_errors | 56 from google.appengine.api import datastore_errors |
| 57 from google.appengine.ext import ndb | 57 from google.appengine.ext import ndb |
| 58 | 58 |
| 59 from components import auth | 59 from components import auth |
| 60 from components import datastore_utils | 60 from components import datastore_utils |
| 61 from components import pubsub | 61 from components import pubsub |
| 62 from components import utils | 62 from components import utils |
| 63 from server import task_pack | 63 from server import task_pack |
| 64 import cipd | |
| 64 | 65 |
| 65 | 66 |
| 66 # Maximum acceptable priority value, which is effectively the lowest priority. | 67 # Maximum acceptable priority value, which is effectively the lowest priority. |
| 67 MAXIMUM_PRIORITY = 255 | 68 MAXIMUM_PRIORITY = 255 |
| 68 | 69 |
| 69 | 70 |
| 70 # Enforced on both task request and bots. | 71 # Enforced on both task request and bots. |
| 71 DIMENSION_KEY_RE = ur'^[a-zA-Z\-\_\.]+$' | 72 DIMENSION_KEY_RE = ur'^[a-zA-Z\-\_\.]+$' |
| 72 | 73 |
| 73 | 74 |
| (...skipping 122 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 196 | 197 |
| 197 | 198 |
| 198 def _validate_tags(prop, value): | 199 def _validate_tags(prop, value): |
| 199 """Validates and sorts TaskRequest.tags.""" | 200 """Validates and sorts TaskRequest.tags.""" |
| 200 if not ':' in value: | 201 if not ':' in value: |
| 201 # pylint: disable=W0212 | 202 # pylint: disable=W0212 |
| 202 raise datastore_errors.BadValueError( | 203 raise datastore_errors.BadValueError( |
| 203 '%s must be key:value form, not %s' % (prop._name, value)) | 204 '%s must be key:value form, not %s' % (prop._name, value)) |
| 204 | 205 |
| 205 | 206 |
| 207 def _validate_package_name(prop, value): | |
| 208 """Validates a CIPD package name.""" | |
| 209 if not cipd.is_valid_package_name(value): | |
| 210 raise datastore_errors.BadValueError( | |
| 211 '%s must be a valid CIPD package name "%s"' % (prop._name, value)) | |
| 212 | |
| 213 | |
| 214 def _validate_package_version(prop, value): | |
| 215 """Validates a CIPD package version.""" | |
| 216 if not cipd.is_valid_version(value): | |
| 217 raise datastore_errors.BadValueError( | |
| 218 '%s must be a valid package version "%s"' % (prop._name, value)) | |
| 219 | |
| 220 | |
| 206 ### Models. | 221 ### Models. |
| 207 | 222 |
| 208 | 223 |
| 209 class FilesRef(ndb.Model): | 224 class FilesRef(ndb.Model): |
| 210 """Defines a data tree reference, normally a reference to a .isolated file.""" | 225 """Defines a data tree reference, normally a reference to a .isolated file.""" |
| 211 # The hash of an isolated archive. | 226 # The hash of an isolated archive. |
| 212 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) | 227 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) |
| 213 # The hostname of the isolated server to use. | 228 # The hostname of the isolated server to use. |
| 214 isolatedserver = ndb.StringProperty( | 229 isolatedserver = ndb.StringProperty( |
| 215 validator=_validate_hostname, indexed=False) | 230 validator=_validate_hostname, indexed=False) |
| 216 # Namespace on the isolate server. | 231 # Namespace on the isolate server. |
| 217 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) | 232 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) |
| 218 | 233 |
| 219 def _pre_put_hook(self): | 234 def _pre_put_hook(self): |
| 220 super(FilesRef, self)._pre_put_hook() | 235 super(FilesRef, self)._pre_put_hook() |
| 221 if not self.isolated or not self.isolatedserver or not self.namespace: | 236 if not self.isolated or not self.isolatedserver or not self.namespace: |
| 222 raise datastore_errors.BadValueError( | 237 raise datastore_errors.BadValueError( |
| 223 'isolated requires server and namespace') | 238 'isolated requires server and namespace') |
| 224 | 239 |
| 225 | 240 |
| 241 class CipdPackage(ndb.Model): | |
| 242 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. | |
| 243 | |
| 244 A part of TaskProperties. | |
| 245 """ | |
| 246 package_name = ndb.StringProperty( | |
| 247 indexed=False, validator=_validate_package_name) | |
| 248 version = ndb.StringProperty( | |
| 249 indexed=False, validator=_validate_package_version) | |
| 250 | |
| 251 def _pre_put_hook(self): | |
| 252 super(CipdPackage, self)._pre_put_hook() | |
| 253 if not self.package_name: | |
| 254 raise datastore_errors.BadValueError('CIPD package name is required') | |
| 255 if not self.version: | |
| 256 raise datastore_errors.BadValueError('CIPD package version is required') | |
| 257 | |
| 258 | |
| 226 class TaskProperties(ndb.Model): | 259 class TaskProperties(ndb.Model): |
| 227 """Defines all the properties of a task to be run on the Swarming | 260 """Defines all the properties of a task to be run on the Swarming |
| 228 infrastructure. | 261 infrastructure. |
| 229 | 262 |
| 230 This entity is not saved in the DB as a standalone entity, instead it is | 263 This entity is not saved in the DB as a standalone entity, instead it is |
| 231 embedded in a TaskRequest. | 264 embedded in a TaskRequest. |
| 232 | 265 |
| 233 This model is immutable. | 266 This model is immutable. |
| 234 | 267 |
| 235 New-style TaskProperties supports invocation of run_isolated. When this | 268 New-style TaskProperties supports invocation of run_isolated. When this |
| 236 behavior is desired, the member .inputs_ref must be suppled. .extra_args can | 269 behavior is desired, the member .inputs_ref must be suppled. .extra_args can |
| 237 be supplied to pass extraneous arguments. | 270 be supplied to pass extraneous arguments. |
| 238 """ | 271 """ |
| 239 # Hashing algorithm used to hash TaskProperties to create its key. | 272 # Hashing algorithm used to hash TaskProperties to create its key. |
| 240 HASHING_ALGO = hashlib.sha1 | 273 HASHING_ALGO = hashlib.sha1 |
| 241 | 274 |
| 242 # Commands to run. It is a list of 1 item, the command to run. | 275 # Commands to run. It is a list of 1 item, the command to run. |
| 243 # TODO(maruel): Remove after 2016-06-01. | 276 # TODO(maruel): Remove after 2016-06-01. |
| 244 commands = datastore_utils.DeterministicJsonProperty( | 277 commands = datastore_utils.DeterministicJsonProperty( |
| 245 json_type=list, indexed=False) | 278 json_type=list, indexed=False) |
| 246 # Command to run. This is only relevant when self._inputs_ref is None. This is | 279 # Command to run. This is only relevant when self._inputs_ref is None. This is |
| 247 # what is called 'raw commands', in the sense that no inputs files are | 280 # what is called 'raw commands', in the sense that no inputs files are |
| 248 # declared. | 281 # declared. |
| 249 command = ndb.StringProperty(repeated=True, indexed=False) | 282 command = ndb.StringProperty(repeated=True, indexed=False) |
| 250 | 283 |
| 251 # File inputs of the task. Only inputs_ref or command&data can be specified. | 284 # File inputs of the task. Only inputs_ref or command&data can be specified. |
| 252 inputs_ref = ndb.LocalStructuredProperty(FilesRef) | 285 inputs_ref = ndb.LocalStructuredProperty(FilesRef) |
| 253 | 286 |
| 287 # A list of CIPD packages to install $CIPD_PATH and $PATH before task | |
| 288 # execution. | |
| 289 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) | |
| 290 | |
| 254 # Filter to use to determine the required properties on the bot to run on. For | 291 # Filter to use to determine the required properties on the bot to run on. For |
| 255 # example, Windows or hostname. Encoded as json. Optional but highly | 292 # example, Windows or hostname. Encoded as json. Optional but highly |
| 256 # recommended. | 293 # recommended. |
| 257 dimensions = datastore_utils.DeterministicJsonProperty( | 294 dimensions = datastore_utils.DeterministicJsonProperty( |
| 258 validator=_validate_dimensions, json_type=dict, indexed=False) | 295 validator=_validate_dimensions, json_type=dict, indexed=False) |
| 259 | 296 |
| 260 # Environment variables. Encoded as json. Optional. | 297 # Environment variables. Encoded as json. Optional. |
| 261 env = datastore_utils.DeterministicJsonProperty( | 298 env = datastore_utils.DeterministicJsonProperty( |
| 262 validator=_validate_dict_of_strings, json_type=dict, indexed=False) | 299 validator=_validate_dict_of_strings, json_type=dict, indexed=False) |
| 263 | 300 |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 326 raise datastore_errors.BadValueError( | 363 raise datastore_errors.BadValueError( |
| 327 'commands is not supported anymore') | 364 'commands is not supported anymore') |
| 328 if not self.is_terminate: | 365 if not self.is_terminate: |
| 329 if bool(self.command) == bool(self.inputs_ref): | 366 if bool(self.command) == bool(self.inputs_ref): |
| 330 raise datastore_errors.BadValueError('use one of command or inputs_ref') | 367 raise datastore_errors.BadValueError('use one of command or inputs_ref') |
| 331 if self.extra_args and not self.inputs_ref: | 368 if self.extra_args and not self.inputs_ref: |
| 332 raise datastore_errors.BadValueError('extra_args require inputs_ref') | 369 raise datastore_errors.BadValueError('extra_args require inputs_ref') |
| 333 if self.inputs_ref: | 370 if self.inputs_ref: |
| 334 self.inputs_ref._pre_put_hook() | 371 self.inputs_ref._pre_put_hook() |
| 335 | 372 |
| 373 package_names = set() | |
| 374 for p in self.packages: | |
| 375 p._pre_put_hook() | |
| 376 if p.package_name in package_names: | |
| 377 # We don't support different versions of the same package yet. | |
|
M-A Ruel
2016/04/27 17:43:56
"yet"? Ever? I don't think this comment is necessa
nodir
2016/04/29 22:28:08
removed comment
| |
| 378 raise datastore_errors.BadValueError( | |
| 379 'package %s is specified more than once' % p.package_name) | |
| 380 package_names.add(p.package_name) | |
| 381 self.packages = sorted(self.packages, key=lambda p: p.package_name) | |
|
M-A Ruel
2016/04/27 17:43:56
self.packages.sort(key=lambda p: p.package_name)
?
nodir
2016/04/29 22:28:08
Done.
| |
| 382 | |
| 383 if self.idempotent: | |
| 384 pinned = lambda p: cipd.is_pinned_version(p.version) | |
| 385 if self.packages and any(not pinned(p) for p in self.packages): | |
| 386 raise datastore_errors.BadValueError( | |
| 387 'an idempotent task cannot have unpinned packages; ' | |
| 388 'use instance IDs or tags as package versions') | |
| 389 | |
| 390 | |
| 336 | 391 |
| 337 class TaskRequest(ndb.Model): | 392 class TaskRequest(ndb.Model): |
| 338 """Contains a user request. | 393 """Contains a user request. |
| 339 | 394 |
| 340 Key id is a decreasing integer based on time since utils.EPOCH plus some | 395 Key id is a decreasing integer based on time since utils.EPOCH plus some |
| 341 randomness on lower order bits. See _new_request_key() for the complete gory | 396 randomness on lower order bits. See _new_request_key() for the complete gory |
| 342 details. | 397 details. |
| 343 | 398 |
| 344 There is also "old style keys" which inherit from a fake root entity | 399 There is also "old style keys" which inherit from a fake root entity |
| 345 TaskRequestShard. | 400 TaskRequestShard. |
| (...skipping 212 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 558 root_entity_shard_id)) | 613 root_entity_shard_id)) |
| 559 | 614 |
| 560 | 615 |
| 561 def make_request(request, is_bot_or_admin): | 616 def make_request(request, is_bot_or_admin): |
| 562 """Registers the request in the DB. | 617 """Registers the request in the DB. |
| 563 | 618 |
| 564 Fills up some values. | 619 Fills up some values. |
| 565 | 620 |
| 566 If parent_task_id is set, properties for the parent are used: | 621 If parent_task_id is set, properties for the parent are used: |
| 567 - priority: defaults to parent.priority - 1 | 622 - priority: defaults to parent.priority - 1 |
| 568 - user: overriden by parent.user | 623 - user: overridden by parent.user |
| 569 | 624 |
| 570 """ | 625 """ |
| 571 assert request.__class__ is TaskRequest | 626 assert request.__class__ is TaskRequest |
| 572 if request.parent_task_id: | 627 if request.parent_task_id: |
| 573 run_result_key = task_pack.unpack_run_result_key(request.parent_task_id) | 628 run_result_key = task_pack.unpack_run_result_key(request.parent_task_id) |
| 574 result_summary_key = task_pack.run_result_key_to_result_summary_key( | 629 result_summary_key = task_pack.run_result_key_to_result_summary_key( |
| 575 run_result_key) | 630 run_result_key) |
| 576 request_key = task_pack.result_summary_key_to_request_key( | 631 request_key = task_pack.result_summary_key_to_request_key( |
| 577 result_summary_key) | 632 result_summary_key) |
| 578 parent = request_key.get() | 633 parent = request_key.get() |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 648 _put_request(request) | 703 _put_request(request) |
| 649 return request | 704 return request |
| 650 | 705 |
| 651 | 706 |
| 652 def validate_priority(priority): | 707 def validate_priority(priority): |
| 653 """Throws ValueError if priority is not a valid value.""" | 708 """Throws ValueError if priority is not a valid value.""" |
| 654 if 0 > priority or MAXIMUM_PRIORITY < priority: | 709 if 0 > priority or MAXIMUM_PRIORITY < priority: |
| 655 raise datastore_errors.BadValueError( | 710 raise datastore_errors.BadValueError( |
| 656 'priority (%d) must be between 0 and %d (inclusive)' % | 711 'priority (%d) must be between 0 and %d (inclusive)' % |
| 657 (priority, MAXIMUM_PRIORITY)) | 712 (priority, MAXIMUM_PRIORITY)) |
| OLD | NEW |