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 30 matching lines...) Expand all Loading... | |
| 41 | | 41 | |
| 42 <See task_to_run.py and task_result.py> | 42 <See task_to_run.py and task_result.py> |
| 43 | 43 |
| 44 TaskProperties is embedded in TaskRequest. TaskProperties is still declared as a | 44 TaskProperties is embedded in TaskRequest. TaskProperties is still declared as a |
| 45 separate entity to clearly declare the boundary for task request deduplication. | 45 separate entity to clearly declare the boundary for task request deduplication. |
| 46 """ | 46 """ |
| 47 | 47 |
| 48 | 48 |
| 49 import datetime | 49 import datetime |
| 50 import hashlib | 50 import hashlib |
| 51 import logging | |
| 52 import random | 51 import random |
| 53 import re | 52 import re |
| 54 import urlparse | 53 import urlparse |
| 55 | 54 |
| 56 from google.appengine.api import datastore_errors | 55 from google.appengine.api import datastore_errors |
| 57 from google.appengine.ext import ndb | 56 from google.appengine.ext import ndb |
| 58 | 57 |
| 59 from components import auth | 58 from components import auth |
| 60 from components import datastore_utils | 59 from components import datastore_utils |
| 61 from components import pubsub | 60 from components import pubsub |
| 62 from components import utils | 61 from components import utils |
| 62 | |
| 63 # Must be after 'components' import, since they add it to sys.path. | |
| 64 from google import protobuf | |
| 65 | |
| 66 from proto import config_pb2 | |
| 67 from server import config | |
| 63 from server import task_pack | 68 from server import task_pack |
| 64 import cipd | 69 import cipd |
| 65 | 70 |
| 66 | 71 |
| 67 # Maximum acceptable priority value, which is effectively the lowest priority. | 72 # Maximum acceptable priority value, which is effectively the lowest priority. |
| 68 MAXIMUM_PRIORITY = 255 | 73 MAXIMUM_PRIORITY = 255 |
| 69 | 74 |
| 70 | 75 |
| 71 # Enforced on both task request and bots. | 76 # Enforced on both task request and bots. |
| 72 DIMENSION_KEY_RE = ur'^[a-zA-Z\-\_\.]+$' | 77 DIMENSION_KEY_RE = ur'^[a-zA-Z\-\_\.]+$' |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 87 # The world started on 2010-01-01 at 00:00:00 UTC. The rationale is that using | 92 # The world started on 2010-01-01 at 00:00:00 UTC. The rationale is that using |
| 88 # EPOCH (1970) means that 40 years worth of keys are wasted. | 93 # EPOCH (1970) means that 40 years worth of keys are wasted. |
| 89 # | 94 # |
| 90 # Note: This creates a 'naive' object instead of a formal UTC object. Note that | 95 # Note: This creates a 'naive' object instead of a formal UTC object. Note that |
| 91 # datetime.datetime.utcnow() also return naive objects. That's python. | 96 # datetime.datetime.utcnow() also return naive objects. That's python. |
| 92 _BEGINING_OF_THE_WORLD = datetime.datetime(2010, 1, 1, 0, 0, 0, 0) | 97 _BEGINING_OF_THE_WORLD = datetime.datetime(2010, 1, 1, 0, 0, 0, 0) |
| 93 | 98 |
| 94 | 99 |
| 95 # Used for isolated files. | 100 # Used for isolated files. |
| 96 _HASH_CHARS = frozenset('0123456789abcdef') | 101 _HASH_CHARS = frozenset('0123456789abcdef') |
| 97 NAMESPACE_RE = re.compile(r'^[a-z0-9A-Z\-._]+$') | |
| 98 | 102 |
| 99 | 103 |
| 100 ### Properties validators must come before the models. | 104 ### Properties validators must come before the models. |
| 101 | 105 |
| 102 | 106 |
| 103 def _validate_isolated(prop, value): | 107 def _validate_isolated(prop, value): |
| 104 if value: | 108 if value: |
| 105 if not _HASH_CHARS.issuperset(value) or len(value) != 40: | 109 if not _HASH_CHARS.issuperset(value) or len(value) != 40: |
| 106 raise datastore_errors.BadValueError( | 110 raise datastore_errors.BadValueError( |
| 107 '%s must be lowercase hex of length 40, not %s' % (prop._name, value)) | 111 '%s must be lowercase hex of length 40, not %s' % (prop._name, value)) |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 118 if parsed.netloc.endswith('appspot.com'): | 122 if parsed.netloc.endswith('appspot.com'): |
| 119 if parsed.scheme != 'https': | 123 if parsed.scheme != 'https': |
| 120 raise datastore_errors.BadValueError( | 124 raise datastore_errors.BadValueError( |
| 121 '%s must be https://, not %s' % (prop._name, value)) | 125 '%s must be https://, not %s' % (prop._name, value)) |
| 122 elif parsed.scheme not in ('http', 'https'): | 126 elif parsed.scheme not in ('http', 'https'): |
| 123 raise datastore_errors.BadValueError( | 127 raise datastore_errors.BadValueError( |
| 124 '%s must be https:// or http://, not %s' % (prop._name, value)) | 128 '%s must be https:// or http://, not %s' % (prop._name, value)) |
| 125 | 129 |
| 126 | 130 |
| 127 def _validate_namespace(prop, value): | 131 def _validate_namespace(prop, value): |
| 128 if not NAMESPACE_RE.match(value): | 132 if not config.NAMESPACE_RE.match(value): |
| 129 raise datastore_errors.BadValueError('malformed %s' % prop._name) | 133 raise datastore_errors.BadValueError('malformed %s' % prop._name) |
| 130 | 134 |
| 131 | 135 |
| 132 def _validate_dict_of_strings(prop, value): | 136 def _validate_dict_of_strings(prop, value): |
| 133 """Validates TaskProperties.env.""" | 137 """Validates TaskProperties.env.""" |
| 134 if not all( | 138 if not all( |
| 135 isinstance(k, unicode) and isinstance(v, unicode) | 139 isinstance(k, unicode) and isinstance(v, unicode) |
| 136 for k, v in value.iteritems()): | 140 for k, v in value.iteritems()): |
| 137 # pylint: disable=W0212 | 141 # pylint: disable=W0212 |
| 138 raise TypeError('%s must be a dict of strings' % prop._name) | 142 raise TypeError('%s must be a dict of strings' % prop._name) |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 197 | 201 |
| 198 | 202 |
| 199 def _validate_tags(prop, value): | 203 def _validate_tags(prop, value): |
| 200 """Validates and sorts TaskRequest.tags.""" | 204 """Validates and sorts TaskRequest.tags.""" |
| 201 if not ':' in value: | 205 if not ':' in value: |
| 202 # pylint: disable=W0212 | 206 # pylint: disable=W0212 |
| 203 raise datastore_errors.BadValueError( | 207 raise datastore_errors.BadValueError( |
| 204 '%s must be key:value form, not %s' % (prop._name, value)) | 208 '%s must be key:value form, not %s' % (prop._name, value)) |
| 205 | 209 |
| 206 | 210 |
| 207 def _validate_package_name(prop, value): | 211 def _validate_cipd_settings(prop, value): |
| 208 """Validates a CIPD package name.""" | 212 """Validates serialized CIPD settings. |
| 209 if not cipd.is_valid_package_name(value): | 213 |
| 214 Validates both serialization format and values. | |
| 215 """ | |
| 216 try: | |
| 217 settings = config_pb2.CipdSettings() | |
| 218 settings.MergeFromString(value) # May raise protobuf.message.DecodeError. | |
| 219 config.validate_cipd_settings(settings) # May raise ValueError. | |
| 220 except (ValueError, protobuf.message.DecodeError) as ex: | |
| 210 raise datastore_errors.BadValueError( | 221 raise datastore_errors.BadValueError( |
| 211 '%s must be a valid CIPD package name "%s"' % (prop._name, value)) | 222 '%s must be a valid CipdSettings protobuf message: %s' % |
| 223 (prop._name, ex)) | |
| 224 | |
| 225 | |
| 226 def _validate_package_name_template(prop, value): | |
| 227 """Validates a CIPD package name template.""" | |
| 228 if not cipd.is_valid_package_name_template(value): | |
| 229 raise datastore_errors.BadValueError( | |
| 230 '%s must be a valid CIPD package name template "%s"' % ( | |
| 231 prop._name, value)) | |
| 212 | 232 |
| 213 | 233 |
| 214 def _validate_package_version(prop, value): | 234 def _validate_package_version(prop, value): |
| 215 """Validates a CIPD package version.""" | 235 """Validates a CIPD package version.""" |
| 216 if not cipd.is_valid_version(value): | 236 if not cipd.is_valid_version(value): |
| 217 raise datastore_errors.BadValueError( | 237 raise datastore_errors.BadValueError( |
| 218 '%s must be a valid package version "%s"' % (prop._name, value)) | 238 '%s must be a valid package version "%s"' % (prop._name, value)) |
| 219 | 239 |
| 220 | 240 |
| 221 ### Models. | 241 ### Models. |
| 222 | 242 |
| 223 | 243 |
| 224 class FilesRef(ndb.Model): | 244 class FilesRef(ndb.Model): |
| 225 """Defines a data tree reference, normally a reference to a .isolated file.""" | 245 """Defines a data tree reference, normally a reference to a .isolated file.""" |
| 226 | 246 |
| 227 # Specifies FilesRef mode. | 247 # Specifies FilesRef mode. |
| 228 # if True, this instance is a reference to a tree, as class docstring says. | 248 # if True, this instance is a reference to a tree, as class docstring says. |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 249 if not self.isolatedserver or not self.namespace: | 269 if not self.isolatedserver or not self.namespace: |
| 250 raise datastore_errors.BadValueError( | 270 raise datastore_errors.BadValueError( |
| 251 'isolate server and namespace are required') | 271 'isolate server and namespace are required') |
| 252 | 272 |
| 253 | 273 |
| 254 class CipdPackage(ndb.Model): | 274 class CipdPackage(ndb.Model): |
| 255 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. | 275 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. |
| 256 | 276 |
| 257 A part of TaskProperties. | 277 A part of TaskProperties. |
| 258 """ | 278 """ |
| 279 # Package name template. May use cipd.ALL_PARAMS. | |
| 280 # Most users will specify ${platform} parameter. | |
| 259 package_name = ndb.StringProperty( | 281 package_name = ndb.StringProperty( |
| 260 indexed=False, validator=_validate_package_name) | 282 indexed=False, validator=_validate_package_name_template) |
| 283 # Package version that is valid for all packages matched by package_name. | |
| 284 # Most users will specify tags. | |
| 261 version = ndb.StringProperty( | 285 version = ndb.StringProperty( |
| 262 indexed=False, validator=_validate_package_version) | 286 indexed=False, validator=_validate_package_version) |
| 263 | 287 |
| 264 def _pre_put_hook(self): | 288 def _pre_put_hook(self): |
| 265 super(CipdPackage, self)._pre_put_hook() | 289 super(CipdPackage, self)._pre_put_hook() |
| 266 if not self.package_name: | 290 if not self.package_name: |
| 267 raise datastore_errors.BadValueError('CIPD package name is required') | 291 raise datastore_errors.BadValueError('CIPD package name is required') |
| 268 if not self.version: | 292 if not self.version: |
| 269 raise datastore_errors.BadValueError('CIPD package version is required') | 293 raise datastore_errors.BadValueError('CIPD package version is required') |
| 270 | 294 |
| 271 | 295 |
| 296 class CipdInput(ndb.Model): | |
|
M-A Ruel
2016/05/11 14:56:45
Why not CipdInputs and cipd_inputs? It's can be mo
nodir
2016/05/12 01:53:22
Because word "input" does not necessarily mean sin
M-A Ruel
2016/05/12 13:49:45
Oh, I had never realized. I wish I knew with input
| |
| 297 """Specifies which CIPD client and packages to install, from which server. | |
| 298 | |
| 299 A part of TaskProperties. | |
| 300 """ | |
| 301 # Serialized config_pb2.CipdSettings message. | |
| 302 # We keep it here to avoid depending on external mutable state. | |
| 303 # At the time of writing, it takes 120 bytes. | |
| 304 settings = ndb.BlobProperty(validator=_validate_cipd_settings) | |
| 305 | |
| 306 # List of packages to install in $CIPD_PATH prior task execution. | |
| 307 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) | |
| 308 | |
| 309 def _pre_put_hook(self): | |
| 310 if not self.settings: | |
| 311 raise datastore_errors.BadValueError( | |
| 312 'cipd_input settings are required; ' | |
| 313 'either specify them explicitly or configure defaults on the server.') | |
| 314 | |
| 315 if not self.packages: | |
| 316 raise datastore_errors.BadValueError( | |
| 317 'cipd_input cannot have an empty package list') | |
| 318 | |
| 319 package_names = set() | |
| 320 for p in self.packages: | |
| 321 p._pre_put_hook() | |
| 322 if p.package_name in package_names: | |
| 323 raise datastore_errors.BadValueError( | |
| 324 'package %s is specified more than once' % p.package_name) | |
| 325 package_names.add(p.package_name) | |
| 326 self.packages.sort(key=lambda p: p.package_name) | |
| 327 | |
| 328 | |
| 272 class TaskProperties(ndb.Model): | 329 class TaskProperties(ndb.Model): |
| 273 """Defines all the properties of a task to be run on the Swarming | 330 """Defines all the properties of a task to be run on the Swarming |
| 274 infrastructure. | 331 infrastructure. |
| 275 | 332 |
| 276 This entity is not saved in the DB as a standalone entity, instead it is | 333 This entity is not saved in the DB as a standalone entity, instead it is |
| 277 embedded in a TaskRequest. | 334 embedded in a TaskRequest. |
| 278 | 335 |
| 279 This model is immutable. | 336 This model is immutable. |
| 280 | 337 |
| 281 New-style TaskProperties supports invocation of run_isolated. When this | 338 New-style TaskProperties supports invocation of run_isolated. When this |
| (...skipping 21 matching lines...) Expand all Loading... | |
| 303 | 360 |
| 304 # Isolate server, namespace and input isolate hash. | 361 # Isolate server, namespace and input isolate hash. |
| 305 # | 362 # |
| 306 # Despite its name, contains isolate server URL and namespace for isolated | 363 # Despite its name, contains isolate server URL and namespace for isolated |
| 307 # output too. See TODO at the top of this class. | 364 # output too. See TODO at the top of this class. |
| 308 # May be non-None even if task input is not isolated. | 365 # May be non-None even if task input is not isolated. |
| 309 # | 366 # |
| 310 # Only inputs_ref.isolated or command&data can be specified. | 367 # Only inputs_ref.isolated or command&data can be specified. |
| 311 inputs_ref = ndb.LocalStructuredProperty(FilesRef) | 368 inputs_ref = ndb.LocalStructuredProperty(FilesRef) |
| 312 | 369 |
| 313 # A list of CIPD packages to install $CIPD_PATH and $PATH before task | 370 # CIPD packages to install. |
| 314 # execution. | 371 cipd_input = ndb.LocalStructuredProperty(CipdInput) |
| 315 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) | |
| 316 | 372 |
| 317 # Filter to use to determine the required properties on the bot to run on. For | 373 # Filter to use to determine the required properties on the bot to run on. For |
| 318 # example, Windows or hostname. Encoded as json. Optional but highly | 374 # example, Windows or hostname. Encoded as json. Optional but highly |
| 319 # recommended. | 375 # recommended. |
| 320 dimensions = datastore_utils.DeterministicJsonProperty( | 376 dimensions = datastore_utils.DeterministicJsonProperty( |
| 321 validator=_validate_dimensions, json_type=dict, indexed=False) | 377 validator=_validate_dimensions, json_type=dict, indexed=False) |
| 322 | 378 |
| 323 # Environment variables. Encoded as json. Optional. | 379 # Environment variables. Encoded as json. Optional. |
| 324 env = datastore_utils.DeterministicJsonProperty( | 380 env = datastore_utils.DeterministicJsonProperty( |
| 325 validator=_validate_dict_of_strings, json_type=dict, indexed=False) | 381 validator=_validate_dict_of_strings, json_type=dict, indexed=False) |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 393 if bool(self.command) == bool(isolated_input): | 449 if bool(self.command) == bool(isolated_input): |
| 394 raise datastore_errors.BadValueError( | 450 raise datastore_errors.BadValueError( |
| 395 'use one of command or inputs_ref.isolated') | 451 'use one of command or inputs_ref.isolated') |
| 396 if self.extra_args and not isolated_input: | 452 if self.extra_args and not isolated_input: |
| 397 raise datastore_errors.BadValueError( | 453 raise datastore_errors.BadValueError( |
| 398 'extra_args require inputs_ref.isolated') | 454 'extra_args require inputs_ref.isolated') |
| 399 if self.inputs_ref: | 455 if self.inputs_ref: |
| 400 self.inputs_ref.is_ref = False | 456 self.inputs_ref.is_ref = False |
| 401 self.inputs_ref._pre_put_hook() | 457 self.inputs_ref._pre_put_hook() |
| 402 | 458 |
| 403 package_names = set() | 459 if self.cipd_input: |
| 404 for p in self.packages: | 460 self.cipd_input._pre_put_hook() |
| 405 p._pre_put_hook() | 461 if self.idempotent: |
| 406 if p.package_name in package_names: | 462 pinned = lambda p: cipd.is_pinned_version(p.version) |
| 407 raise datastore_errors.BadValueError( | 463 assert self.cipd_input.packages # checked by cipd_input._pre_put_hook |
| 408 'package %s is specified more than once' % p.package_name) | 464 if any(not pinned(p) for p in self.cipd_input.packages): |
| 409 package_names.add(p.package_name) | 465 raise datastore_errors.BadValueError( |
| 410 self.packages.sort(key=lambda p: p.package_name) | 466 'an idempotent task cannot have unpinned packages; ' |
| 411 | 467 'use tags or instance IDs as package versions') |
| 412 if self.idempotent: | |
| 413 pinned = lambda p: cipd.is_pinned_version(p.version) | |
| 414 if self.packages and any(not pinned(p) for p in self.packages): | |
| 415 raise datastore_errors.BadValueError( | |
| 416 'an idempotent task cannot have unpinned packages; ' | |
| 417 'use instance IDs or tags as package versions') | |
| 418 | |
| 419 | 468 |
| 420 class TaskRequest(ndb.Model): | 469 class TaskRequest(ndb.Model): |
| 421 """Contains a user request. | 470 """Contains a user request. |
| 422 | 471 |
| 423 Key id is a decreasing integer based on time since utils.EPOCH plus some | 472 Key id is a decreasing integer based on time since utils.EPOCH plus some |
| 424 randomness on lower order bits. See _new_request_key() for the complete gory | 473 randomness on lower order bits. See _new_request_key() for the complete gory |
| 425 details. | 474 details. |
| 426 | 475 |
| 427 There is also "old style keys" which inherit from a fake root entity | 476 There is also "old style keys" which inherit from a fake root entity |
| 428 TaskRequestShard. | 477 TaskRequestShard. |
| (...skipping 302 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 731 _put_request(request) | 780 _put_request(request) |
| 732 return request | 781 return request |
| 733 | 782 |
| 734 | 783 |
| 735 def validate_priority(priority): | 784 def validate_priority(priority): |
| 736 """Throws ValueError if priority is not a valid value.""" | 785 """Throws ValueError if priority is not a valid value.""" |
| 737 if 0 > priority or MAXIMUM_PRIORITY < priority: | 786 if 0 > priority or MAXIMUM_PRIORITY < priority: |
| 738 raise datastore_errors.BadValueError( | 787 raise datastore_errors.BadValueError( |
| 739 'priority (%d) must be between 0 and %d (inclusive)' % | 788 'priority (%d) must be between 0 and %d (inclusive)' % |
| 740 (priority, MAXIMUM_PRIORITY)) | 789 (priority, MAXIMUM_PRIORITY)) |
| OLD | NEW |