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

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: rebased 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 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
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
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
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
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
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))
OLDNEW
« no previous file with comments | « appengine/swarming/server/config_test.py ('k') | appengine/swarming/server/task_request_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698