| 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 from server import config |
| 63 from server import task_pack | 64 from server import task_pack |
| 64 import cipd | 65 import cipd |
| 65 | 66 |
| 66 | 67 |
| 67 # Maximum acceptable priority value, which is effectively the lowest priority. | 68 # Maximum acceptable priority value, which is effectively the lowest priority. |
| 68 MAXIMUM_PRIORITY = 255 | 69 MAXIMUM_PRIORITY = 255 |
| 69 | 70 |
| 70 | 71 |
| 71 # Enforced on both task request and bots. | 72 # Enforced on both task request and bots. |
| 72 DIMENSION_KEY_RE = ur'^[a-zA-Z\-\_\.]+$' | 73 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 | 88 # 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. | 89 # EPOCH (1970) means that 40 years worth of keys are wasted. |
| 89 # | 90 # |
| 90 # Note: This creates a 'naive' object instead of a formal UTC object. Note that | 91 # 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. | 92 # datetime.datetime.utcnow() also return naive objects. That's python. |
| 92 _BEGINING_OF_THE_WORLD = datetime.datetime(2010, 1, 1, 0, 0, 0, 0) | 93 _BEGINING_OF_THE_WORLD = datetime.datetime(2010, 1, 1, 0, 0, 0, 0) |
| 93 | 94 |
| 94 | 95 |
| 95 # Used for isolated files. | 96 # Used for isolated files. |
| 96 _HASH_CHARS = frozenset('0123456789abcdef') | 97 _HASH_CHARS = frozenset('0123456789abcdef') |
| 97 NAMESPACE_RE = re.compile(r'^[a-z0-9A-Z\-._]+$') | |
| 98 | 98 |
| 99 | 99 |
| 100 ### Properties validators must come before the models. | 100 ### Properties validators must come before the models. |
| 101 | 101 |
| 102 | 102 |
| 103 def _validate_isolated(prop, value): | 103 def _validate_isolated(prop, value): |
| 104 if value: | 104 if value: |
| 105 if not _HASH_CHARS.issuperset(value) or len(value) != 40: | 105 if not _HASH_CHARS.issuperset(value) or len(value) != 40: |
| 106 raise datastore_errors.BadValueError( | 106 raise datastore_errors.BadValueError( |
| 107 '%s must be lowercase hex of length 40, not %s' % (prop._name, value)) | 107 '%s must be lowercase hex of length 40, not %s' % (prop._name, value)) |
| 108 | 108 |
| 109 | 109 |
| 110 def _validate_hostname(prop, value): | 110 def _validate_url(prop, value): |
| 111 # pylint: disable=unused-argument | 111 # pylint: disable=unused-argument |
| 112 if value: | 112 if value: |
| 113 # It must be https://*.appspot.com or https?://* | 113 # It must be https://*.appspot.com or https?://* |
| 114 parsed = urlparse.urlparse(value) | 114 parsed = urlparse.urlparse(value) |
| 115 if not parsed.netloc: | 115 if not parsed.netloc: |
| 116 raise datastore_errors.BadValueError( | 116 raise datastore_errors.BadValueError( |
| 117 '%s must be valid hostname, not %s' % (prop._name, value)) | 117 '%s must be valid hostname, not %s' % (prop._name, value)) |
| 118 if parsed.netloc.endswith('appspot.com'): | 118 if parsed.netloc.endswith('appspot.com'): |
| 119 if parsed.scheme != 'https': | 119 if parsed.scheme != 'https': |
| 120 raise datastore_errors.BadValueError( | 120 raise datastore_errors.BadValueError( |
| 121 '%s must be https://, not %s' % (prop._name, value)) | 121 '%s must be https://, not %s' % (prop._name, value)) |
| 122 elif parsed.scheme not in ('http', 'https'): | 122 elif parsed.scheme not in ('http', 'https'): |
| 123 raise datastore_errors.BadValueError( | 123 raise datastore_errors.BadValueError( |
| 124 '%s must be https:// or http://, not %s' % (prop._name, value)) | 124 '%s must be https:// or http://, not %s' % (prop._name, value)) |
| 125 | 125 |
| 126 | 126 |
| 127 def _validate_namespace(prop, value): | 127 def _validate_namespace(prop, value): |
| 128 if not NAMESPACE_RE.match(value): | 128 if not config.NAMESPACE_RE.match(value): |
| 129 raise datastore_errors.BadValueError('malformed %s' % prop._name) | 129 raise datastore_errors.BadValueError('malformed %s' % prop._name) |
| 130 | 130 |
| 131 | 131 |
| 132 def _validate_dict_of_strings(prop, value): | 132 def _validate_dict_of_strings(prop, value): |
| 133 """Validates TaskProperties.env.""" | 133 """Validates TaskProperties.env.""" |
| 134 if not all( | 134 if not all( |
| 135 isinstance(k, unicode) and isinstance(v, unicode) | 135 isinstance(k, unicode) and isinstance(v, unicode) |
| 136 for k, v in value.iteritems()): | 136 for k, v in value.iteritems()): |
| 137 # pylint: disable=W0212 | 137 # pylint: disable=W0212 |
| 138 raise TypeError('%s must be a dict of strings' % prop._name) | 138 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 | 197 |
| 198 | 198 |
| 199 def _validate_tags(prop, value): | 199 def _validate_tags(prop, value): |
| 200 """Validates and sorts TaskRequest.tags.""" | 200 """Validates and sorts TaskRequest.tags.""" |
| 201 if not ':' in value: | 201 if not ':' in value: |
| 202 # pylint: disable=W0212 | 202 # pylint: disable=W0212 |
| 203 raise datastore_errors.BadValueError( | 203 raise datastore_errors.BadValueError( |
| 204 '%s must be key:value form, not %s' % (prop._name, value)) | 204 '%s must be key:value form, not %s' % (prop._name, value)) |
| 205 | 205 |
| 206 | 206 |
| 207 def _validate_package_name(prop, value): | 207 def _validate_package_name_template(prop, value): |
| 208 """Validates a CIPD package name.""" | 208 """Validates a CIPD package name template.""" |
| 209 if not cipd.is_valid_package_name(value): | 209 if not cipd.is_valid_package_name_template(value): |
| 210 raise datastore_errors.BadValueError( | 210 raise datastore_errors.BadValueError( |
| 211 '%s must be a valid CIPD package name "%s"' % (prop._name, value)) | 211 '%s must be a valid CIPD package name template "%s"' % ( |
| 212 prop._name, value)) |
| 212 | 213 |
| 213 | 214 |
| 214 def _validate_package_version(prop, value): | 215 def _validate_package_version(prop, value): |
| 215 """Validates a CIPD package version.""" | 216 """Validates a CIPD package version.""" |
| 216 if not cipd.is_valid_version(value): | 217 if not cipd.is_valid_version(value): |
| 217 raise datastore_errors.BadValueError( | 218 raise datastore_errors.BadValueError( |
| 218 '%s must be a valid package version "%s"' % (prop._name, value)) | 219 '%s must be a valid package version "%s"' % (prop._name, value)) |
| 219 | 220 |
| 220 | 221 |
| 221 ### Models. | 222 ### Models. |
| 222 | 223 |
| 223 | 224 |
| 224 class FilesRef(ndb.Model): | 225 class FilesRef(ndb.Model): |
| 225 """Defines a data tree reference, normally a reference to a .isolated file.""" | 226 """Defines a data tree reference, normally a reference to a .isolated file.""" |
| 226 | 227 |
| 227 # TODO(maruel): make this class have one responsibility. Currently it is used | 228 # TODO(maruel): make this class have one responsibility. Currently it is used |
| 228 # in two modes: | 229 # in two modes: |
| 229 # - a reference to a tree, as class docstring says. | 230 # - a reference to a tree, as class docstring says. |
| 230 # - input/output settings in TaskProperties. | 231 # - input/output settings in TaskProperties. |
| 231 | 232 |
| 232 # The hash of an isolated archive. | 233 # The hash of an isolated archive. |
| 233 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) | 234 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) |
| 234 # The hostname of the isolated server to use. | 235 # The hostname of the isolated server to use. |
| 235 isolatedserver = ndb.StringProperty( | 236 isolatedserver = ndb.StringProperty( |
| 236 validator=_validate_hostname, indexed=False) | 237 validator=_validate_url, indexed=False) |
| 237 # Namespace on the isolate server. | 238 # Namespace on the isolate server. |
| 238 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) | 239 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) |
| 239 | 240 |
| 240 def _pre_put_hook(self): | 241 def _pre_put_hook(self): |
| 241 super(FilesRef, self)._pre_put_hook() | 242 super(FilesRef, self)._pre_put_hook() |
| 242 if not self.isolatedserver or not self.namespace: | 243 if not self.isolatedserver or not self.namespace: |
| 243 raise datastore_errors.BadValueError( | 244 raise datastore_errors.BadValueError( |
| 244 'isolate server and namespace are required') | 245 'isolate server and namespace are required') |
| 245 | 246 |
| 246 | 247 |
| 247 class CipdPackage(ndb.Model): | 248 class CipdPackage(ndb.Model): |
| 248 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. | 249 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. |
| 249 | 250 |
| 250 A part of TaskProperties. | 251 A part of TaskProperties. |
| 251 """ | 252 """ |
| 253 # Package name template. May use cipd.ALL_PARAMS. |
| 254 # Most users will specify ${platform} parameter. |
| 252 package_name = ndb.StringProperty( | 255 package_name = ndb.StringProperty( |
| 253 indexed=False, validator=_validate_package_name) | 256 indexed=False, validator=_validate_package_name_template) |
| 257 # Package version that is valid for all packages matched by package_name. |
| 258 # Most users will specify tags. |
| 254 version = ndb.StringProperty( | 259 version = ndb.StringProperty( |
| 255 indexed=False, validator=_validate_package_version) | 260 indexed=False, validator=_validate_package_version) |
| 256 | 261 |
| 257 def _pre_put_hook(self): | 262 def _pre_put_hook(self): |
| 258 super(CipdPackage, self)._pre_put_hook() | 263 super(CipdPackage, self)._pre_put_hook() |
| 259 if not self.package_name: | 264 if not self.package_name: |
| 260 raise datastore_errors.BadValueError('CIPD package name is required') | 265 raise datastore_errors.BadValueError('CIPD package name is required') |
| 261 if not self.version: | 266 if not self.version: |
| 262 raise datastore_errors.BadValueError('CIPD package version is required') | 267 raise datastore_errors.BadValueError('CIPD package version is required') |
| 263 | 268 |
| 264 | 269 |
| 270 class CipdInput(ndb.Model): |
| 271 """Specifies which CIPD client and packages to install, from which server. |
| 272 |
| 273 A part of TaskProperties. |
| 274 """ |
| 275 # URL of the CIPD server. Must start with "https://" or "http://". |
| 276 server = ndb.StringProperty(indexed=False, validator=_validate_url) |
| 277 |
| 278 # CIPD package of CIPD client to use. |
| 279 # client_package.version is required. |
| 280 client_package = ndb.LocalStructuredProperty(CipdPackage) |
| 281 |
| 282 # List of packages to install in $CIPD_PATH prior task execution. |
| 283 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) |
| 284 |
| 285 def _pre_put_hook(self): |
| 286 if not self.server: |
| 287 raise datastore_errors.BadValueError('cipd server is required') |
| 288 if not self.client_package: |
| 289 raise datastore_errors.BadValueError('client_package is required') |
| 290 self.client_package._pre_put_hook() |
| 291 |
| 292 if not self.packages: |
| 293 raise datastore_errors.BadValueError( |
| 294 'cipd_input cannot have an empty package list') |
| 295 |
| 296 package_names = set() |
| 297 for p in self.packages: |
| 298 p._pre_put_hook() |
| 299 if p.package_name in package_names: |
| 300 raise datastore_errors.BadValueError( |
| 301 'package %s is specified more than once' % p.package_name) |
| 302 package_names.add(p.package_name) |
| 303 self.packages.sort(key=lambda p: p.package_name) |
| 304 |
| 305 |
| 265 class TaskProperties(ndb.Model): | 306 class TaskProperties(ndb.Model): |
| 266 """Defines all the properties of a task to be run on the Swarming | 307 """Defines all the properties of a task to be run on the Swarming |
| 267 infrastructure. | 308 infrastructure. |
| 268 | 309 |
| 269 This entity is not saved in the DB as a standalone entity, instead it is | 310 This entity is not saved in the DB as a standalone entity, instead it is |
| 270 embedded in a TaskRequest. | 311 embedded in a TaskRequest. |
| 271 | 312 |
| 272 This model is immutable. | 313 This model is immutable. |
| 273 | 314 |
| 274 New-style TaskProperties supports invocation of run_isolated. When this | 315 New-style TaskProperties supports invocation of run_isolated. When this |
| (...skipping 21 matching lines...) Expand all Loading... |
| 296 | 337 |
| 297 # Isolate server, namespace and input isolate hash. | 338 # Isolate server, namespace and input isolate hash. |
| 298 # | 339 # |
| 299 # Despite its name, contains isolate server URL and namespace for isolated | 340 # Despite its name, contains isolate server URL and namespace for isolated |
| 300 # output too. See TODO at the top of this class. | 341 # output too. See TODO at the top of this class. |
| 301 # May be non-None even if task input is not isolated. | 342 # May be non-None even if task input is not isolated. |
| 302 # | 343 # |
| 303 # Only inputs_ref.isolated or command can be specified. | 344 # Only inputs_ref.isolated or command can be specified. |
| 304 inputs_ref = ndb.LocalStructuredProperty(FilesRef) | 345 inputs_ref = ndb.LocalStructuredProperty(FilesRef) |
| 305 | 346 |
| 306 # A list of CIPD packages to install $CIPD_PATH and $PATH before task | 347 # CIPD packages to install. |
| 307 # execution. | 348 cipd_input = ndb.LocalStructuredProperty(CipdInput) |
| 308 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) | |
| 309 | 349 |
| 310 # Filter to use to determine the required properties on the bot to run on. For | 350 # Filter to use to determine the required properties on the bot to run on. For |
| 311 # example, Windows or hostname. Encoded as json. Optional but highly | 351 # example, Windows or hostname. Encoded as json. Optional but highly |
| 312 # recommended. | 352 # recommended. |
| 313 dimensions = datastore_utils.DeterministicJsonProperty( | 353 dimensions = datastore_utils.DeterministicJsonProperty( |
| 314 validator=_validate_dimensions, json_type=dict, indexed=False) | 354 validator=_validate_dimensions, json_type=dict, indexed=False) |
| 315 | 355 |
| 316 # Environment variables. Encoded as json. Optional. | 356 # Environment variables. Encoded as json. Optional. |
| 317 env = datastore_utils.DeterministicJsonProperty( | 357 env = datastore_utils.DeterministicJsonProperty( |
| 318 validator=_validate_dict_of_strings, json_type=dict, indexed=False) | 358 validator=_validate_dict_of_strings, json_type=dict, indexed=False) |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 385 isolated_input = self.inputs_ref and self.inputs_ref.isolated | 425 isolated_input = self.inputs_ref and self.inputs_ref.isolated |
| 386 if bool(self.command) == bool(isolated_input): | 426 if bool(self.command) == bool(isolated_input): |
| 387 raise datastore_errors.BadValueError( | 427 raise datastore_errors.BadValueError( |
| 388 'use one of command or inputs_ref.isolated') | 428 'use one of command or inputs_ref.isolated') |
| 389 if self.extra_args and not isolated_input: | 429 if self.extra_args and not isolated_input: |
| 390 raise datastore_errors.BadValueError( | 430 raise datastore_errors.BadValueError( |
| 391 'extra_args require inputs_ref.isolated') | 431 'extra_args require inputs_ref.isolated') |
| 392 if self.inputs_ref: | 432 if self.inputs_ref: |
| 393 self.inputs_ref._pre_put_hook() | 433 self.inputs_ref._pre_put_hook() |
| 394 | 434 |
| 395 package_names = set() | 435 if self.cipd_input: |
| 396 for p in self.packages: | 436 self.cipd_input._pre_put_hook() |
| 397 p._pre_put_hook() | 437 if self.idempotent: |
| 398 if p.package_name in package_names: | 438 pinned = lambda p: cipd.is_pinned_version(p.version) |
| 399 raise datastore_errors.BadValueError( | 439 assert self.cipd_input.packages # checked by cipd_input._pre_put_hook |
| 400 'package %s is specified more than once' % p.package_name) | 440 if any(not pinned(p) for p in self.cipd_input.packages): |
| 401 package_names.add(p.package_name) | 441 raise datastore_errors.BadValueError( |
| 402 self.packages.sort(key=lambda p: p.package_name) | 442 'an idempotent task cannot have unpinned packages; ' |
| 403 | 443 'use tags or instance IDs as package versions') |
| 404 if self.idempotent: | |
| 405 pinned = lambda p: cipd.is_pinned_version(p.version) | |
| 406 if self.packages and any(not pinned(p) for p in self.packages): | |
| 407 raise datastore_errors.BadValueError( | |
| 408 'an idempotent task cannot have unpinned packages; ' | |
| 409 'use instance IDs or tags as package versions') | |
| 410 | |
| 411 | 444 |
| 412 class TaskRequest(ndb.Model): | 445 class TaskRequest(ndb.Model): |
| 413 """Contains a user request. | 446 """Contains a user request. |
| 414 | 447 |
| 415 Key id is a decreasing integer based on time since utils.EPOCH plus some | 448 Key id is a decreasing integer based on time since utils.EPOCH plus some |
| 416 randomness on lower order bits. See _new_request_key() for the complete gory | 449 randomness on lower order bits. See _new_request_key() for the complete gory |
| 417 details. | 450 details. |
| 418 | 451 |
| 419 There is also "old style keys" which inherit from a fake root entity | 452 There is also "old style keys" which inherit from a fake root entity |
| 420 TaskRequestShard. | 453 TaskRequestShard. |
| (...skipping 302 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 723 _put_request(request) | 756 _put_request(request) |
| 724 return request | 757 return request |
| 725 | 758 |
| 726 | 759 |
| 727 def validate_priority(priority): | 760 def validate_priority(priority): |
| 728 """Throws ValueError if priority is not a valid value.""" | 761 """Throws ValueError if priority is not a valid value.""" |
| 729 if 0 > priority or MAXIMUM_PRIORITY < priority: | 762 if 0 > priority or MAXIMUM_PRIORITY < priority: |
| 730 raise datastore_errors.BadValueError( | 763 raise datastore_errors.BadValueError( |
| 731 'priority (%d) must be between 0 and %d (inclusive)' % | 764 'priority (%d) must be between 0 and %d (inclusive)' % |
| 732 (priority, MAXIMUM_PRIORITY)) | 765 (priority, MAXIMUM_PRIORITY)) |
| OLD | NEW |