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

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: simplify cipd server/client embedding in task properties 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 # Specifies FilesRef mode. 228 # Specifies FilesRef mode.
228 # if True, this instance is a reference to a tree, as class docstring says. 229 # if True, this instance is a reference to a tree, as class docstring says.
229 # if False, this instance is isolate input/output settings in TaskProperties. 230 # if False, this instance is isolate input/output settings in TaskProperties.
230 # TODO(maruel): refactor this class, get rid of this. 231 # TODO(maruel): refactor this class, get rid of this.
231 is_ref = True 232 is_ref = True
232 233
233 # if is_ref, the hash of an isolated archive. 234 # if is_ref, the hash of an isolated archive.
234 # otherwise, the hash of the input isolated archive. 235 # otherwise, the hash of the input isolated archive.
235 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False) 236 isolated = ndb.StringProperty(validator=_validate_isolated, indexed=False)
236 # The hostname of the isolated server to use. 237 # The hostname of the isolated server to use.
237 isolatedserver = ndb.StringProperty( 238 isolatedserver = ndb.StringProperty(
238 validator=_validate_hostname, indexed=False) 239 validator=_validate_url, indexed=False)
239 # Namespace on the isolate server. 240 # Namespace on the isolate server.
240 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False) 241 namespace = ndb.StringProperty(validator=_validate_namespace, indexed=False)
241 242
242 def _pre_put_hook(self): 243 def _pre_put_hook(self):
243 super(FilesRef, self)._pre_put_hook() 244 super(FilesRef, self)._pre_put_hook()
244 if self.is_ref: 245 if self.is_ref:
245 if not self.isolated or not self.isolatedserver or not self.namespace: 246 if not self.isolated or not self.isolatedserver or not self.namespace:
246 raise datastore_errors.BadValueError( 247 raise datastore_errors.BadValueError(
247 'isolated requires server and namespace') 248 'isolated requires server and namespace')
248 else: 249 else:
249 if not self.isolatedserver or not self.namespace: 250 if not self.isolatedserver or not self.namespace:
250 raise datastore_errors.BadValueError( 251 raise datastore_errors.BadValueError(
251 'isolate server and namespace are required') 252 'isolate server and namespace are required')
252 253
253 254
254 class CipdPackage(ndb.Model): 255 class CipdPackage(ndb.Model):
255 """A CIPD package to install in $CIPD_PATH and $PATH before task execution. 256 """A CIPD package to install in $CIPD_PATH and $PATH before task execution.
256 257
257 A part of TaskProperties. 258 A part of TaskProperties.
258 """ 259 """
260 # Package name template. May use cipd.ALL_PARAMS.
261 # Most users will specify ${platform} parameter.
259 package_name = ndb.StringProperty( 262 package_name = ndb.StringProperty(
260 indexed=False, validator=_validate_package_name) 263 indexed=False, validator=_validate_package_name_template)
264 # Package version that is valid for all packages matched by package_name.
265 # Most users will specify tags.
261 version = ndb.StringProperty( 266 version = ndb.StringProperty(
262 indexed=False, validator=_validate_package_version) 267 indexed=False, validator=_validate_package_version)
263 268
264 def _pre_put_hook(self): 269 def _pre_put_hook(self):
265 super(CipdPackage, self)._pre_put_hook() 270 super(CipdPackage, self)._pre_put_hook()
266 if not self.package_name: 271 if not self.package_name:
267 raise datastore_errors.BadValueError('CIPD package name is required') 272 raise datastore_errors.BadValueError('CIPD package name is required')
268 if not self.version: 273 if not self.version:
269 raise datastore_errors.BadValueError('CIPD package version is required') 274 raise datastore_errors.BadValueError('CIPD package version is required')
270 275
271 276
277 class CipdInput(ndb.Model):
278 """Specifies which CIPD client and packages to install, from which server.
279
280 A part of TaskProperties.
281 """
282 # URL of the CIPD server. Must start with "https://" or "http://".
283 server = ndb.StringProperty(indexed=False, validator=_validate_url)
284
285 # CIPD package of CIPD client to use.
286 # client_package.version is required.
287 client_package = ndb.LocalStructuredProperty(CipdPackage)
288
289 # List of packages to install in $CIPD_PATH prior task execution.
290 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True)
291
292 def _pre_put_hook(self):
293 if not self.server:
294 raise datastore_errors.BadValueError('cipd server is required')
295 if not self.client_package:
296 raise datastore_errors.BadValueError('client_package is required')
297 self.client_package._pre_put_hook()
298
299 if not self.packages:
300 raise datastore_errors.BadValueError(
301 'cipd_input cannot have an empty package list')
302
303 package_names = set()
304 for p in self.packages:
305 p._pre_put_hook()
306 if p.package_name in package_names:
307 raise datastore_errors.BadValueError(
308 'package %s is specified more than once' % p.package_name)
309 package_names.add(p.package_name)
310 self.packages.sort(key=lambda p: p.package_name)
311
312
272 class TaskProperties(ndb.Model): 313 class TaskProperties(ndb.Model):
273 """Defines all the properties of a task to be run on the Swarming 314 """Defines all the properties of a task to be run on the Swarming
274 infrastructure. 315 infrastructure.
275 316
276 This entity is not saved in the DB as a standalone entity, instead it is 317 This entity is not saved in the DB as a standalone entity, instead it is
277 embedded in a TaskRequest. 318 embedded in a TaskRequest.
278 319
279 This model is immutable. 320 This model is immutable.
280 321
281 New-style TaskProperties supports invocation of run_isolated. When this 322 New-style TaskProperties supports invocation of run_isolated. When this
(...skipping 21 matching lines...) Expand all
303 344
304 # Isolate server, namespace and input isolate hash. 345 # Isolate server, namespace and input isolate hash.
305 # 346 #
306 # Despite its name, contains isolate server URL and namespace for isolated 347 # Despite its name, contains isolate server URL and namespace for isolated
307 # output too. See TODO at the top of this class. 348 # output too. See TODO at the top of this class.
308 # May be non-None even if task input is not isolated. 349 # May be non-None even if task input is not isolated.
309 # 350 #
310 # Only inputs_ref.isolated or command&data can be specified. 351 # Only inputs_ref.isolated or command&data can be specified.
311 inputs_ref = ndb.LocalStructuredProperty(FilesRef) 352 inputs_ref = ndb.LocalStructuredProperty(FilesRef)
312 353
313 # A list of CIPD packages to install $CIPD_PATH and $PATH before task 354 # CIPD packages to install.
314 # execution. 355 cipd_input = ndb.LocalStructuredProperty(CipdInput)
315 packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True)
316 356
317 # Filter to use to determine the required properties on the bot to run on. For 357 # 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 358 # example, Windows or hostname. Encoded as json. Optional but highly
319 # recommended. 359 # recommended.
320 dimensions = datastore_utils.DeterministicJsonProperty( 360 dimensions = datastore_utils.DeterministicJsonProperty(
321 validator=_validate_dimensions, json_type=dict, indexed=False) 361 validator=_validate_dimensions, json_type=dict, indexed=False)
322 362
323 # Environment variables. Encoded as json. Optional. 363 # Environment variables. Encoded as json. Optional.
324 env = datastore_utils.DeterministicJsonProperty( 364 env = datastore_utils.DeterministicJsonProperty(
325 validator=_validate_dict_of_strings, json_type=dict, indexed=False) 365 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): 433 if bool(self.command) == bool(isolated_input):
394 raise datastore_errors.BadValueError( 434 raise datastore_errors.BadValueError(
395 'use one of command or inputs_ref.isolated') 435 'use one of command or inputs_ref.isolated')
396 if self.extra_args and not isolated_input: 436 if self.extra_args and not isolated_input:
397 raise datastore_errors.BadValueError( 437 raise datastore_errors.BadValueError(
398 'extra_args require inputs_ref.isolated') 438 'extra_args require inputs_ref.isolated')
399 if self.inputs_ref: 439 if self.inputs_ref:
400 self.inputs_ref.is_ref = False 440 self.inputs_ref.is_ref = False
401 self.inputs_ref._pre_put_hook() 441 self.inputs_ref._pre_put_hook()
402 442
403 package_names = set() 443 if self.cipd_input:
404 for p in self.packages: 444 self.cipd_input._pre_put_hook()
405 p._pre_put_hook() 445 if self.idempotent:
406 if p.package_name in package_names: 446 pinned = lambda p: cipd.is_pinned_version(p.version)
407 raise datastore_errors.BadValueError( 447 assert self.cipd_input.packages # checked by cipd_input._pre_put_hook
408 'package %s is specified more than once' % p.package_name) 448 if any(not pinned(p) for p in self.cipd_input.packages):
409 package_names.add(p.package_name) 449 raise datastore_errors.BadValueError(
410 self.packages.sort(key=lambda p: p.package_name) 450 'an idempotent task cannot have unpinned packages; '
411 451 '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 452
420 class TaskRequest(ndb.Model): 453 class TaskRequest(ndb.Model):
421 """Contains a user request. 454 """Contains a user request.
422 455
423 Key id is a decreasing integer based on time since utils.EPOCH plus some 456 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 457 randomness on lower order bits. See _new_request_key() for the complete gory
425 details. 458 details.
426 459
427 There is also "old style keys" which inherit from a fake root entity 460 There is also "old style keys" which inherit from a fake root entity
428 TaskRequestShard. 461 TaskRequestShard.
(...skipping 302 matching lines...) Expand 10 before | Expand all | Expand 10 after
731 _put_request(request) 764 _put_request(request)
732 return request 765 return request
733 766
734 767
735 def validate_priority(priority): 768 def validate_priority(priority):
736 """Throws ValueError if priority is not a valid value.""" 769 """Throws ValueError if priority is not a valid value."""
737 if 0 > priority or MAXIMUM_PRIORITY < priority: 770 if 0 > priority or MAXIMUM_PRIORITY < priority:
738 raise datastore_errors.BadValueError( 771 raise datastore_errors.BadValueError(
739 'priority (%d) must be between 0 and %d (inclusive)' % 772 'priority (%d) must be between 0 and %d (inclusive)' %
740 (priority, MAXIMUM_PRIORITY)) 773 (priority, MAXIMUM_PRIORITY))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698