App Engine Python SDK version 1.9.12
[gae.git] / python / google / appengine / api / appinfo.py
blobc4a1e05818084ffa9160450309da23fabf07327c
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 """AppInfo tools.
23 Library for working with AppInfo records in memory, store and load from
24 configuration files.
25 """
41 import logging
42 import os
43 import re
44 import string
45 import sys
46 import wsgiref.util
48 if os.environ.get('APPENGINE_RUNTIME') == 'python27':
49 from google.appengine.api import pagespeedinfo
50 from google.appengine.api import validation
51 from google.appengine.api import yaml_builder
52 from google.appengine.api import yaml_listener
53 from google.appengine.api import yaml_object
54 else:
56 from google.appengine.api import pagespeedinfo
57 from google.appengine.api import validation
58 from google.appengine.api import yaml_builder
59 from google.appengine.api import yaml_listener
60 from google.appengine.api import yaml_object
62 from google.appengine.api import appinfo_errors
63 from google.appengine.api import backendinfo
68 _URL_REGEX = r'(?!\^)/.*|\..*|(\(.).*(?!\$).'
69 _FILES_REGEX = r'.+'
70 _URL_ROOT_REGEX = r'/.*'
73 _DELTA_REGEX = r'([0-9]+)([DdHhMm]|[sS]?)'
74 _EXPIRATION_REGEX = r'\s*(%s)(\s+%s)*\s*' % (_DELTA_REGEX, _DELTA_REGEX)
75 _START_PATH = '/_ah/start'
80 _ALLOWED_SERVICES = ['mail', 'mail_bounce', 'xmpp_message', 'xmpp_subscribe',
81 'xmpp_presence', 'xmpp_error', 'channel_presence', 'rest',
82 'warmup']
83 _SERVICE_RE_STRING = '(' + '|'.join(_ALLOWED_SERVICES) + ')'
86 _PAGE_NAME_REGEX = r'^.+$'
89 _EXPIRATION_CONVERSIONS = {
90 'd': 60 * 60 * 24,
91 'h': 60 * 60,
92 'm': 60,
93 's': 1,
98 APP_ID_MAX_LEN = 100
99 MODULE_ID_MAX_LEN = 63
103 MODULE_VERSION_ID_MAX_LEN = 63
104 MAX_URL_MAPS = 100
107 PARTITION_SEPARATOR = '~'
110 DOMAIN_SEPARATOR = ':'
113 VERSION_SEPARATOR = '.'
116 MODULE_SEPARATOR = ':'
119 DEFAULT_MODULE = 'default'
123 PARTITION_RE_STRING_WITHOUT_SEPARATOR = (r'[a-z\d\-]{1,%d}' % APP_ID_MAX_LEN)
124 PARTITION_RE_STRING = (r'%s\%s' %
125 (PARTITION_RE_STRING_WITHOUT_SEPARATOR,
126 PARTITION_SEPARATOR))
127 DOMAIN_RE_STRING_WITHOUT_SEPARATOR = (r'(?!\-)[a-z\d\-\.]{1,%d}' %
128 APP_ID_MAX_LEN)
129 DOMAIN_RE_STRING = (r'%s%s' %
130 (DOMAIN_RE_STRING_WITHOUT_SEPARATOR, DOMAIN_SEPARATOR))
131 DISPLAY_APP_ID_RE_STRING = r'(?!-)[a-z\d\-]{0,%d}[a-z\d]' % (APP_ID_MAX_LEN - 1)
132 APPLICATION_RE_STRING = (r'(?:%s)?(?:%s)?%s' %
133 (PARTITION_RE_STRING,
134 DOMAIN_RE_STRING,
135 DISPLAY_APP_ID_RE_STRING))
144 MODULE_ID_RE_STRING = r'^(?!-)[a-z\d\-]{0,%d}[a-z\d]$' % (MODULE_ID_MAX_LEN - 1)
145 MODULE_VERSION_ID_RE_STRING = (r'^(?!-)[a-z\d\-]{0,%d}[a-z\d]$' %
146 (MODULE_VERSION_ID_MAX_LEN - 1))
148 _IDLE_INSTANCES_REGEX = r'^([\d]+|automatic)$'
150 _INSTANCES_REGEX = r'^[1-9][\d]*$'
151 _INSTANCE_CLASS_REGEX = r'^([fF](1|2|4|4_1G)|[bB](1|2|4|8|4_1G))$'
153 _CONCURRENT_REQUESTS_REGEX = r'^([1-9]\d*)$'
158 _PENDING_LATENCY_REGEX = r'^(\d+((\.\d{1,3})?s|ms)|automatic)$'
160 _IDLE_TIMEOUT_REGEX = r'^[\d]+(s|m)$'
162 ALTERNATE_HOSTNAME_SEPARATOR = '-dot-'
165 BUILTIN_NAME_PREFIX = 'ah-builtin'
167 RUNTIME_RE_STRING = r'[a-z][a-z0-9\-]{0,29}'
169 API_VERSION_RE_STRING = r'[\w.]{1,32}'
171 SOURCE_LANGUAGE_RE_STRING = r'[\w.\-]{1,32}'
173 HANDLER_STATIC_FILES = 'static_files'
174 HANDLER_STATIC_DIR = 'static_dir'
175 HANDLER_SCRIPT = 'script'
176 HANDLER_API_ENDPOINT = 'api_endpoint'
178 LOGIN_OPTIONAL = 'optional'
179 LOGIN_REQUIRED = 'required'
180 LOGIN_ADMIN = 'admin'
182 AUTH_FAIL_ACTION_REDIRECT = 'redirect'
183 AUTH_FAIL_ACTION_UNAUTHORIZED = 'unauthorized'
185 DATASTORE_ID_POLICY_LEGACY = 'legacy'
186 DATASTORE_ID_POLICY_DEFAULT = 'default'
188 SECURE_HTTP = 'never'
189 SECURE_HTTPS = 'always'
190 SECURE_HTTP_OR_HTTPS = 'optional'
192 SECURE_DEFAULT = 'default'
194 REQUIRE_MATCHING_FILE = 'require_matching_file'
196 DEFAULT_SKIP_FILES = (r'^(.*/)?('
197 r'(#.*#)|'
198 r'(.*~)|'
199 r'(.*\.py[co])|'
200 r'(.*/RCS/.*)|'
201 r'(\..*)|'
202 r')$')
204 SKIP_NO_FILES = r'(?!)'
206 DEFAULT_NOBUILD_FILES = (r'^$')
209 LOGIN = 'login'
210 AUTH_FAIL_ACTION = 'auth_fail_action'
211 SECURE = 'secure'
212 URL = 'url'
213 POSITION = 'position'
214 POSITION_HEAD = 'head'
215 POSITION_TAIL = 'tail'
216 STATIC_FILES = 'static_files'
217 UPLOAD = 'upload'
218 STATIC_DIR = 'static_dir'
219 MIME_TYPE = 'mime_type'
220 SCRIPT = 'script'
221 EXPIRATION = 'expiration'
222 API_ENDPOINT = 'api_endpoint'
223 HTTP_HEADERS = 'http_headers'
224 APPLICATION_READABLE = 'application_readable'
225 REDIRECT_HTTP_RESPONSE_CODE = 'redirect_http_response_code'
228 APPLICATION = 'application'
229 MODULE = 'module'
230 AUTOMATIC_SCALING = 'automatic_scaling'
231 MANUAL_SCALING = 'manual_scaling'
232 BASIC_SCALING = 'basic_scaling'
233 VM = 'vm'
234 VM_SETTINGS = 'vm_settings'
235 VM_HEALTH_CHECK = 'vm_health_check'
236 VERSION = 'version'
237 MAJOR_VERSION = 'major_version'
238 MINOR_VERSION = 'minor_version'
239 RUNTIME = 'runtime'
240 API_VERSION = 'api_version'
241 SOURCE_LANGUAGE = 'source_language'
242 BUILTINS = 'builtins'
243 INCLUDES = 'includes'
244 HANDLERS = 'handlers'
245 LIBRARIES = 'libraries'
246 DEFAULT_EXPIRATION = 'default_expiration'
247 SKIP_FILES = 'skip_files'
248 NOBUILD_FILES = 'nobuild_files'
249 SERVICES = 'inbound_services'
250 DERIVED_FILE_TYPE = 'derived_file_type'
251 JAVA_PRECOMPILED = 'java_precompiled'
252 PYTHON_PRECOMPILED = 'python_precompiled'
253 ADMIN_CONSOLE = 'admin_console'
254 ERROR_HANDLERS = 'error_handlers'
255 BACKENDS = 'backends'
256 THREADSAFE = 'threadsafe'
257 DATASTORE_AUTO_ID_POLICY = 'auto_id_policy'
258 API_CONFIG = 'api_config'
259 CODE_LOCK = 'code_lock'
260 ENV_VARIABLES = 'env_variables'
261 PAGESPEED = 'pagespeed'
263 INSTANCE_CLASS = 'instance_class'
265 MINIMUM_PENDING_LATENCY = 'min_pending_latency'
266 MAXIMUM_PENDING_LATENCY = 'max_pending_latency'
267 MINIMUM_IDLE_INSTANCES = 'min_idle_instances'
268 MAXIMUM_IDLE_INSTANCES = 'max_idle_instances'
269 MAXIMUM_CONCURRENT_REQUEST = 'max_concurrent_requests'
274 MIN_NUM_INSTANCES = 'min_num_instances'
275 MAX_NUM_INSTANCES = 'max_num_instances'
276 COOL_DOWN_PERIOD_SEC = 'cool_down_period_sec'
277 CPU_UTILIZATION = 'cpu_utilization'
278 CPU_UTILIZATION_UTILIZATION = 'target_utilization'
279 CPU_UTILIZATION_AGGREGATION_WINDOW_LENGTH_SEC = 'aggregation_window_length_sec'
283 INSTANCES = 'instances'
286 MAX_INSTANCES = 'max_instances'
287 IDLE_TIMEOUT = 'idle_timeout'
290 PAGES = 'pages'
291 NAME = 'name'
294 ERROR_CODE = 'error_code'
295 FILE = 'file'
296 _ERROR_CODE_REGEX = r'(default|over_quota|dos_api_denial|timeout)'
299 ON = 'on'
300 ON_ALIASES = ['yes', 'y', 'True', 't', '1', 'true']
301 OFF = 'off'
302 OFF_ALIASES = ['no', 'n', 'False', 'f', '0', 'false']
307 ENABLE_HEALTH_CHECK = 'enable_health_check'
308 CHECK_INTERVAL_SEC = 'check_interval_sec'
309 TIMEOUT_SEC = 'timeout_sec'
310 UNHEALTHY_THRESHOLD = 'unhealthy_threshold'
311 HEALTHY_THRESHOLD = 'healthy_threshold'
312 RESTART_THRESHOLD = 'restart_threshold'
313 HOST = 'host'
316 class _VersionedLibrary(object):
317 """A versioned library supported by App Engine."""
319 def __init__(self,
320 name,
321 url,
322 description,
323 supported_versions,
324 default_version=None,
325 deprecated_versions=None,
326 experimental_versions=None):
327 """Initializer for _VersionedLibrary.
329 Args:
330 name: The name of the library e.g. "django".
331 url: The URL for the library's project page e.g.
332 "http://www.djangoproject.com/".
333 description: A short description of the library e.g. "A framework...".
334 supported_versions: A list of supported version names ordered by release
335 date e.g. ["v1", "v2", "v3"].
336 default_version: The version of the library that is enabled by default
337 in the Python 2.7 runtime or None if the library is not available by
338 default e.g. "v1".
339 deprecated_versions: A list of the versions of the library that have been
340 deprecated e.g. ["v1", "v2"].
341 experimental_versions: A list of the versions of the library that are
342 current experimental e.g. ["v1"].
344 self.name = name
345 self.url = url
346 self.description = description
347 self.supported_versions = supported_versions
348 self.default_version = default_version
349 self.deprecated_versions = deprecated_versions or []
350 self.experimental_versions = experimental_versions or []
352 @property
353 def non_deprecated_versions(self):
354 return [version for version in self.supported_versions
355 if version not in self.deprecated_versions]
358 _SUPPORTED_LIBRARIES = [
359 _VersionedLibrary(
360 'django',
361 'http://www.djangoproject.com/',
362 'A full-featured web application framework for Python.',
363 ['1.2', '1.3', '1.4', '1.5'],
364 experimental_versions=['1.5'],
366 _VersionedLibrary(
367 'endpoints',
368 'https://developers.google.com/appengine/docs/python/endpoints/',
369 'Libraries for building APIs in an App Engine application.',
370 ['1.0']),
371 _VersionedLibrary(
372 'jinja2',
373 'http://jinja.pocoo.org/docs/',
374 'A modern and designer friendly templating language for Python.',
375 ['2.6']),
376 _VersionedLibrary(
377 'lxml',
378 'http://lxml.de/',
379 'A Pythonic binding for the C libraries libxml2 and libxslt.',
380 ['2.3', '2.3.5'],
381 experimental_versions=['2.3.5'],
383 _VersionedLibrary(
384 'markupsafe',
385 'http://pypi.python.org/pypi/MarkupSafe',
386 'A XML/HTML/XHTML markup safe string for Python.',
387 ['0.15']),
388 _VersionedLibrary(
389 'matplotlib',
390 'http://matplotlib.org/',
391 'A 2D plotting library which produces publication-quality figures.',
392 ['1.2.0'],
393 experimental_versions=['1.2.0'],
395 _VersionedLibrary(
396 'MySQLdb',
397 'http://mysql-python.sourceforge.net/',
398 'A Python DB API v2.0 compatible interface to MySQL.',
399 ['1.2.4b4'],
400 experimental_versions=['1.2.4b4']
402 _VersionedLibrary(
403 'numpy',
404 'http://numpy.scipy.org/',
405 'A general-purpose library for array-processing.',
406 ['1.6.1']),
407 _VersionedLibrary(
408 'PIL',
409 'http://www.pythonware.com/library/pil/handbook/',
410 'A library for creating and transforming images.',
411 ['1.1.7']),
412 _VersionedLibrary(
413 'protorpc',
414 'https://code.google.com/p/google-protorpc/',
415 'A framework for implementing HTTP-based remote procedure call (RPC) '
416 'services.',
417 ['1.0'],
418 default_version='1.0',
420 _VersionedLibrary(
421 'PyAMF',
422 'http://www.pyamf.org/',
423 'A library that provides (AMF) Action Message Format functionality.',
424 ['0.6.1']),
425 _VersionedLibrary(
426 'pycrypto',
427 'https://www.dlitz.net/software/pycrypto/',
428 'A library of cryptogoogle.appengine._internal.graphy functions such as random number generation.',
429 ['2.3', '2.6'],
431 _VersionedLibrary(
432 'setuptools',
433 'http://pypi.python.org/pypi/setuptools',
434 'A library that provides package and module discovery capabilities.',
435 ['0.6c11']),
436 _VersionedLibrary(
437 'ssl',
438 'http://docs.python.org/dev/library/ssl.html',
439 'The SSL socket wrapper built-in module.',
440 ['2.7'],
441 experimental_versions=['2.7']),
442 _VersionedLibrary(
443 'webapp2',
444 'http://webapp-improved.appspot.com/',
445 'A lightweight Python web framework.',
446 ['2.3', '2.5.1', '2.5.2'],
447 default_version='2.3',
448 deprecated_versions=['2.3']
450 _VersionedLibrary(
451 'webob',
452 'http://www.webob.org/',
453 'A library that provides wrappers around the WSGI request environment.',
454 ['1.1.1', '1.2.3'],
455 default_version='1.1.1',
457 _VersionedLibrary(
458 'yaml',
459 'http://www.yaml.org/',
460 'A library for YAML serialization and deserialization.',
461 ['3.10'],
462 default_version='3.10'
466 _NAME_TO_SUPPORTED_LIBRARY = dict((library.name, library)
467 for library in _SUPPORTED_LIBRARIES)
471 REQUIRED_LIBRARIES = {
472 ('jinja2', '2.6'): [('markupsafe', '0.15'), ('setuptools', '0.6c11')],
473 ('jinja2', 'latest'): [('markupsafe', 'latest'), ('setuptools', 'latest')],
474 ('matplotlib', '1.1.1'): [('numpy', '1.6.1')],
475 ('matplotlib', '1.2.0'): [('numpy', '1.6.1')],
476 ('matplotlib', 'latest'): [('numpy', 'latest')],
479 _USE_VERSION_FORMAT = ('use one of: "%s" or "latest" '
480 '("latest" recommended for development only)')
484 _HTTP_SEPARATOR_CHARS = frozenset('()<>@,;:\\"/[]?={} \t')
485 _HTTP_TOKEN_CHARS = frozenset(string.printable[:-5]) - _HTTP_SEPARATOR_CHARS
486 _HTTP_TOKEN_RE = re.compile('[%s]+$' % re.escape(''.join(_HTTP_TOKEN_CHARS)))
489 _HTTP_REQUEST_HEADERS = frozenset([
490 'accept',
491 'accept-charset',
492 'accept-encoding',
493 'accept-language',
494 'authorization',
495 'expect',
496 'from',
497 'host',
498 'if-match',
499 'if-modified-since',
500 'if-none-match',
501 'if-range',
502 'if-unmodified-since',
503 'max-forwards',
504 'proxy-authorization',
505 'range',
506 'referer',
507 'te',
508 'user-agent',
513 _MAX_COOKIE_LENGTH = 4096
517 _MAX_URL_LENGTH = 2047
520 class HandlerBase(validation.Validated):
521 """Base class for URLMap and ApiConfigHandler."""
522 ATTRIBUTES = {
524 URL: validation.Optional(_URL_REGEX),
525 LOGIN: validation.Options(LOGIN_OPTIONAL,
526 LOGIN_REQUIRED,
527 LOGIN_ADMIN,
528 default=LOGIN_OPTIONAL),
530 AUTH_FAIL_ACTION: validation.Options(AUTH_FAIL_ACTION_REDIRECT,
531 AUTH_FAIL_ACTION_UNAUTHORIZED,
532 default=AUTH_FAIL_ACTION_REDIRECT),
534 SECURE: validation.Options(SECURE_HTTP,
535 SECURE_HTTPS,
536 SECURE_HTTP_OR_HTTPS,
537 SECURE_DEFAULT,
538 default=SECURE_DEFAULT),
541 HANDLER_SCRIPT: validation.Optional(_FILES_REGEX)
545 class HttpHeadersDict(validation.ValidatedDict):
546 """A dict that limits keys and values what http_headers allows.
548 http_headers is an static handler key i.e. it applies to handlers with
549 static_dir or static_files keys. An example of how http_headers is used is
551 handlers:
552 - url: /static
553 static_dir: static
554 http_headers:
555 X-Foo-Header: foo value
556 X-Bar-Header: bar value
560 DISALLOWED_HEADERS = frozenset([
565 'content-encoding',
566 'content-length',
567 'date',
568 'server'
571 MAX_HEADER_LENGTH = 500
572 MAX_HEADER_VALUE_LENGTHS = {
573 'set-cookie': _MAX_COOKIE_LENGTH,
574 'set-cookie2': _MAX_COOKIE_LENGTH,
575 'location': _MAX_URL_LENGTH}
576 MAX_LEN = 500
578 class KeyValidator(validation.Validator):
579 """Ensures that keys in HttpHeadersDict i.e. header names are valid.
581 An instance is used as HttpHeadersDict's KEY_VALIDATOR.
584 def Validate(self, name, unused_key=None):
585 """Returns argument, or raises an exception if it is invalid.
587 HTTP header names are defined by RFC 2616 section 4.2.
589 Args:
590 name: HTTP header field value.
591 unused_key: Unused.
593 Returns:
594 name argument, unchanged.
596 Raises:
597 appinfo_errors.InvalidHttpHeaderName: argument cannot be used as an HTTP
598 header name.
600 original_name = name
603 if isinstance(name, unicode):
604 try:
605 name = name.encode('ascii')
606 except UnicodeEncodeError:
607 raise appinfo_errors.InvalidHttpHeaderName(
608 'HTTP header values must not contain non-ASCII data')
611 name = name.lower()
613 if not _HTTP_TOKEN_RE.match(name):
614 raise appinfo_errors.InvalidHttpHeaderName(
615 'An HTTP header must be a non-empty RFC 2616 token.')
618 if name in _HTTP_REQUEST_HEADERS:
619 raise appinfo_errors.InvalidHttpHeaderName(
620 '%r can only be used in HTTP requests, not responses.'
621 % original_name)
624 if name.startswith('x-appengine'):
625 raise appinfo_errors.InvalidHttpHeaderName(
626 'HTTP header names that begin with X-Appengine are reserved.')
628 if wsgiref.util.is_hop_by_hop(name):
629 raise appinfo_errors.InvalidHttpHeaderName(
630 'Only use end-to-end headers may be used. See RFC 2616 section'
631 ' 13.5.1.')
633 if name in HttpHeadersDict.DISALLOWED_HEADERS:
634 raise appinfo_errors.InvalidHttpHeaderName(
635 '%s is a disallowed header.' % name)
637 return original_name
639 class ValueValidator(validation.Validator):
640 """Ensures that values in HttpHeadersDict i.e. header values are valid.
642 An instance is used as HttpHeadersDict's VALUE_VALIDATOR.
645 def Validate(self, value, key=None):
646 """Returns value, or raises an exception if it is invalid.
648 According to RFC 2616 section 4.2, header field values must consist "of
649 either *TEXT or combinations of token, separators, and quoted-string".
651 TEXT = <any OCTET except CTLs, but including LWS>
653 Args:
654 value: HTTP header field value.
655 key: HTTP header field name.
657 Returns:
658 value argument.
660 Raises:
661 appinfo_errors.InvalidHttpHeaderValue: argument cannot be used as an
662 HTTP header value.
665 if isinstance(value, unicode):
666 try:
667 value = value.encode('ascii')
668 except UnicodeEncodeError:
669 raise appinfo_errors.InvalidHttpHeaderValue(
670 'HTTP header values must not contain non-ASCII data')
673 key = key.lower()
679 printable = set(string.printable[:-5])
680 if not all(char in printable for char in value):
681 raise appinfo_errors.InvalidHttpHeaderValue(
682 'HTTP header field values must consist of printable characters.')
684 HttpHeadersDict.ValueValidator.AssertHeaderNotTooLong(key, value)
686 return value
688 @staticmethod
689 def AssertHeaderNotTooLong(name, value):
690 header_length = len('%s: %s\r\n' % (name, value))
694 if header_length >= HttpHeadersDict.MAX_HEADER_LENGTH:
698 try:
699 max_len = HttpHeadersDict.MAX_HEADER_VALUE_LENGTHS[name]
700 except KeyError:
701 raise appinfo_errors.InvalidHttpHeaderValue(
702 'HTTP header (name + value) is too long.')
706 if len(value) > max_len:
707 insert = name, len(value), max_len
708 raise appinfo_errors.InvalidHttpHeaderValue(
709 '%r header value has length %d, which exceed the maximum allowed,'
710 ' %d.' % insert)
712 KEY_VALIDATOR = KeyValidator()
713 VALUE_VALIDATOR = ValueValidator()
715 def Get(self, header_name):
716 """Gets a header value.
718 Args:
719 header_name: HTTP header name to look for.
721 Returns:
722 A header value that corresponds to header_name. If more than one such
723 value is in self, one of the values is selected arbitrarily, and
724 returned. The selection is not deterministic.
726 for name in self:
727 if name.lower() == header_name.lower():
728 return self[name]
732 def __setitem__(self, key, value):
733 is_addition = self.Get(key) is None
734 if is_addition and len(self) >= self.MAX_LEN:
735 raise appinfo_errors.TooManyHttpHeaders(
736 'Tried to add another header when the current set of HTTP headers'
737 ' already has the maximum allowed number of headers, %d.'
738 % HttpHeadersDict.MAX_LEN)
739 super(HttpHeadersDict, self).__setitem__(key, value)
742 class URLMap(HandlerBase):
743 """Mapping from URLs to handlers.
745 This class acts like something of a union type. Its purpose is to
746 describe a mapping between a set of URLs and their handlers. What
747 handler type a given instance has is determined by which handler-id
748 attribute is used.
750 Each mapping can have one and only one handler type. Attempting to
751 use more than one handler-id attribute will cause an UnknownHandlerType
752 to be raised during validation. Failure to provide any handler-id
753 attributes will cause MissingHandlerType to be raised during validation.
755 The regular expression used by the url field will be used to match against
756 the entire URL path and query string of the request. This means that
757 partial maps will not be matched. Specifying a url, say /admin, is the
758 same as matching against the regular expression '^/admin$'. Don't begin
759 your matching url with ^ or end them with $. These regular expressions
760 won't be accepted and will raise ValueError.
762 Attributes:
763 login: Whether or not login is required to access URL. Defaults to
764 'optional'.
765 secure: Restriction on the protocol which can be used to serve
766 this URL/handler (HTTP, HTTPS or either).
767 url: Regular expression used to fully match against the request URLs path.
768 See Special Cases for using static_dir.
769 static_files: Handler id attribute that maps URL to the appropriate
770 file. Can use back regex references to the string matched to url.
771 upload: Regular expression used by the application configuration
772 program to know which files are uploaded as blobs. It's very
773 difficult to determine this using just the url and static_files
774 so this attribute must be included. Required when defining a
775 static_files mapping.
776 A matching file name must fully match against the upload regex, similar
777 to how url is matched against the request path. Do not begin upload
778 with ^ or end it with $.
779 static_dir: Handler id that maps the provided url to a sub-directory
780 within the application directory. See Special Cases.
781 mime_type: When used with static_files and static_dir the mime-type
782 of files served from those directories are overridden with this
783 value.
784 script: Handler id that maps URLs to scipt handler within the application
785 directory that will run using CGI.
786 position: Used in AppInclude objects to specify whether a handler
787 should be inserted at the beginning of the primary handler list or at the
788 end. If 'tail' is specified, the handler is inserted at the end,
789 otherwise, the handler is inserted at the beginning. This means that
790 'head' is the effective default.
791 expiration: When used with static files and directories, the time delta to
792 use for cache expiration. Has the form '4d 5h 30m 15s', where each letter
793 signifies days, hours, minutes, and seconds, respectively. The 's' for
794 seconds may be omitted. Only one amount must be specified, combining
795 multiple amounts is optional. Example good values: '10', '1d 6h',
796 '1h 30m', '7d 7d 7d', '5m 30'.
797 api_endpoint: Handler id that identifies endpoint as an API endpoint,
798 calls that terminate here will be handled by the api serving framework.
800 Special cases:
801 When defining a static_dir handler, do not use a regular expression
802 in the url attribute. Both the url and static_dir attributes are
803 automatically mapped to these equivalents:
805 <url>/(.*)
806 <static_dir>/\1
808 For example:
810 url: /images
811 static_dir: images_folder
813 Is the same as this static_files declaration:
815 url: /images/(.*)
816 static_files: images_folder/\1
817 upload: images_folder/(.*)
819 ATTRIBUTES = {
822 HANDLER_STATIC_FILES: validation.Optional(_FILES_REGEX),
823 UPLOAD: validation.Optional(_FILES_REGEX),
824 APPLICATION_READABLE: validation.Optional(bool),
827 HANDLER_STATIC_DIR: validation.Optional(_FILES_REGEX),
830 MIME_TYPE: validation.Optional(str),
831 EXPIRATION: validation.Optional(_EXPIRATION_REGEX),
832 REQUIRE_MATCHING_FILE: validation.Optional(bool),
833 HTTP_HEADERS: validation.Optional(HttpHeadersDict),
836 POSITION: validation.Optional(validation.Options(POSITION_HEAD,
837 POSITION_TAIL)),
840 HANDLER_API_ENDPOINT: validation.Optional(validation.Options(
841 (ON, ON_ALIASES),
842 (OFF, OFF_ALIASES))),
844 REDIRECT_HTTP_RESPONSE_CODE: validation.Optional(validation.Options(
845 '301', '302', '303', '307')),
847 ATTRIBUTES.update(HandlerBase.ATTRIBUTES)
849 COMMON_FIELDS = set([
850 URL, LOGIN, AUTH_FAIL_ACTION, SECURE, REDIRECT_HTTP_RESPONSE_CODE])
854 ALLOWED_FIELDS = {
855 HANDLER_STATIC_FILES: (MIME_TYPE, UPLOAD, EXPIRATION,
856 REQUIRE_MATCHING_FILE, HTTP_HEADERS,
857 APPLICATION_READABLE),
858 HANDLER_STATIC_DIR: (MIME_TYPE, EXPIRATION, REQUIRE_MATCHING_FILE,
859 HTTP_HEADERS, APPLICATION_READABLE),
860 HANDLER_SCRIPT: (POSITION),
861 HANDLER_API_ENDPOINT: (POSITION, SCRIPT),
864 def GetHandler(self):
865 """Get handler for mapping.
867 Returns:
868 Value of the handler (determined by handler id attribute).
870 return getattr(self, self.GetHandlerType())
872 def GetHandlerType(self):
873 """Get handler type of mapping.
875 Returns:
876 Handler type determined by which handler id attribute is set.
878 Raises:
879 UnknownHandlerType: when none of the no handler id attributes are set.
881 UnexpectedHandlerAttribute: when an unexpected attribute is set for the
882 discovered handler type.
884 HandlerTypeMissingAttribute: when the handler is missing a
885 required attribute for its handler type.
887 MissingHandlerAttribute: when a URL handler is missing an attribute
891 if getattr(self, HANDLER_API_ENDPOINT) is not None:
893 mapping_type = HANDLER_API_ENDPOINT
894 else:
895 for id_field in URLMap.ALLOWED_FIELDS.iterkeys():
897 if getattr(self, id_field) is not None:
899 mapping_type = id_field
900 break
901 else:
903 raise appinfo_errors.UnknownHandlerType(
904 'Unknown url handler type.\n%s' % str(self))
906 allowed_fields = URLMap.ALLOWED_FIELDS[mapping_type]
910 for attribute in self.ATTRIBUTES.iterkeys():
911 if (getattr(self, attribute) is not None and
912 not (attribute in allowed_fields or
913 attribute in URLMap.COMMON_FIELDS or
914 attribute == mapping_type)):
915 raise appinfo_errors.UnexpectedHandlerAttribute(
916 'Unexpected attribute "%s" for mapping type %s.' %
917 (attribute, mapping_type))
922 if mapping_type == HANDLER_STATIC_FILES and not self.upload:
923 raise appinfo_errors.MissingHandlerAttribute(
924 'Missing "%s" attribute for URL "%s".' % (UPLOAD, self.url))
926 return mapping_type
928 def CheckInitialized(self):
929 """Adds additional checking to make sure handler has correct fields.
931 In addition to normal ValidatedCheck calls GetHandlerType
932 which validates all the handler fields are configured
933 properly.
935 Raises:
936 UnknownHandlerType: when none of the no handler id attributes are set.
937 UnexpectedHandlerAttribute: when an unexpected attribute is set for the
938 discovered handler type.
939 HandlerTypeMissingAttribute: when the handler is missing a required
940 attribute for its handler type.
941 ContentTypeSpecifiedMultipleTimes: when mime_type is inconsistent with
942 http_headers.
944 super(URLMap, self).CheckInitialized()
945 if self.GetHandlerType() in (STATIC_DIR, STATIC_FILES):
957 self.AssertUniqueContentType()
959 def AssertUniqueContentType(self):
960 """Makes sure that self.http_headers is consistent with self.mime_type.
962 Assumes self is a static handler i.e. either self.static_dir or
963 self.static_files is set (to not None).
965 Raises:
966 appinfo_errors.ContentTypeSpecifiedMultipleTimes: Raised when
967 self.http_headers contains a Content-Type header, and self.mime_type is
968 set. For example, the following configuration would be rejected:
970 handlers:
971 - url: /static
972 static_dir: static
973 mime_type: text/html
974 http_headers:
975 content-type: text/html
977 As this example shows, a configuration will be rejected when
978 http_headers and mime_type specify a content type, even when they
979 specify the same content type.
981 used_both_fields = self.mime_type and self.http_headers
982 if not used_both_fields:
983 return
985 content_type = self.http_headers.Get('Content-Type')
986 if content_type is not None:
987 raise appinfo_errors.ContentTypeSpecifiedMultipleTimes(
988 'http_header specified a Content-Type header of %r in a handler that'
989 ' also specified a mime_type of %r.' % (content_type, self.mime_type))
991 def FixSecureDefaults(self):
992 """Force omitted 'secure: ...' handler fields to 'secure: optional'.
994 The effect is that handler.secure is never equal to the (nominal)
995 default.
997 See http://b/issue?id=2073962.
999 if self.secure == SECURE_DEFAULT:
1000 self.secure = SECURE_HTTP_OR_HTTPS
1002 def WarnReservedURLs(self):
1003 """Generates a warning for reserved URLs.
1005 See:
1006 https://developers.google.com/appengine/docs/python/config/appconfig#Reserved_URLs
1008 if self.url == '/form':
1009 logging.warning(
1010 'The URL path "/form" is reserved and will not be matched.')
1012 def ErrorOnPositionForAppInfo(self):
1013 """Raises an error if position is specified outside of AppInclude objects.
1015 Raises:
1016 PositionUsedInAppYamlHandler: when position attribute is specified for an
1017 app.yaml file instead of an include.yaml file.
1019 if self.position:
1020 raise appinfo_errors.PositionUsedInAppYamlHandler(
1021 'The position attribute was specified for this handler, but this is '
1022 'an app.yaml file. Position attribute is only valid for '
1023 'include.yaml files.')
1026 class AdminConsolePage(validation.Validated):
1027 """Class representing admin console page in AdminConsole object.
1029 ATTRIBUTES = {
1030 URL: _URL_REGEX,
1031 NAME: _PAGE_NAME_REGEX,
1035 class AdminConsole(validation.Validated):
1036 """Class representing admin console directives in application info.
1038 ATTRIBUTES = {
1039 PAGES: validation.Optional(validation.Repeated(AdminConsolePage)),
1042 @classmethod
1043 def Merge(cls, adminconsole_one, adminconsole_two):
1044 """Return the result of merging two AdminConsole objects."""
1053 if not adminconsole_one or not adminconsole_two:
1054 return adminconsole_one or adminconsole_two
1056 if adminconsole_one.pages:
1057 if adminconsole_two.pages:
1058 adminconsole_one.pages.extend(adminconsole_two.pages)
1059 else:
1060 adminconsole_one.pages = adminconsole_two.pages
1062 return adminconsole_one
1065 class ErrorHandlers(validation.Validated):
1066 """Class representing error handler directives in application info.
1068 ATTRIBUTES = {
1069 ERROR_CODE: validation.Optional(_ERROR_CODE_REGEX),
1070 FILE: _FILES_REGEX,
1071 MIME_TYPE: validation.Optional(str),
1075 class BuiltinHandler(validation.Validated):
1076 """Class representing builtin handler directives in application info.
1078 Permits arbitrary keys but their values must be described by the
1079 validation.Options object returned by ATTRIBUTES.
1109 class DynamicAttributes(dict):
1110 """Provide a dictionary object that will always claim to have a key.
1112 This dictionary returns a fixed value for any get operation. The fixed
1113 value passed in as a constructor parameter should be a
1114 validation.Validated object.
1117 def __init__(self, return_value, **parameters):
1118 self.__return_value = return_value
1119 dict.__init__(self, parameters)
1121 def __contains__(self, _):
1122 return True
1124 def __getitem__(self, _):
1125 return self.__return_value
1127 ATTRIBUTES = DynamicAttributes(
1128 validation.Optional(validation.Options((ON, ON_ALIASES),
1129 (OFF, OFF_ALIASES))))
1131 def __init__(self, **attributes):
1132 """Ensure that all BuiltinHandler objects at least have attribute 'default'.
1134 self.builtin_name = ''
1135 super(BuiltinHandler, self).__init__(**attributes)
1137 def __setattr__(self, key, value):
1138 """Permit ATTRIBUTES.iteritems() to return set of items that have values.
1140 Whenever validate calls iteritems(), it is always called on ATTRIBUTES,
1141 not on __dict__, so this override is important to ensure that functions
1142 such as ToYAML() return the correct set of keys.
1144 Raises:
1145 MultipleBuiltinsSpecified: when more than one builtin is defined in a list
1146 element.
1148 if key == 'builtin_name':
1149 object.__setattr__(self, key, value)
1150 elif not self.builtin_name:
1151 self.ATTRIBUTES[key] = ''
1152 self.builtin_name = key
1153 super(BuiltinHandler, self).__setattr__(key, value)
1154 else:
1159 raise appinfo_errors.MultipleBuiltinsSpecified(
1160 'More than one builtin defined in list element. Each new builtin '
1161 'should be prefixed by "-".')
1163 def __getattr__(self, key):
1164 if key.startswith('_'):
1167 raise AttributeError
1168 return None
1170 def ToDict(self):
1171 """Convert BuiltinHander object to a dictionary.
1173 Returns:
1174 dictionary of the form: {builtin_handler_name: on/off}
1176 return {self.builtin_name: getattr(self, self.builtin_name)}
1178 @classmethod
1179 def IsDefined(cls, builtins_list, builtin_name):
1180 """Find if a builtin is defined in a given list of builtin handler objects.
1182 Args:
1183 builtins_list: list of BuiltinHandler objects (typically yaml.builtins)
1184 builtin_name: name of builtin to find whether or not it is defined
1186 Returns:
1187 true if builtin_name is defined by a member of builtins_list,
1188 false otherwise
1190 for b in builtins_list:
1191 if b.builtin_name == builtin_name:
1192 return True
1193 return False
1195 @classmethod
1196 def ListToTuples(cls, builtins_list):
1197 """Converts a list of BuiltinHandler objects to a list of (name, status)."""
1198 return [(b.builtin_name, getattr(b, b.builtin_name)) for b in builtins_list]
1200 @classmethod
1201 def Validate(cls, builtins_list, runtime=None):
1202 """Verify that all BuiltinHandler objects are valid and not repeated.
1204 Args:
1205 builtins_list: list of BuiltinHandler objects to validate.
1206 runtime: if set then warnings are generated for builtins that have been
1207 deprecated in the given runtime.
1209 Raises:
1210 InvalidBuiltinFormat: if the name of a Builtinhandler object
1211 cannot be determined.
1212 DuplicateBuiltinsSpecified: if a builtin handler name is used
1213 more than once in the list.
1215 seen = set()
1216 for b in builtins_list:
1217 if not b.builtin_name:
1218 raise appinfo_errors.InvalidBuiltinFormat(
1219 'Name of builtin for list object %s could not be determined.'
1220 % b)
1221 if b.builtin_name in seen:
1222 raise appinfo_errors.DuplicateBuiltinsSpecified(
1223 'Builtin %s was specified more than once in one yaml file.'
1224 % b.builtin_name)
1231 if b.builtin_name == 'datastore_admin' and runtime == 'python':
1232 logging.warning(
1233 'The datastore_admin builtin is deprecated. You can find '
1234 'information on how to enable it through the Administrative '
1235 'Console here: '
1236 'http://developers.google.com/appengine/docs/adminconsole/'
1237 'datastoreadmin.html')
1238 elif b.builtin_name == 'mapreduce' and runtime == 'python':
1239 logging.warning(
1240 'The mapreduce builtin is deprecated. You can find more '
1241 'information on how to configure and use it here: '
1242 'http://developers.google.com/appengine/docs/python/dataprocessing/'
1243 'overview.html')
1245 seen.add(b.builtin_name)
1248 class ApiConfigHandler(HandlerBase):
1249 """Class representing api_config handler directives in application info."""
1250 ATTRIBUTES = HandlerBase.ATTRIBUTES
1251 ATTRIBUTES.update({
1253 URL: validation.Regex(_URL_REGEX),
1254 HANDLER_SCRIPT: validation.Regex(_FILES_REGEX)
1258 class Library(validation.Validated):
1259 """Class representing the configuration of a single library."""
1261 ATTRIBUTES = {'name': validation.Type(str),
1262 'version': validation.Type(str)}
1264 def CheckInitialized(self):
1265 """Raises if the library configuration is not valid."""
1266 super(Library, self).CheckInitialized()
1267 if self.name not in _NAME_TO_SUPPORTED_LIBRARY:
1268 raise appinfo_errors.InvalidLibraryName(
1269 'the library "%s" is not supported' % self.name)
1271 supported_library = _NAME_TO_SUPPORTED_LIBRARY[self.name]
1272 if self.version != 'latest':
1273 if self.version not in supported_library.supported_versions:
1274 raise appinfo_errors.InvalidLibraryVersion(
1275 ('%s version "%s" is not supported, ' + _USE_VERSION_FORMAT) % (
1276 self.name,
1277 self.version,
1278 '", "'.join(supported_library.non_deprecated_versions)))
1279 elif self.version in supported_library.deprecated_versions:
1280 logging.warning(
1281 ('%s version "%s" is deprecated, ' + _USE_VERSION_FORMAT) % (
1282 self.name,
1283 self.version,
1284 '", "'.join(supported_library.non_deprecated_versions)))
1287 class CpuUtilization(validation.Validated):
1288 """Class representing the configuration of VM CPU utilization."""
1290 ATTRIBUTES = {
1291 CPU_UTILIZATION_UTILIZATION: validation.Optional(
1292 validation.Range(1e-6, 1.0, float)),
1293 CPU_UTILIZATION_AGGREGATION_WINDOW_LENGTH_SEC: validation.Optional(
1294 validation.Range(1, sys.maxint)),
1298 class AutomaticScaling(validation.Validated):
1299 """Class representing automatic scaling settings in the AppInfoExternal."""
1300 ATTRIBUTES = {
1301 MINIMUM_IDLE_INSTANCES: validation.Optional(_IDLE_INSTANCES_REGEX),
1302 MAXIMUM_IDLE_INSTANCES: validation.Optional(_IDLE_INSTANCES_REGEX),
1303 MINIMUM_PENDING_LATENCY: validation.Optional(_PENDING_LATENCY_REGEX),
1304 MAXIMUM_PENDING_LATENCY: validation.Optional(_PENDING_LATENCY_REGEX),
1305 MAXIMUM_CONCURRENT_REQUEST: validation.Optional(
1306 _CONCURRENT_REQUESTS_REGEX),
1308 MIN_NUM_INSTANCES: validation.Optional(validation.Range(1, sys.maxint)),
1309 MAX_NUM_INSTANCES: validation.Optional(validation.Range(1, sys.maxint)),
1310 COOL_DOWN_PERIOD_SEC: validation.Optional(
1311 validation.Range(60, sys.maxint, int)),
1312 CPU_UTILIZATION: validation.Optional(CpuUtilization),
1316 class ManualScaling(validation.Validated):
1317 """Class representing manual scaling settings in the AppInfoExternal."""
1318 ATTRIBUTES = {
1319 INSTANCES: validation.Regex(_INSTANCES_REGEX),
1323 class BasicScaling(validation.Validated):
1324 """Class representing basic scaling settings in the AppInfoExternal."""
1325 ATTRIBUTES = {
1326 MAX_INSTANCES: validation.Regex(_INSTANCES_REGEX),
1327 IDLE_TIMEOUT: validation.Optional(_IDLE_TIMEOUT_REGEX),
1331 class VmSettings(validation.ValidatedDict):
1332 """Class for VM settings.
1334 We don't validate these further because the feature is in flux.
1337 KEY_VALIDATOR = validation.Regex('[a-zA-Z_][a-zA-Z0-9_]*')
1338 VALUE_VALIDATOR = str
1340 @classmethod
1341 def Merge(cls, vm_settings_one, vm_settings_two):
1343 result_vm_settings = (vm_settings_two or {}).copy()
1347 result_vm_settings.update(vm_settings_one or {})
1348 return VmSettings(**result_vm_settings) if result_vm_settings else None
1351 class EnvironmentVariables(validation.ValidatedDict):
1352 """Class representing a mapping of environment variable key value pairs."""
1354 KEY_VALIDATOR = validation.Regex('[a-zA-Z_][a-zA-Z0-9_]*')
1355 VALUE_VALIDATOR = str
1357 @classmethod
1358 def Merge(cls, env_variables_one, env_variables_two):
1359 """Merges to EnvironmentVariables instances.
1361 Args:
1362 env_variables_one: The first EnvironmentVariables instance or None.
1363 env_variables_two: The second EnvironmentVariables instance or None.
1365 Returns:
1366 The merged EnvironmentVariables instance, or None if both input instances
1367 are None or empty.
1369 If a variable is specified by both instances, the value from
1370 env_variables_two is used.
1373 result_env_variables = (env_variables_one or {}).copy()
1374 result_env_variables.update(env_variables_two or {})
1375 return (EnvironmentVariables(**result_env_variables)
1376 if result_env_variables else None)
1379 def VmSafeSetRuntime(appyaml, runtime):
1380 """Sets the runtime while respecting vm runtimes rules for runtime settings.
1382 Args:
1383 appyaml: AppInfoExternal instance, which will be modified.
1384 runtime: The runtime to use.
1386 Returns:
1387 The passed in appyaml (which has been modified).
1389 if appyaml.vm:
1390 if not appyaml.vm_settings:
1391 appyaml.vm_settings = VmSettings()
1394 if runtime == 'dart' or runtime == 'contrib-dart':
1395 runtime = 'dart'
1396 appyaml.vm_settings['has_docker_image'] = True
1399 appyaml.vm_settings['vm_runtime'] = runtime
1400 appyaml.runtime = 'vm'
1401 else:
1402 appyaml.runtime = runtime
1403 return appyaml
1406 def NormalizeVmSettings(appyaml):
1407 """Normalize Vm settings.
1409 Args:
1410 appyaml: AppInfoExternal instance.
1412 Returns:
1413 Normalized app yaml.
1421 if appyaml.vm:
1422 if not appyaml.vm_settings:
1423 appyaml.vm_settings = VmSettings()
1424 if 'vm_runtime' not in appyaml.vm_settings:
1425 appyaml = VmSafeSetRuntime(appyaml, appyaml.runtime)
1426 return appyaml
1429 class VmHealthCheck(validation.Validated):
1430 """Class representing the configuration of VM health check."""
1432 ATTRIBUTES = {
1433 ENABLE_HEALTH_CHECK: validation.Optional(validation.TYPE_BOOL),
1434 CHECK_INTERVAL_SEC: validation.Optional(validation.Range(0, sys.maxint)),
1435 TIMEOUT_SEC: validation.Optional(validation.Range(0, sys.maxint)),
1436 UNHEALTHY_THRESHOLD: validation.Optional(validation.Range(0, sys.maxint)),
1437 HEALTHY_THRESHOLD: validation.Optional(validation.Range(0, sys.maxint)),
1438 RESTART_THRESHOLD: validation.Optional(validation.Range(0, sys.maxint)),
1439 HOST: validation.Optional(validation.TYPE_STR)}
1442 class AppInclude(validation.Validated):
1443 """Class representing the contents of an included app.yaml file.
1445 Used for both builtins and includes directives.
1451 ATTRIBUTES = {
1452 BUILTINS: validation.Optional(validation.Repeated(BuiltinHandler)),
1453 INCLUDES: validation.Optional(validation.Type(list)),
1454 HANDLERS: validation.Optional(validation.Repeated(URLMap), default=[]),
1455 ADMIN_CONSOLE: validation.Optional(AdminConsole),
1456 MANUAL_SCALING: validation.Optional(ManualScaling),
1457 VM: validation.Optional(bool),
1458 VM_SETTINGS: validation.Optional(VmSettings),
1459 ENV_VARIABLES: validation.Optional(EnvironmentVariables),
1460 SKIP_FILES: validation.RegexStr(default=SKIP_NO_FILES),
1465 @classmethod
1466 def MergeManualScaling(cls, appinclude_one, appinclude_two):
1467 """Takes the greater of <manual_scaling.instances> from the args.
1469 Note that appinclude_one is mutated to be the merged result in this process.
1471 Also, this function needs to be updated if ManualScaling gets additional
1472 fields.
1474 Args:
1475 appinclude_one: object one to merge. Must have a "manual_scaling" field
1476 which contains a ManualScaling().
1477 appinclude_two: object two to merge. Must have a "manual_scaling" field
1478 which contains a ManualScaling().
1480 Returns:
1481 Object that is the result of merging
1482 appinclude_one.manual_scaling.instances and
1483 appinclude_two.manual_scaling.instances. I.e., <appinclude_one>
1484 after the mutations are complete.
1487 def _Instances(appinclude):
1488 if appinclude.manual_scaling:
1489 if appinclude.manual_scaling.instances:
1490 return int(appinclude.manual_scaling.instances)
1491 return None
1495 instances = max(_Instances(appinclude_one), _Instances(appinclude_two))
1496 if instances is not None:
1497 appinclude_one.manual_scaling = ManualScaling(instances=str(instances))
1498 return appinclude_one
1500 @classmethod
1501 def _CommonMergeOps(cls, one, two):
1502 """This function performs common merge operations."""
1504 AppInclude.MergeManualScaling(one, two)
1507 one.admin_console = AdminConsole.Merge(one.admin_console,
1508 two.admin_console)
1512 one.vm = two.vm or one.vm
1515 one.vm_settings = VmSettings.Merge(one.vm_settings,
1516 two.vm_settings)
1520 one.env_variables = EnvironmentVariables.Merge(one.env_variables,
1521 two.env_variables)
1523 one.skip_files = cls.MergeSkipFiles(one.skip_files, two.skip_files)
1525 return one
1527 @classmethod
1528 def MergeAppYamlAppInclude(cls, appyaml, appinclude):
1529 """This function merges an app.yaml file with referenced builtins/includes.
1535 if not appinclude:
1536 return appyaml
1539 if appinclude.handlers:
1540 tail = appyaml.handlers or []
1541 appyaml.handlers = []
1543 for h in appinclude.handlers:
1544 if not h.position or h.position == 'head':
1545 appyaml.handlers.append(h)
1546 else:
1547 tail.append(h)
1551 h.position = None
1553 appyaml.handlers.extend(tail)
1555 appyaml = cls._CommonMergeOps(appyaml, appinclude)
1556 return NormalizeVmSettings(appyaml)
1558 @classmethod
1559 def MergeAppIncludes(cls, appinclude_one, appinclude_two):
1560 """This function merges the non-referential state of the provided AppInclude
1561 objects. That is, builtins and includes directives are not preserved, but
1562 any static objects are copied into an aggregate AppInclude object that
1563 preserves the directives of both provided AppInclude objects.
1565 Note that appinclude_one is mutated to be the merged result in this process.
1567 Args:
1568 appinclude_one: object one to merge
1569 appinclude_two: object two to merge
1571 Returns:
1572 AppInclude object that is the result of merging the static directives of
1573 appinclude_one and appinclude_two. I.e., <appinclude_one> after the
1574 mutations are complete.
1579 if not appinclude_one or not appinclude_two:
1580 return appinclude_one or appinclude_two
1584 if appinclude_one.handlers:
1585 if appinclude_two.handlers:
1586 appinclude_one.handlers.extend(appinclude_two.handlers)
1587 else:
1588 appinclude_one.handlers = appinclude_two.handlers
1590 return cls._CommonMergeOps(appinclude_one, appinclude_two)
1592 @staticmethod
1593 def MergeSkipFiles(skip_files_one, skip_files_two):
1594 if skip_files_one == SKIP_NO_FILES:
1595 return skip_files_two
1596 if skip_files_two == SKIP_NO_FILES:
1597 return skip_files_one
1598 return validation.RegexStr().Validate(
1599 [skip_files_one, skip_files_two], SKIP_FILES)
1604 class AppInfoExternal(validation.Validated):
1605 """Class representing users application info.
1607 This class is passed to a yaml_object builder to provide the validation
1608 for the application information file format parser.
1610 Attributes:
1611 application: Unique identifier for application.
1612 version: Application's major version.
1613 runtime: Runtime used by application.
1614 api_version: Which version of APIs to use.
1615 source_language: Optional specification of the source language.
1616 For example we specify "php-quercus" if this is a Java app
1617 that was generated from PHP source using Quercus
1618 handlers: List of URL handlers.
1619 default_expiration: Default time delta to use for cache expiration for
1620 all static files, unless they have their own specific 'expiration' set.
1621 See the URLMap.expiration field's documentation for more information.
1622 skip_files: An re object. Files that match this regular expression will
1623 not be uploaded by appcfg.py. For example:
1624 skip_files: |
1625 .svn.*|
1626 #.*#
1627 nobuild_files: An re object. Files that match this regular expression will
1628 not be built into the app. Go only.
1629 api_config: URL root and script/servlet path for enhanced api serving
1632 ATTRIBUTES = {
1635 APPLICATION: validation.Optional(APPLICATION_RE_STRING),
1636 MODULE: validation.Optional(MODULE_ID_RE_STRING),
1637 VERSION: validation.Optional(MODULE_VERSION_ID_RE_STRING),
1638 RUNTIME: RUNTIME_RE_STRING,
1641 API_VERSION: API_VERSION_RE_STRING,
1642 INSTANCE_CLASS: validation.Optional(_INSTANCE_CLASS_REGEX),
1643 SOURCE_LANGUAGE: validation.Optional(
1644 validation.Regex(SOURCE_LANGUAGE_RE_STRING)),
1645 AUTOMATIC_SCALING: validation.Optional(AutomaticScaling),
1646 MANUAL_SCALING: validation.Optional(ManualScaling),
1647 BASIC_SCALING: validation.Optional(BasicScaling),
1648 VM: validation.Optional(bool),
1649 VM_SETTINGS: validation.Optional(VmSettings),
1650 VM_HEALTH_CHECK: validation.Optional(VmHealthCheck),
1651 BUILTINS: validation.Optional(validation.Repeated(BuiltinHandler)),
1652 INCLUDES: validation.Optional(validation.Type(list)),
1653 HANDLERS: validation.Optional(validation.Repeated(URLMap), default=[]),
1654 LIBRARIES: validation.Optional(validation.Repeated(Library)),
1656 SERVICES: validation.Optional(validation.Repeated(
1657 validation.Regex(_SERVICE_RE_STRING))),
1658 DEFAULT_EXPIRATION: validation.Optional(_EXPIRATION_REGEX),
1659 SKIP_FILES: validation.RegexStr(default=DEFAULT_SKIP_FILES),
1660 NOBUILD_FILES: validation.RegexStr(default=DEFAULT_NOBUILD_FILES),
1661 DERIVED_FILE_TYPE: validation.Optional(validation.Repeated(
1662 validation.Options(JAVA_PRECOMPILED, PYTHON_PRECOMPILED))),
1663 ADMIN_CONSOLE: validation.Optional(AdminConsole),
1664 ERROR_HANDLERS: validation.Optional(validation.Repeated(ErrorHandlers)),
1665 BACKENDS: validation.Optional(validation.Repeated(
1666 backendinfo.BackendEntry)),
1667 THREADSAFE: validation.Optional(bool),
1668 DATASTORE_AUTO_ID_POLICY: validation.Optional(
1669 validation.Options(DATASTORE_ID_POLICY_LEGACY,
1670 DATASTORE_ID_POLICY_DEFAULT)),
1671 API_CONFIG: validation.Optional(ApiConfigHandler),
1672 CODE_LOCK: validation.Optional(bool),
1673 ENV_VARIABLES: validation.Optional(EnvironmentVariables),
1674 PAGESPEED: validation.Optional(pagespeedinfo.PagespeedEntry),
1681 _skip_runtime_checks = False
1683 def CheckInitialized(self):
1684 """Performs non-regex-based validation.
1686 The following are verified:
1687 - At least one url mapping is provided in the URL mappers.
1688 - Number of url mappers doesn't exceed MAX_URL_MAPS.
1689 - Major version does not contain the string -dot-.
1690 - If api_endpoints are defined, an api_config stanza must be defined.
1691 - If the runtime is python27 and threadsafe is set, then no CGI handlers
1692 can be used.
1693 - That the version name doesn't start with BUILTIN_NAME_PREFIX
1694 - If redirect_http_response_code exists, it is in the list of valid 300s.
1696 Raises:
1697 DuplicateLibrary: if the name library name is specified more than once.
1698 MissingURLMapping: if no URLMap object is present in the object.
1699 TooManyURLMappings: if there are too many URLMap entries.
1700 MissingApiConfig: if api_endpoints exist without an api_config.
1701 MissingThreadsafe: if threadsafe is not set but the runtime requires it.
1702 ThreadsafeWithCgiHandler: if the runtime is python27, threadsafe is set
1703 and CGI handlers are specified.
1704 TooManyScalingSettingsError: if more than one scaling settings block is
1705 present.
1706 RuntimeDoesNotSupportLibraries: if libraries clause is used for a runtime
1707 that does not support it (e.g. python25).
1709 super(AppInfoExternal, self).CheckInitialized()
1710 if (not self.handlers and not self.builtins and not self.includes
1711 and not self.vm):
1712 raise appinfo_errors.MissingURLMapping(
1713 'No URLMap entries found in application configuration')
1714 if self.handlers and len(self.handlers) > MAX_URL_MAPS:
1715 raise appinfo_errors.TooManyURLMappings(
1716 'Found more than %d URLMap entries in application configuration' %
1717 MAX_URL_MAPS)
1719 if (self.threadsafe is None and
1720 self.runtime == 'python27' and
1721 not self._skip_runtime_checks):
1722 raise appinfo_errors.MissingThreadsafe(
1723 'threadsafe must be present and set to either "yes" or "no"')
1725 if self.auto_id_policy == DATASTORE_ID_POLICY_LEGACY:
1726 datastore_auto_ids_url = ('http://developers.google.com/'
1727 'appengine/docs/python/datastore/'
1728 'entities#Kinds_and_Identifiers')
1729 appcfg_auto_ids_url = ('http://developers.google.com/appengine/docs/'
1730 'python/config/appconfig#auto_id_policy')
1731 logging.warning(
1732 "You have set the datastore auto_id_policy to 'legacy'. It is "
1733 "recommended that you select 'default' instead.\n"
1734 "Legacy auto ids are deprecated. You can continue to allocate\n"
1735 "legacy ids manually using the allocate_ids() API functions.\n"
1736 "For more information see:\n"
1737 + datastore_auto_ids_url + '\n' + appcfg_auto_ids_url + '\n')
1739 if self.libraries:
1740 vm_runtime_python27 = (
1741 self.runtime == 'vm' and
1742 hasattr(self, 'vm_settings') and
1743 self.vm_settings['vm_runtime'] == 'python27')
1744 if not self._skip_runtime_checks and not (
1745 vm_runtime_python27 or self.runtime == 'python27'):
1746 raise appinfo_errors.RuntimeDoesNotSupportLibraries(
1747 'libraries entries are only supported by the "python27" runtime')
1749 library_names = [library.name for library in self.libraries]
1750 for library_name in library_names:
1751 if library_names.count(library_name) > 1:
1752 raise appinfo_errors.DuplicateLibrary(
1753 'Duplicate library entry for %s' % library_name)
1755 if self.version and self.version.find(ALTERNATE_HOSTNAME_SEPARATOR) != -1:
1756 raise validation.ValidationError(
1757 'Version "%s" cannot contain the string "%s"' % (
1758 self.version, ALTERNATE_HOSTNAME_SEPARATOR))
1759 if self.version and self.version.startswith(BUILTIN_NAME_PREFIX):
1760 raise validation.ValidationError(
1761 ('Version "%s" cannot start with "%s" because it is a '
1762 'reserved version name prefix.') % (self.version,
1763 BUILTIN_NAME_PREFIX))
1764 if self.handlers:
1765 api_endpoints = [handler.url for handler in self.handlers
1766 if handler.GetHandlerType() == HANDLER_API_ENDPOINT]
1767 if api_endpoints and not self.api_config:
1768 raise appinfo_errors.MissingApiConfig(
1769 'An api_endpoint handler was specified, but the required '
1770 'api_config stanza was not configured.')
1771 if (self.threadsafe and
1772 self.runtime == 'python27' and
1773 not self._skip_runtime_checks):
1774 for handler in self.handlers:
1775 if (handler.script and (handler.script.endswith('.py') or
1776 '/' in handler.script)):
1777 raise appinfo_errors.ThreadsafeWithCgiHandler(
1778 'threadsafe cannot be enabled with CGI handler: %s' %
1779 handler.script)
1780 if sum([bool(self.automatic_scaling),
1781 bool(self.manual_scaling),
1782 bool(self.basic_scaling)]) > 1:
1783 raise appinfo_errors.TooManyScalingSettingsError(
1784 "There may be only one of 'automatic_scaling', 'manual_scaling', "
1785 "or 'basic_scaling'.")
1788 def GetAllLibraries(self):
1789 """Returns a list of all Library instances active for this configuration.
1791 Returns:
1792 The list of active Library instances for this configuration. This includes
1793 directly-specified libraries as well as any required dependencies.
1795 if not self.libraries:
1796 return []
1798 library_names = set(library.name for library in self.libraries)
1799 required_libraries = []
1801 for library in self.libraries:
1802 for required_name, required_version in REQUIRED_LIBRARIES.get(
1803 (library.name, library.version), []):
1804 if required_name not in library_names:
1805 required_libraries.append(Library(name=required_name,
1806 version=required_version))
1808 return [Library(**library.ToDict())
1809 for library in self.libraries + required_libraries]
1811 def GetNormalizedLibraries(self):
1812 """Returns a list of normalized Library instances for this configuration.
1814 Returns:
1815 The list of active Library instances for this configuration. This includes
1816 directly-specified libraries, their required dependencies as well as any
1817 libraries enabled by default. Any libraries with "latest" as their version
1818 will be replaced with the latest available version.
1820 libraries = self.GetAllLibraries()
1821 enabled_libraries = set(library.name for library in libraries)
1822 for library in _SUPPORTED_LIBRARIES:
1823 if library.default_version and library.name not in enabled_libraries:
1824 libraries.append(Library(name=library.name,
1825 version=library.default_version))
1826 for library in libraries:
1827 if library.version == 'latest':
1828 library.version = _NAME_TO_SUPPORTED_LIBRARY[
1829 library.name].supported_versions[-1]
1830 return libraries
1832 def ApplyBackendSettings(self, backend_name):
1833 """Applies settings from the indicated backend to the AppInfoExternal.
1835 Backend entries may contain directives that modify other parts of the
1836 app.yaml, such as the 'start' directive, which adds a handler for the start
1837 request. This method performs those modifications.
1839 Args:
1840 backend_name: The name of a backend defined in 'backends'.
1842 Raises:
1843 BackendNotFound: if the indicated backend was not listed in 'backends'.
1844 DuplicateBackend: if backend is found more than once in 'backends'.
1846 if backend_name is None:
1847 return
1849 if self.backends is None:
1850 raise appinfo_errors.BackendNotFound
1852 self.version = backend_name
1854 match = None
1855 for backend in self.backends:
1856 if backend.name != backend_name:
1857 continue
1858 if match:
1859 raise appinfo_errors.DuplicateBackend
1860 else:
1861 match = backend
1863 if match is None:
1864 raise appinfo_errors.BackendNotFound
1866 if match.start is None:
1867 return
1869 start_handler = URLMap(url=_START_PATH, script=match.start)
1870 self.handlers.insert(0, start_handler)
1872 def GetEffectiveRuntime(self):
1873 """Returns the app's runtime, resolving VMs to the underlying vm_runtime.
1875 Returns:
1876 The effective runtime: the value of vm_settings.vm_runtime if runtime is
1877 "vm", or runtime otherwise.
1879 if (self.runtime == 'vm' and hasattr(self, 'vm_settings')
1880 and self.vm_settings is not None):
1881 return self.vm_settings.get('vm_runtime')
1882 return self.runtime
1885 def ValidateHandlers(handlers, is_include_file=False):
1886 """Validates a list of handler (URLMap) objects.
1888 Args:
1889 handlers: A list of a handler (URLMap) objects.
1890 is_include_file: If true, indicates the we are performing validation
1891 for handlers in an AppInclude file, which may contain special directives.
1893 if not handlers:
1894 return
1896 for handler in handlers:
1897 handler.FixSecureDefaults()
1898 handler.WarnReservedURLs()
1899 if not is_include_file:
1900 handler.ErrorOnPositionForAppInfo()
1903 def LoadSingleAppInfo(app_info):
1904 """Load a single AppInfo object where one and only one is expected.
1906 Validates that the the values in the AppInfo match the validators defined
1907 in this file. (in particular, in AppInfoExternal.ATTRIBUTES)
1909 Args:
1910 app_info: A file-like object or string. If it is a string, parse it as
1911 a configuration file. If it is a file-like object, read in data and
1912 parse.
1914 Returns:
1915 An instance of AppInfoExternal as loaded from a YAML file.
1917 Raises:
1918 ValueError: if a specified service is not valid.
1919 EmptyConfigurationFile: when there are no documents in YAML file.
1920 MultipleConfigurationFile: when there is more than one document in YAML
1921 file.
1922 DuplicateBackend: if backend is found more than once in 'backends'.
1923 yaml_errors.EventError: if the app.yaml fails validation.
1925 builder = yaml_object.ObjectBuilder(AppInfoExternal)
1926 handler = yaml_builder.BuilderHandler(builder)
1927 listener = yaml_listener.EventListener(handler)
1928 listener.Parse(app_info)
1930 app_infos = handler.GetResults()
1931 if len(app_infos) < 1:
1932 raise appinfo_errors.EmptyConfigurationFile()
1933 if len(app_infos) > 1:
1934 raise appinfo_errors.MultipleConfigurationFile()
1936 appyaml = app_infos[0]
1937 ValidateHandlers(appyaml.handlers)
1938 if appyaml.builtins:
1939 BuiltinHandler.Validate(appyaml.builtins, appyaml.runtime)
1941 return NormalizeVmSettings(appyaml)
1944 class AppInfoSummary(validation.Validated):
1945 """This class contains only basic summary information about an app.
1947 It is used to pass back information about the newly created app to users
1948 after a new version has been created.
1955 ATTRIBUTES = {
1956 APPLICATION: APPLICATION_RE_STRING,
1957 MAJOR_VERSION: MODULE_VERSION_ID_RE_STRING,
1958 MINOR_VERSION: validation.TYPE_LONG
1962 def LoadAppInclude(app_include):
1963 """Load a single AppInclude object where one and only one is expected.
1965 Args:
1966 app_include: A file-like object or string. If it is a string, parse it as
1967 a configuration file. If it is a file-like object, read in data and
1968 parse.
1970 Returns:
1971 An instance of AppInclude as loaded from a YAML file.
1973 Raises:
1974 EmptyConfigurationFile: when there are no documents in YAML file.
1975 MultipleConfigurationFile: when there is more than one document in YAML
1976 file.
1978 builder = yaml_object.ObjectBuilder(AppInclude)
1979 handler = yaml_builder.BuilderHandler(builder)
1980 listener = yaml_listener.EventListener(handler)
1981 listener.Parse(app_include)
1983 includes = handler.GetResults()
1984 if len(includes) < 1:
1985 raise appinfo_errors.EmptyConfigurationFile()
1986 if len(includes) > 1:
1987 raise appinfo_errors.MultipleConfigurationFile()
1989 includeyaml = includes[0]
1990 if includeyaml.handlers:
1991 for handler in includeyaml.handlers:
1992 handler.FixSecureDefaults()
1993 handler.WarnReservedURLs()
1994 if includeyaml.builtins:
1995 BuiltinHandler.Validate(includeyaml.builtins)
1997 return includeyaml
2000 def ParseExpiration(expiration):
2001 """Parses an expiration delta string.
2003 Args:
2004 expiration: String that matches _DELTA_REGEX.
2006 Returns:
2007 Time delta in seconds.
2009 delta = 0
2010 for match in re.finditer(_DELTA_REGEX, expiration):
2011 amount = int(match.group(1))
2012 units = _EXPIRATION_CONVERSIONS.get(match.group(2).lower(), 1)
2013 delta += amount * units
2014 return delta
2022 _file_path_positive_re = re.compile(r'^[ 0-9a-zA-Z\._\+/@\$-]{1,256}$')
2025 _file_path_negative_1_re = re.compile(r'\.\.|^\./|\.$|/\./|^-|^_ah/|^/')
2028 _file_path_negative_2_re = re.compile(r'//|/$')
2032 _file_path_negative_3_re = re.compile(r'^ | $|/ | /')
2035 def ValidFilename(filename):
2036 """Determines if filename is valid.
2038 filename must be a valid pathname.
2039 - It must contain only letters, numbers, @, _, +, /, $, ., and -.
2040 - It must be less than 256 chars.
2041 - It must not contain "/./", "/../", or "//".
2042 - It must not end in "/".
2043 - All spaces must be in the middle of a directory or file name.
2045 Args:
2046 filename: The filename to validate.
2048 Returns:
2049 An error string if the filename is invalid. Returns '' if the filename
2050 is valid.
2052 if _file_path_positive_re.match(filename) is None:
2053 return 'Invalid character in filename: %s' % filename
2054 if _file_path_negative_1_re.search(filename) is not None:
2055 return ('Filename cannot contain "." or ".." '
2056 'or start with "-" or "_ah/": %s' %
2057 filename)
2058 if _file_path_negative_2_re.search(filename) is not None:
2059 return 'Filename cannot have trailing / or contain //: %s' % filename
2060 if _file_path_negative_3_re.search(filename) is not None:
2061 return 'Any spaces must be in the middle of a filename: %s' % filename
2062 return ''