Chromium Code Reviews| Index: appengine/swarming/server/task_request.py |
| diff --git a/appengine/swarming/server/task_request.py b/appengine/swarming/server/task_request.py |
| index e57a1519661537ec0c87a83c8e937ad6c94d6a2b..90ec9a55d2107c42259c4fd8c477ccbe32003547 100644 |
| --- a/appengine/swarming/server/task_request.py |
| +++ b/appengine/swarming/server/task_request.py |
| @@ -48,7 +48,6 @@ separate entity to clearly declare the boundary for task request deduplication. |
| import datetime |
| import hashlib |
| -import logging |
| import random |
| import re |
| import urlparse |
| @@ -60,6 +59,12 @@ from components import auth |
| from components import datastore_utils |
| from components import pubsub |
| from components import utils |
| + |
| +# Must be after 'components' import, since they add it to sys.path. |
| +from google import protobuf |
| + |
| +from proto import config_pb2 |
| +from server import config |
| from server import task_pack |
| import cipd |
| @@ -94,7 +99,6 @@ _BEGINING_OF_THE_WORLD = datetime.datetime(2010, 1, 1, 0, 0, 0, 0) |
| # Used for isolated files. |
| _HASH_CHARS = frozenset('0123456789abcdef') |
| -NAMESPACE_RE = re.compile(r'^[a-z0-9A-Z\-._]+$') |
| ### Properties validators must come before the models. |
| @@ -125,7 +129,7 @@ def _validate_hostname(prop, value): |
| def _validate_namespace(prop, value): |
| - if not NAMESPACE_RE.match(value): |
| + if not config.NAMESPACE_RE.match(value): |
| raise datastore_errors.BadValueError('malformed %s' % prop._name) |
| @@ -204,18 +208,34 @@ def _validate_tags(prop, value): |
| '%s must be key:value form, not %s' % (prop._name, value)) |
| -def _validate_package_name(prop, value): |
| - """Validates a CIPD package name.""" |
| - if not cipd.is_valid_package_name(value): |
| +def _validate_cipd_settings(prop, value): |
| + """Validates serialized CIPD settings. |
| + |
| + Validates both serialization format and values. |
| + """ |
| + try: |
| + settings = config_pb2.CipdSettings() |
| + settings.MergeFromString(value) # May raise protobuf.message.DecodeError. |
| + config.validate_cipd_settings(settings) # May raise ValueError. |
| + except (ValueError, protobuf.message.DecodeError) as ex: |
| + raise datastore_errors.BadValueError( |
| + '%s must be a valid CipdSettings protobuf message: %s' % |
| + (prop._name, ex)) |
| + |
| + |
| +def _validate_package_name_template(prop, value): |
| + """Validates a CIPD package name template.""" |
| + if not cipd.is_valid_package_name_template(value): |
| raise datastore_errors.BadValueError( |
| - '%s must be a valid CIPD package name "%s"' % (prop._name, value)) |
| + '%s must be a valid CIPD package name template "%s"' % ( |
| + prop._name, value)) |
| def _validate_package_version(prop, value): |
| """Validates a CIPD package version.""" |
| if not cipd.is_valid_version(value): |
| raise datastore_errors.BadValueError( |
| - '%s must be a valid package version "%s"' % (prop._name, value)) |
| + '%s must be a valid package version "%s"' % (prop._name, value)) |
| ### Models. |
| @@ -256,8 +276,12 @@ class CipdPackage(ndb.Model): |
| A part of TaskProperties. |
| """ |
| + # Package name template. May use cipd.ALL_PARAMS. |
| + # Most users will specify ${platform} parameter. |
| package_name = ndb.StringProperty( |
| - indexed=False, validator=_validate_package_name) |
| + indexed=False, validator=_validate_package_name_template) |
| + # Package version that is valid for all packages matched by package_name. |
| + # Most users will specify tags. |
| version = ndb.StringProperty( |
| indexed=False, validator=_validate_package_version) |
| @@ -269,6 +293,39 @@ class CipdPackage(ndb.Model): |
| raise datastore_errors.BadValueError('CIPD package version is required') |
| +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
|
| + """Specifies which CIPD client and packages to install, from which server. |
| + |
| + A part of TaskProperties. |
| + """ |
| + # Serialized config_pb2.CipdSettings message. |
| + # We keep it here to avoid depending on external mutable state. |
| + # At the time of writing, it takes 120 bytes. |
| + settings = ndb.BlobProperty(validator=_validate_cipd_settings) |
| + |
| + # List of packages to install in $CIPD_PATH prior task execution. |
| + packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) |
| + |
| + def _pre_put_hook(self): |
| + if not self.settings: |
| + raise datastore_errors.BadValueError( |
| + 'cipd_input settings are required; ' |
| + 'either specify them explicitly or configure defaults on the server.') |
| + |
| + if not self.packages: |
| + raise datastore_errors.BadValueError( |
| + 'cipd_input cannot have an empty package list') |
| + |
| + package_names = set() |
| + for p in self.packages: |
| + p._pre_put_hook() |
| + if p.package_name in package_names: |
| + raise datastore_errors.BadValueError( |
| + 'package %s is specified more than once' % p.package_name) |
| + package_names.add(p.package_name) |
| + self.packages.sort(key=lambda p: p.package_name) |
| + |
| + |
| class TaskProperties(ndb.Model): |
| """Defines all the properties of a task to be run on the Swarming |
| infrastructure. |
| @@ -310,9 +367,8 @@ class TaskProperties(ndb.Model): |
| # Only inputs_ref.isolated or command&data can be specified. |
| inputs_ref = ndb.LocalStructuredProperty(FilesRef) |
| - # A list of CIPD packages to install $CIPD_PATH and $PATH before task |
| - # execution. |
| - packages = ndb.LocalStructuredProperty(CipdPackage, repeated=True) |
| + # CIPD packages to install. |
| + cipd_input = ndb.LocalStructuredProperty(CipdInput) |
| # Filter to use to determine the required properties on the bot to run on. For |
| # example, Windows or hostname. Encoded as json. Optional but highly |
| @@ -400,22 +456,15 @@ class TaskProperties(ndb.Model): |
| self.inputs_ref.is_ref = False |
| self.inputs_ref._pre_put_hook() |
| - package_names = set() |
| - for p in self.packages: |
| - p._pre_put_hook() |
| - if p.package_name in package_names: |
| - raise datastore_errors.BadValueError( |
| - 'package %s is specified more than once' % p.package_name) |
| - package_names.add(p.package_name) |
| - self.packages.sort(key=lambda p: p.package_name) |
| - |
| - if self.idempotent: |
| - pinned = lambda p: cipd.is_pinned_version(p.version) |
| - if self.packages and any(not pinned(p) for p in self.packages): |
| - raise datastore_errors.BadValueError( |
| - 'an idempotent task cannot have unpinned packages; ' |
| - 'use instance IDs or tags as package versions') |
| - |
| + if self.cipd_input: |
| + self.cipd_input._pre_put_hook() |
| + if self.idempotent: |
| + pinned = lambda p: cipd.is_pinned_version(p.version) |
| + assert self.cipd_input.packages # checked by cipd_input._pre_put_hook |
| + if any(not pinned(p) for p in self.cipd_input.packages): |
| + raise datastore_errors.BadValueError( |
| + 'an idempotent task cannot have unpinned packages; ' |
| + 'use tags or instance IDs as package versions') |
| class TaskRequest(ndb.Model): |
| """Contains a user request. |