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.
17 """Stores application configuration taken from e.g. app.yaml, queues.yaml."""
19 # TODO: Support more than just app.yaml.
31 from google
.appengine
.api
import appinfo
32 from google
.appengine
.api
import appinfo_includes
33 from google
.appengine
.api
import backendinfo
34 from google
.appengine
.api
import dispatchinfo
35 from google
.appengine
.tools
.devappserver2
import errors
37 # Constants passed to functions registered with
38 # ServerConfiguration.add_change_callback.
39 NORMALIZED_LIBRARIES_CHANGED
= 1
40 SKIP_FILES_CHANGED
= 2
42 INBOUND_SERVICES_CHANGED
= 4
43 ENV_VARIABLES_CHANGED
= 5
44 ERROR_HANDLERS_CHANGED
= 6
45 NOBUILD_FILES_CHANGED
= 7
48 class ServerConfiguration(object):
49 """Stores server configuration information.
51 Most configuration options are mutable and may change any time
52 check_for_updates is called. Client code must be able to cope with these
55 Other properties are immutable (see _IMMUTABLE_PROPERTIES) and are guaranteed
56 to be constant for the lifetime of the instance.
59 _IMMUTABLE_PROPERTIES
= [
60 ('application', 'application'),
61 ('version', 'major_version'),
62 ('runtime', 'runtime'),
63 ('threadsafe', 'threadsafe'),
64 ('server', 'server_name'),
65 ('basic_scaling', 'basic_scaling'),
66 ('manual_scaling', 'manual_scaling'),
67 ('automatic_scaling', 'automatic_scaling')]
69 def __init__(self
, yaml_path
):
70 """Initializer for ServerConfiguration.
73 yaml_path: A string containing the full path of the yaml file containing
74 the configuration for this server.
76 self
._yaml
_path
= yaml_path
77 self
._app
_info
_external
= None
78 self
._application
_root
= os
.path
.realpath(os
.path
.dirname(yaml_path
))
79 self
._last
_failure
_message
= None
81 self
._app
_info
_external
, files_to_check
= self
._parse
_configuration
(
83 self
._mtimes
= self
._get
_mtimes
([self
._yaml
_path
] + files_to_check
)
84 self
._application
= 'dev~%s' % self
._app
_info
_external
.application
85 self
._api
_version
= self
._app
_info
_external
.api_version
86 self
._server
_name
= self
._app
_info
_external
.server
87 self
._version
= self
._app
_info
_external
.version
88 self
._threadsafe
= self
._app
_info
_external
.threadsafe
89 self
._basic
_scaling
= self
._app
_info
_external
.basic_scaling
90 self
._manual
_scaling
= self
._app
_info
_external
.manual_scaling
91 self
._automatic
_scaling
= self
._app
_info
_external
.automatic_scaling
92 self
._runtime
= self
._app
_info
_external
.runtime
93 if self
._runtime
== 'python':
95 'The "python" runtime specified in "%s" is not supported - the '
96 '"python27" runtime will be used instead. A description of the '
97 'differences between the two can be found here:\n'
98 'https://developers.google.com/appengine/docs/python/python25/diff27',
100 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
104 def application_root(self
):
105 """The directory containing the application e.g. "/home/user/myapp"."""
106 return self
._application
_root
109 def application(self
):
110 return self
._application
113 def api_version(self
):
114 return self
._api
_version
117 def server_name(self
):
118 return self
._server
_name
or 'default'
121 def major_version(self
):
125 def version_id(self
):
126 if self
.server_name
== 'default':
129 self
._minor
_version
_id
)
131 return '%s:%s.%s' % (
134 self
._minor
_version
_id
)
141 def threadsafe(self
):
142 return self
._threadsafe
145 def basic_scaling(self
):
146 return self
._basic
_scaling
149 def manual_scaling(self
):
150 return self
._manual
_scaling
153 def automatic_scaling(self
):
154 return self
._automatic
_scaling
157 def normalized_libraries(self
):
158 return self
._app
_info
_external
.GetNormalizedLibraries()
161 def skip_files(self
):
162 return self
._app
_info
_external
.skip_files
165 def nobuild_files(self
):
166 return self
._app
_info
_external
.nobuild_files
169 def error_handlers(self
):
170 return self
._app
_info
_external
.error_handlers
174 return self
._app
_info
_external
.handlers
177 def inbound_services(self
):
178 return self
._app
_info
_external
.inbound_services
181 def env_variables(self
):
182 return self
._app
_info
_external
.env_variables
185 def is_backend(self
):
188 def check_for_updates(self
):
189 """Return any configuration changes since the last check_for_updates call.
192 A set containing the changes that occured. See the *_CHANGED module
195 new_mtimes
= self
._get
_mtimes
(self
._mtimes
.keys())
196 if new_mtimes
== self
._mtimes
:
200 app_info_external
, files_to_check
= self
._parse
_configuration
(
203 failure_message
= str(e
)
204 if failure_message
!= self
._last
_failure
_message
:
205 logging
.error('Configuration is not valid: %s', failure_message
)
206 self
._last
_failure
_message
= failure_message
208 self
._last
_failure
_message
= None
210 self
._mtimes
= self
._get
_mtimes
([self
._yaml
_path
] + files_to_check
)
212 for app_info_attribute
, self_attribute
in self
._IMMUTABLE
_PROPERTIES
:
213 app_info_value
= getattr(app_info_external
, app_info_attribute
)
214 self_value
= getattr(self
, self_attribute
)
215 if (app_info_value
== self_value
or
216 app_info_value
== getattr(self
._app
_info
_external
,
217 app_info_attribute
)):
218 # Only generate a warning if the value is both different from the
219 # immutable value *and* different from the last loaded value.
222 if isinstance(app_info_value
, types
.StringTypes
):
223 logging
.warning('Restart the development server to see updates to "%s" '
229 logging
.warning('Restart the development server to see updates to "%s"',
233 if (app_info_external
.GetNormalizedLibraries() !=
234 self
.normalized_libraries
):
235 changes
.add(NORMALIZED_LIBRARIES_CHANGED
)
236 if app_info_external
.skip_files
!= self
.skip_files
:
237 changes
.add(SKIP_FILES_CHANGED
)
238 if app_info_external
.nobuild_files
!= self
.nobuild_files
:
239 changes
.add(NOBUILD_FILES_CHANGED
)
240 if app_info_external
.handlers
!= self
.handlers
:
241 changes
.add(HANDLERS_CHANGED
)
242 if app_info_external
.inbound_services
!= self
.inbound_services
:
243 changes
.add(INBOUND_SERVICES_CHANGED
)
244 if app_info_external
.env_variables
!= self
.env_variables
:
245 changes
.add(ENV_VARIABLES_CHANGED
)
246 if app_info_external
.error_handlers
!= self
.error_handlers
:
247 changes
.add(ERROR_HANDLERS_CHANGED
)
249 self
._app
_info
_external
= app_info_external
251 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
256 def _get_mtimes(filenames
):
257 filename_to_mtime
= {}
258 for filename
in filenames
:
260 filename_to_mtime
[filename
] = os
.path
.getmtime(filename
)
262 # Ignore deleted includes.
263 if e
.errno
!= errno
.ENOENT
:
265 return filename_to_mtime
268 def _parse_configuration(configuration_path
):
269 # TODO: It probably makes sense to catch the exception raised
270 # by Parse() and re-raise it using a module-specific exception.
271 with
open(configuration_path
) as f
:
272 return appinfo_includes
.ParseAndReturnIncludePaths(f
)
275 class BackendsConfiguration(object):
276 """Stores configuration information for a backends.yaml file."""
278 def __init__(self
, app_yaml_path
, backend_yaml_path
):
279 """Initializer for BackendsConfiguration.
282 app_yaml_path: A string containing the full path of the yaml file
283 containing the configuration for this server.
284 backend_yaml_path: A string containing the full path of the backends.yaml
285 file containing the configuration for backends.
287 self
._update
_lock
= threading
.RLock()
288 self
._base
_server
_configuration
= ServerConfiguration(app_yaml_path
)
289 backend_info_external
= self
._parse
_configuration
(
292 self
._backends
_name
_to
_backend
_entry
= {}
293 for backend
in backend_info_external
.backends
or []:
294 self
._backends
_name
_to
_backend
_entry
[backend
.name
] = backend
295 self
._changes
= dict(
296 (backend_name
, set())
297 for backend_name
in self
._backends
_name
_to
_backend
_entry
)
300 def _parse_configuration(configuration_path
):
301 # TODO: It probably makes sense to catch the exception raised
302 # by Parse() and re-raise it using a module-specific exception.
303 with
open(configuration_path
) as f
:
304 return backendinfo
.LoadBackendInfo(f
)
306 def get_backend_configurations(self
):
307 return [BackendConfiguration(self
._base
_server
_configuration
, self
, entry
)
308 for entry
in self
._backends
_name
_to
_backend
_entry
.values()]
310 def check_for_updates(self
, backend_name
):
311 """Return any configuration changes since the last check_for_updates call.
314 backend_name: A str containing the name of the backend to be checked for
318 A set containing the changes that occured. See the *_CHANGED module
321 with self
._update
_lock
:
322 server_changes
= self
._base
_server
_configuration
.check_for_updates()
324 for backend_changes
in self
._changes
.values():
325 backend_changes
.update(server_changes
)
326 changes
= self
._changes
[backend_name
]
327 self
._changes
[backend_name
] = set()
331 class BackendConfiguration(object):
332 """Stores backend configuration information.
334 This interface is and must remain identical to ServerConfiguration.
337 def __init__(self
, server_configuration
, backends_configuration
,
339 """Initializer for BackendConfiguration.
342 server_configuration: A ServerConfiguration to use.
343 backends_configuration: The BackendsConfiguration that tracks updates for
344 this BackendConfiguration.
345 backend_entry: A backendinfo.BackendEntry containing the backend
348 self
._server
_configuration
= server_configuration
349 self
._backends
_configuration
= backends_configuration
350 self
._backend
_entry
= backend_entry
352 if backend_entry
.dynamic
:
353 self
._basic
_scaling
= appinfo
.BasicScaling(
354 max_instances
=backend_entry
.instances
or 1)
355 self
._manual
_scaling
= None
357 self
._basic
_scaling
= None
358 self
._manual
_scaling
= appinfo
.ManualScaling(
359 instances
=backend_entry
.instances
or 1)
360 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
364 def application_root(self
):
365 """The directory containing the application e.g. "/home/user/myapp"."""
366 return self
._server
_configuration
.application_root
369 def application(self
):
370 return self
._server
_configuration
.application
373 def api_version(self
):
374 return self
._server
_configuration
.api_version
377 def server_name(self
):
378 return self
._backend
_entry
.name
381 def major_version(self
):
382 return self
._server
_configuration
.major_version
385 def version_id(self
):
386 return '%s:%s.%s' % (
389 self
._minor
_version
_id
)
393 return self
._server
_configuration
.runtime
396 def threadsafe(self
):
397 return self
._server
_configuration
.threadsafe
400 def basic_scaling(self
):
401 return self
._basic
_scaling
404 def manual_scaling(self
):
405 return self
._manual
_scaling
408 def automatic_scaling(self
):
412 def normalized_libraries(self
):
413 return self
._server
_configuration
.normalized_libraries
416 def skip_files(self
):
417 return self
._server
_configuration
.skip_files
420 def nobuild_files(self
):
421 return self
._server
_configuration
.nobuild_files
424 def error_handlers(self
):
425 return self
._server
_configuration
.error_handlers
429 if self
._backend
_entry
.start
:
430 return [appinfo
.URLMap(
432 script
=self
._backend
_entry
.start
,
433 login
='admin')] + self
._server
_configuration
.handlers
434 return self
._server
_configuration
.handlers
437 def inbound_services(self
):
438 return self
._server
_configuration
.inbound_services
441 def env_variables(self
):
442 return self
._server
_configuration
.env_variables
445 def is_backend(self
):
448 def check_for_updates(self
):
449 """Return any configuration changes since the last check_for_updates call.
452 A set containing the changes that occured. See the *_CHANGED module
455 changes
= self
._backends
_configuration
.check_for_updates(
456 self
._backend
_entry
.name
)
458 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
463 class DispatchConfiguration(object):
464 """Stores dispatcher configuration information."""
466 def __init__(self
, yaml_path
):
467 self
._yaml
_path
= yaml_path
468 self
._mtime
= os
.path
.getmtime(self
._yaml
_path
)
469 self
._process
_dispatch
_entries
(self
._parse
_configuration
(self
._yaml
_path
))
472 def _parse_configuration(configuration_path
):
473 # TODO: It probably makes sense to catch the exception raised
474 # by LoadSingleDispatch() and re-raise it using a module-specific exception.
475 with
open(configuration_path
) as f
:
476 return dispatchinfo
.LoadSingleDispatch(f
)
478 def check_for_updates(self
):
479 mtime
= os
.path
.getmtime(self
._yaml
_path
)
480 if mtime
> self
._mtime
:
483 dispatch_info_external
= self
._parse
_configuration
(self
._yaml
_path
)
485 failure_message
= str(e
)
486 logging
.error('Configuration is not valid: %s', failure_message
)
488 self
._process
_dispatch
_entries
(dispatch_info_external
)
490 def _process_dispatch_entries(self
, dispatch_info_external
):
491 path_only_entries
= []
492 hostname_entries
= []
493 for entry
in dispatch_info_external
.dispatch
:
494 parsed_url
= dispatchinfo
.ParsedURL(entry
.url
)
496 hostname_entries
.append(entry
)
498 path_only_entries
.append((parsed_url
, entry
.server
))
501 'Hostname routing is not supported by the development server. The '
502 'following dispatch entries will not match any requests:\n%s',
503 '\n\t'.join(str(entry
) for entry
in hostname_entries
))
504 self
._entries
= path_only_entries
511 class ApplicationConfiguration(object):
512 """Stores application configuration information."""
514 def __init__(self
, yaml_paths
):
515 """Initializer for ApplicationConfiguration.
518 yaml_paths: A list of strings containing the paths to yaml files.
522 if len(yaml_paths
) == 1 and os
.path
.isdir(yaml_paths
[0]):
523 directory_path
= yaml_paths
[0]
524 for app_yaml_path
in [os
.path
.join(directory_path
, 'app.yaml'),
525 os
.path
.join(directory_path
, 'app.yml')]:
526 if os
.path
.exists(app_yaml_path
):
527 yaml_paths
= [app_yaml_path
]
530 raise errors
.AppConfigNotFoundError(
531 'no app.yaml file at %r' % directory_path
)
532 for backends_yaml_path
in [os
.path
.join(directory_path
, 'backends.yaml'),
533 os
.path
.join(directory_path
, 'backends.yml')]:
534 if os
.path
.exists(backends_yaml_path
):
535 yaml_paths
.append(backends_yaml_path
)
537 for yaml_path
in yaml_paths
:
538 if os
.path
.isdir(yaml_path
):
539 raise errors
.InvalidAppConfigError(
540 '"%s" is a directory and a yaml configuration file is required' %
542 elif (yaml_path
.endswith('backends.yaml') or
543 yaml_path
.endswith('backends.yml')):
544 # TODO: Reuse the ServerConfiguration created for the app.yaml
545 # instead of creating another one for the same file.
547 BackendsConfiguration(yaml_path
.replace('backends.y', 'app.y'),
548 yaml_path
).get_backend_configurations())
549 elif (yaml_path
.endswith('dispatch.yaml') or
550 yaml_path
.endswith('dispatch.yml')):
552 raise errors
.InvalidAppConfigError(
553 'Multiple dispatch.yaml files specified')
554 self
.dispatch
= DispatchConfiguration(yaml_path
)
556 server_configuration
= ServerConfiguration(yaml_path
)
557 self
.servers
.append(server_configuration
)
558 application_ids
= set(server
.application
559 for server
in self
.servers
)
560 if len(application_ids
) > 1:
561 raise errors
.InvalidAppConfigError(
562 'More than one application ID found: %s' %
563 ', '.join(sorted(application_ids
)))
565 self
._app
_id
= application_ids
.pop()
567 for server
in self
.servers
:
568 if server
.server_name
in server_names
:
569 raise errors
.InvalidAppConfigError('Duplicate server: %s' %
571 server_names
.add(server
.server_name
)
573 if 'default' not in server_names
:
574 raise errors
.InvalidAppConfigError(
575 'A default server must be specified.')
577 set(server_name
for _
, server_name
in self
.dispatch
.dispatch
) -
580 raise errors
.InvalidAppConfigError(
581 'Servers %s specified in dispatch.yaml are not defined by a yaml '
582 'file.' % sorted(missing_servers
))