Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(550)

Side by Side Diff: appengine/swarming/server/task_request.py

Issue 1946253003: swarming: refactor cipd input (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@default-isolate-server
Patch Set: fix import google.protobuf Created 4 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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
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
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
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
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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698