blob: 88c48be10066fc14162361ba4f55c2bbcc6bd8e7 [file] [log] [blame]
[email protected]3c703672013-03-19 23:06:511#!/usr/bin/env python
2#
3# Copyright 2007 Google Inc.
4#
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
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
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.
16#
17"""Stores application configuration taken from e.g. app.yaml, queues.yaml."""
18
19# TODO: Support more than just app.yaml.
20
21
22import errno
23import logging
24import os
25import os.path
26import random
27import string
28import threading
29import types
30
31from google.appengine.api import appinfo
32from google.appengine.api import appinfo_includes
33from google.appengine.api import backendinfo
[email protected]6da8fcb2013-04-09 23:18:4234from google.appengine.api import dispatchinfo
[email protected]21ed1cd2014-03-19 22:38:3535from google.appengine.tools import yaml_translator
[email protected]3c703672013-03-19 23:06:5136from google.appengine.tools.devappserver2 import errors
37
38# Constants passed to functions registered with
[email protected]200fcb72013-07-20 01:18:3639# ModuleConfiguration.add_change_callback.
[email protected]3c703672013-03-19 23:06:5140NORMALIZED_LIBRARIES_CHANGED = 1
41SKIP_FILES_CHANGED = 2
42HANDLERS_CHANGED = 3
43INBOUND_SERVICES_CHANGED = 4
44ENV_VARIABLES_CHANGED = 5
45ERROR_HANDLERS_CHANGED = 6
46NOBUILD_FILES_CHANGED = 7
47
48
[email protected]21ed1cd2014-03-19 22:38:3549def java_supported():
50 """True if this SDK supports running Java apps in the dev appserver."""
51 java_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'java')
52 return os.path.isdir(java_dir)
53
54
[email protected]200fcb72013-07-20 01:18:3655class ModuleConfiguration(object):
56 """Stores module configuration information.
[email protected]3c703672013-03-19 23:06:5157
58 Most configuration options are mutable and may change any time
59 check_for_updates is called. Client code must be able to cope with these
60 changes.
61
62 Other properties are immutable (see _IMMUTABLE_PROPERTIES) and are guaranteed
63 to be constant for the lifetime of the instance.
64 """
65
66 _IMMUTABLE_PROPERTIES = [
67 ('application', 'application'),
68 ('version', 'major_version'),
69 ('runtime', 'runtime'),
70 ('threadsafe', 'threadsafe'),
[email protected]200fcb72013-07-20 01:18:3671 ('module', 'module_name'),
[email protected]3c703672013-03-19 23:06:5172 ('basic_scaling', 'basic_scaling'),
73 ('manual_scaling', 'manual_scaling'),
74 ('automatic_scaling', 'automatic_scaling')]
75
[email protected]21ed1cd2014-03-19 22:38:3576 def __init__(self, config_path):
[email protected]200fcb72013-07-20 01:18:3677 """Initializer for ModuleConfiguration.
[email protected]3c703672013-03-19 23:06:5178
79 Args:
[email protected]21ed1cd2014-03-19 22:38:3580 config_path: A string containing the full path of the yaml or xml file
81 containing the configuration for this module.
[email protected]3c703672013-03-19 23:06:5182 """
[email protected]21ed1cd2014-03-19 22:38:3583 self._config_path = config_path
84 root = os.path.dirname(config_path)
85 self._is_java = os.path.normpath(config_path).endswith(
86 os.sep + 'WEB-INF' + os.sep + 'appengine-web.xml')
87 if self._is_java:
88 # We assume Java's XML-based config files only if config_path is
89 # something like /foo/bar/WEB-INF/appengine-web.xml. In this case,
90 # the application root is /foo/bar. Other apps, configured with YAML,
91 # have something like /foo/bar/app.yaml, with application root /foo/bar.
92 root = os.path.dirname(root)
93 self._application_root = os.path.realpath(root)
[email protected]3c703672013-03-19 23:06:5194 self._last_failure_message = None
95
96 self._app_info_external, files_to_check = self._parse_configuration(
[email protected]21ed1cd2014-03-19 22:38:3597 self._config_path)
98 self._mtimes = self._get_mtimes(files_to_check)
[email protected]982cc4d2014-04-04 22:36:4899 self._application = '%s~%s' % (self.partition,
100 self.application_external_name)
[email protected]3c703672013-03-19 23:06:51101 self._api_version = self._app_info_external.api_version
[email protected]200fcb72013-07-20 01:18:36102 self._module_name = self._app_info_external.module
[email protected]3c703672013-03-19 23:06:51103 self._version = self._app_info_external.version
104 self._threadsafe = self._app_info_external.threadsafe
105 self._basic_scaling = self._app_info_external.basic_scaling
106 self._manual_scaling = self._app_info_external.manual_scaling
107 self._automatic_scaling = self._app_info_external.automatic_scaling
108 self._runtime = self._app_info_external.runtime
109 if self._runtime == 'python':
110 logging.warning(
111 'The "python" runtime specified in "%s" is not supported - the '
112 '"python27" runtime will be used instead. A description of the '
113 'differences between the two can be found here:\n'
114 'https://developers.google.com/appengine/docs/python/python25/diff27',
[email protected]21ed1cd2014-03-19 22:38:35115 self._config_path)
[email protected]3c703672013-03-19 23:06:51116 self._minor_version_id = ''.join(random.choice(string.digits) for _ in
117 range(18))
118
119 @property
120 def application_root(self):
121 """The directory containing the application e.g. "/home/user/myapp"."""
122 return self._application_root
123
124 @property
125 def application(self):
126 return self._application
127
128 @property
[email protected]982cc4d2014-04-04 22:36:48129 def partition(self):
130 return 'dev'
131
132 @property
133 def application_external_name(self):
134 return self._app_info_external.application
135
136 @property
[email protected]3c703672013-03-19 23:06:51137 def api_version(self):
138 return self._api_version
139
140 @property
[email protected]200fcb72013-07-20 01:18:36141 def module_name(self):
[email protected]2e4b3c82014-02-26 20:08:51142 return self._module_name or appinfo.DEFAULT_MODULE
[email protected]3c703672013-03-19 23:06:51143
144 @property
145 def major_version(self):
146 return self._version
147
148 @property
[email protected]982cc4d2014-04-04 22:36:48149 def minor_version(self):
150 return self._minor_version_id
151
152 @property
[email protected]3c703672013-03-19 23:06:51153 def version_id(self):
[email protected]2e4b3c82014-02-26 20:08:51154 if self.module_name == appinfo.DEFAULT_MODULE:
[email protected]98325492014-01-15 21:40:06155 return '%s.%s' % (
156 self.major_version,
157 self._minor_version_id)
158 else:
159 return '%s:%s.%s' % (
160 self.module_name,
161 self.major_version,
162 self._minor_version_id)
[email protected]3c703672013-03-19 23:06:51163
164 @property
165 def runtime(self):
166 return self._runtime
167
168 @property
[email protected]982cc4d2014-04-04 22:36:48169 def effective_runtime(self):
170 return self._app_info_external.GetEffectiveRuntime()
171
172 @property
[email protected]3c703672013-03-19 23:06:51173 def threadsafe(self):
174 return self._threadsafe
175
176 @property
177 def basic_scaling(self):
178 return self._basic_scaling
179
180 @property
181 def manual_scaling(self):
182 return self._manual_scaling
183
184 @property
185 def automatic_scaling(self):
186 return self._automatic_scaling
187
188 @property
189 def normalized_libraries(self):
190 return self._app_info_external.GetNormalizedLibraries()
191
192 @property
193 def skip_files(self):
194 return self._app_info_external.skip_files
195
196 @property
197 def nobuild_files(self):
198 return self._app_info_external.nobuild_files
199
200 @property
201 def error_handlers(self):
202 return self._app_info_external.error_handlers
203
204 @property
205 def handlers(self):
206 return self._app_info_external.handlers
207
208 @property
209 def inbound_services(self):
210 return self._app_info_external.inbound_services
211
212 @property
213 def env_variables(self):
214 return self._app_info_external.env_variables
215
216 @property
217 def is_backend(self):
218 return False
219
220 def check_for_updates(self):
221 """Return any configuration changes since the last check_for_updates call.
222
223 Returns:
224 A set containing the changes that occured. See the *_CHANGED module
225 constants.
226 """
227 new_mtimes = self._get_mtimes(self._mtimes.keys())
228 if new_mtimes == self._mtimes:
229 return set()
230
231 try:
232 app_info_external, files_to_check = self._parse_configuration(
[email protected]21ed1cd2014-03-19 22:38:35233 self._config_path)
[email protected]3c703672013-03-19 23:06:51234 except Exception, e:
235 failure_message = str(e)
236 if failure_message != self._last_failure_message:
237 logging.error('Configuration is not valid: %s', failure_message)
238 self._last_failure_message = failure_message
239 return set()
240 self._last_failure_message = None
241
[email protected]21ed1cd2014-03-19 22:38:35242 self._mtimes = self._get_mtimes(files_to_check)
[email protected]3c703672013-03-19 23:06:51243
244 for app_info_attribute, self_attribute in self._IMMUTABLE_PROPERTIES:
245 app_info_value = getattr(app_info_external, app_info_attribute)
246 self_value = getattr(self, self_attribute)
247 if (app_info_value == self_value or
248 app_info_value == getattr(self._app_info_external,
249 app_info_attribute)):
250 # Only generate a warning if the value is both different from the
251 # immutable value *and* different from the last loaded value.
252 continue
253
254 if isinstance(app_info_value, types.StringTypes):
[email protected]200fcb72013-07-20 01:18:36255 logging.warning('Restart the development module to see updates to "%s" '
[email protected]3c703672013-03-19 23:06:51256 '["%s" => "%s"]',
257 app_info_attribute,
258 self_value,
259 app_info_value)
260 else:
[email protected]200fcb72013-07-20 01:18:36261 logging.warning('Restart the development module to see updates to "%s"',
[email protected]3c703672013-03-19 23:06:51262 app_info_attribute)
263
264 changes = set()
265 if (app_info_external.GetNormalizedLibraries() !=
266 self.normalized_libraries):
267 changes.add(NORMALIZED_LIBRARIES_CHANGED)
268 if app_info_external.skip_files != self.skip_files:
269 changes.add(SKIP_FILES_CHANGED)
270 if app_info_external.nobuild_files != self.nobuild_files:
271 changes.add(NOBUILD_FILES_CHANGED)
272 if app_info_external.handlers != self.handlers:
273 changes.add(HANDLERS_CHANGED)
274 if app_info_external.inbound_services != self.inbound_services:
275 changes.add(INBOUND_SERVICES_CHANGED)
276 if app_info_external.env_variables != self.env_variables:
277 changes.add(ENV_VARIABLES_CHANGED)
278 if app_info_external.error_handlers != self.error_handlers:
279 changes.add(ERROR_HANDLERS_CHANGED)
280
281 self._app_info_external = app_info_external
282 if changes:
283 self._minor_version_id = ''.join(random.choice(string.digits) for _ in
284 range(18))
285 return changes
286
287 @staticmethod
288 def _get_mtimes(filenames):
289 filename_to_mtime = {}
290 for filename in filenames:
291 try:
292 filename_to_mtime[filename] = os.path.getmtime(filename)
293 except OSError as e:
294 # Ignore deleted includes.
295 if e.errno != errno.ENOENT:
296 raise
297 return filename_to_mtime
298
[email protected]21ed1cd2014-03-19 22:38:35299 def _parse_configuration(self, configuration_path):
300 """Parse a configuration file (like app.yaml or appengine-web.xml).
301
302 Args:
303 configuration_path: A string containing the full path of the yaml file
304 containing the configuration for this module.
305
306 Returns:
307 A tuple where the first element is the parsed appinfo.AppInfoExternal
308 object and the second element is a list of the paths of the files that
309 were used to produce it, namely the input configuration_path and any
310 other file that was included from that one.
311 """
312 if self._is_java:
313 config, files = self._parse_java_configuration(configuration_path)
314 else:
315 with open(configuration_path) as f:
316 config, files = appinfo_includes.ParseAndReturnIncludePaths(f)
317 return config, [configuration_path] + files
318
[email protected]3c703672013-03-19 23:06:51319 @staticmethod
[email protected]21ed1cd2014-03-19 22:38:35320 def _parse_java_configuration(app_engine_web_xml_path):
321 """Parse appengine-web.xml and web.xml.
322
323 Args:
324 app_engine_web_xml_path: A string containing the full path of the
325 .../WEB-INF/appengine-web.xml file. The corresponding
326 .../WEB-INF/web.xml file must also be present.
327
328 Returns:
329 A tuple where the first element is the parsed appinfo.AppInfoExternal
330 object and the second element is a list of the paths of the files that
331 were used to produce it, namely the input appengine-web.xml file and the
332 corresponding web.xml file.
333 """
334 with open(app_engine_web_xml_path) as f:
335 app_engine_web_xml_str = f.read()
336 web_inf_dir = os.path.dirname(app_engine_web_xml_path)
337 web_xml_path = os.path.join(web_inf_dir, 'web.xml')
338 with open(web_xml_path) as f:
339 web_xml_str = f.read()
340 static_files = []
341 # TODO: need to enumerate static files here
342 app_yaml_str = yaml_translator.TranslateXmlToYaml(
343 app_engine_web_xml_str, web_xml_str, static_files)
344 config = appinfo.LoadSingleAppInfo(app_yaml_str)
345 return config, [app_engine_web_xml_path, web_xml_path]
[email protected]3c703672013-03-19 23:06:51346
347
348class BackendsConfiguration(object):
349 """Stores configuration information for a backends.yaml file."""
350
[email protected]21ed1cd2014-03-19 22:38:35351 def __init__(self, app_config_path, backend_config_path):
[email protected]3c703672013-03-19 23:06:51352 """Initializer for BackendsConfiguration.
353
354 Args:
[email protected]21ed1cd2014-03-19 22:38:35355 app_config_path: A string containing the full path of the yaml file
[email protected]200fcb72013-07-20 01:18:36356 containing the configuration for this module.
[email protected]21ed1cd2014-03-19 22:38:35357 backend_config_path: A string containing the full path of the
358 backends.yaml file containing the configuration for backends.
[email protected]3c703672013-03-19 23:06:51359 """
360 self._update_lock = threading.RLock()
[email protected]21ed1cd2014-03-19 22:38:35361 self._base_module_configuration = ModuleConfiguration(app_config_path)
[email protected]3c703672013-03-19 23:06:51362 backend_info_external = self._parse_configuration(
[email protected]21ed1cd2014-03-19 22:38:35363 backend_config_path)
[email protected]3c703672013-03-19 23:06:51364
365 self._backends_name_to_backend_entry = {}
366 for backend in backend_info_external.backends or []:
367 self._backends_name_to_backend_entry[backend.name] = backend
368 self._changes = dict(
369 (backend_name, set())
370 for backend_name in self._backends_name_to_backend_entry)
371
372 @staticmethod
373 def _parse_configuration(configuration_path):
374 # TODO: It probably makes sense to catch the exception raised
375 # by Parse() and re-raise it using a module-specific exception.
376 with open(configuration_path) as f:
377 return backendinfo.LoadBackendInfo(f)
378
379 def get_backend_configurations(self):
[email protected]200fcb72013-07-20 01:18:36380 return [BackendConfiguration(self._base_module_configuration, self, entry)
[email protected]3c703672013-03-19 23:06:51381 for entry in self._backends_name_to_backend_entry.values()]
382
383 def check_for_updates(self, backend_name):
384 """Return any configuration changes since the last check_for_updates call.
385
386 Args:
387 backend_name: A str containing the name of the backend to be checked for
388 updates.
389
390 Returns:
391 A set containing the changes that occured. See the *_CHANGED module
392 constants.
393 """
394 with self._update_lock:
[email protected]200fcb72013-07-20 01:18:36395 module_changes = self._base_module_configuration.check_for_updates()
396 if module_changes:
[email protected]3c703672013-03-19 23:06:51397 for backend_changes in self._changes.values():
[email protected]200fcb72013-07-20 01:18:36398 backend_changes.update(module_changes)
[email protected]3c703672013-03-19 23:06:51399 changes = self._changes[backend_name]
400 self._changes[backend_name] = set()
401 return changes
402
403
404class BackendConfiguration(object):
405 """Stores backend configuration information.
406
[email protected]200fcb72013-07-20 01:18:36407 This interface is and must remain identical to ModuleConfiguration.
[email protected]3c703672013-03-19 23:06:51408 """
409
[email protected]200fcb72013-07-20 01:18:36410 def __init__(self, module_configuration, backends_configuration,
[email protected]3c703672013-03-19 23:06:51411 backend_entry):
412 """Initializer for BackendConfiguration.
413
414 Args:
[email protected]200fcb72013-07-20 01:18:36415 module_configuration: A ModuleConfiguration to use.
[email protected]3c703672013-03-19 23:06:51416 backends_configuration: The BackendsConfiguration that tracks updates for
417 this BackendConfiguration.
418 backend_entry: A backendinfo.BackendEntry containing the backend
419 configuration.
420 """
[email protected]200fcb72013-07-20 01:18:36421 self._module_configuration = module_configuration
[email protected]3c703672013-03-19 23:06:51422 self._backends_configuration = backends_configuration
423 self._backend_entry = backend_entry
424
425 if backend_entry.dynamic:
426 self._basic_scaling = appinfo.BasicScaling(
427 max_instances=backend_entry.instances or 1)
428 self._manual_scaling = None
429 else:
430 self._basic_scaling = None
431 self._manual_scaling = appinfo.ManualScaling(
432 instances=backend_entry.instances or 1)
433 self._minor_version_id = ''.join(random.choice(string.digits) for _ in
434 range(18))
435
436 @property
437 def application_root(self):
438 """The directory containing the application e.g. "/home/user/myapp"."""
[email protected]200fcb72013-07-20 01:18:36439 return self._module_configuration.application_root
[email protected]3c703672013-03-19 23:06:51440
441 @property
442 def application(self):
[email protected]200fcb72013-07-20 01:18:36443 return self._module_configuration.application
[email protected]3c703672013-03-19 23:06:51444
445 @property
[email protected]982cc4d2014-04-04 22:36:48446 def partition(self):
447 return self._module_configuration.partition
448
449 @property
450 def application_external_name(self):
451 return self._module_configuration.application_external_name
452
453 @property
[email protected]3c703672013-03-19 23:06:51454 def api_version(self):
[email protected]200fcb72013-07-20 01:18:36455 return self._module_configuration.api_version
[email protected]3c703672013-03-19 23:06:51456
457 @property
[email protected]200fcb72013-07-20 01:18:36458 def module_name(self):
[email protected]3c703672013-03-19 23:06:51459 return self._backend_entry.name
460
461 @property
462 def major_version(self):
[email protected]200fcb72013-07-20 01:18:36463 return self._module_configuration.major_version
[email protected]3c703672013-03-19 23:06:51464
465 @property
[email protected]982cc4d2014-04-04 22:36:48466 def minor_version(self):
467 return self._minor_version_id
468
469 @property
[email protected]3c703672013-03-19 23:06:51470 def version_id(self):
[email protected]98325492014-01-15 21:40:06471 return '%s:%s.%s' % (
472 self.module_name,
[email protected]3c703672013-03-19 23:06:51473 self.major_version,
474 self._minor_version_id)
475
476 @property
477 def runtime(self):
[email protected]200fcb72013-07-20 01:18:36478 return self._module_configuration.runtime
[email protected]3c703672013-03-19 23:06:51479
480 @property
[email protected]982cc4d2014-04-04 22:36:48481 def effective_runtime(self):
482 return self._module_configuration.effective_runtime
483
484 @property
[email protected]3c703672013-03-19 23:06:51485 def threadsafe(self):
[email protected]200fcb72013-07-20 01:18:36486 return self._module_configuration.threadsafe
[email protected]3c703672013-03-19 23:06:51487
488 @property
489 def basic_scaling(self):
490 return self._basic_scaling
491
492 @property
493 def manual_scaling(self):
494 return self._manual_scaling
495
496 @property
497 def automatic_scaling(self):
498 return None
499
500 @property
501 def normalized_libraries(self):
[email protected]200fcb72013-07-20 01:18:36502 return self._module_configuration.normalized_libraries
[email protected]3c703672013-03-19 23:06:51503
504 @property
505 def skip_files(self):
[email protected]200fcb72013-07-20 01:18:36506 return self._module_configuration.skip_files
[email protected]3c703672013-03-19 23:06:51507
508 @property
509 def nobuild_files(self):
[email protected]200fcb72013-07-20 01:18:36510 return self._module_configuration.nobuild_files
[email protected]3c703672013-03-19 23:06:51511
512 @property
513 def error_handlers(self):
[email protected]200fcb72013-07-20 01:18:36514 return self._module_configuration.error_handlers
[email protected]3c703672013-03-19 23:06:51515
516 @property
517 def handlers(self):
518 if self._backend_entry.start:
519 return [appinfo.URLMap(
520 url='/_ah/start',
521 script=self._backend_entry.start,
[email protected]200fcb72013-07-20 01:18:36522 login='admin')] + self._module_configuration.handlers
523 return self._module_configuration.handlers
[email protected]3c703672013-03-19 23:06:51524
525 @property
526 def inbound_services(self):
[email protected]200fcb72013-07-20 01:18:36527 return self._module_configuration.inbound_services
[email protected]3c703672013-03-19 23:06:51528
529 @property
530 def env_variables(self):
[email protected]200fcb72013-07-20 01:18:36531 return self._module_configuration.env_variables
[email protected]3c703672013-03-19 23:06:51532
533 @property
534 def is_backend(self):
535 return True
536
537 def check_for_updates(self):
538 """Return any configuration changes since the last check_for_updates call.
539
540 Returns:
541 A set containing the changes that occured. See the *_CHANGED module
542 constants.
543 """
544 changes = self._backends_configuration.check_for_updates(
545 self._backend_entry.name)
546 if changes:
547 self._minor_version_id = ''.join(random.choice(string.digits) for _ in
548 range(18))
549 return changes
550
551
[email protected]6da8fcb2013-04-09 23:18:42552class DispatchConfiguration(object):
553 """Stores dispatcher configuration information."""
554
[email protected]21ed1cd2014-03-19 22:38:35555 def __init__(self, config_path):
556 self._config_path = config_path
557 self._mtime = os.path.getmtime(self._config_path)
558 self._process_dispatch_entries(self._parse_configuration(self._config_path))
[email protected]6da8fcb2013-04-09 23:18:42559
560 @staticmethod
561 def _parse_configuration(configuration_path):
562 # TODO: It probably makes sense to catch the exception raised
563 # by LoadSingleDispatch() and re-raise it using a module-specific exception.
564 with open(configuration_path) as f:
565 return dispatchinfo.LoadSingleDispatch(f)
566
567 def check_for_updates(self):
[email protected]21ed1cd2014-03-19 22:38:35568 mtime = os.path.getmtime(self._config_path)
[email protected]6da8fcb2013-04-09 23:18:42569 if mtime > self._mtime:
570 self._mtime = mtime
571 try:
[email protected]21ed1cd2014-03-19 22:38:35572 dispatch_info_external = self._parse_configuration(self._config_path)
[email protected]6da8fcb2013-04-09 23:18:42573 except Exception, e:
574 failure_message = str(e)
575 logging.error('Configuration is not valid: %s', failure_message)
576 return
577 self._process_dispatch_entries(dispatch_info_external)
578
579 def _process_dispatch_entries(self, dispatch_info_external):
580 path_only_entries = []
581 hostname_entries = []
582 for entry in dispatch_info_external.dispatch:
583 parsed_url = dispatchinfo.ParsedURL(entry.url)
584 if parsed_url.host:
585 hostname_entries.append(entry)
586 else:
[email protected]200fcb72013-07-20 01:18:36587 path_only_entries.append((parsed_url, entry.module))
[email protected]6da8fcb2013-04-09 23:18:42588 if hostname_entries:
589 logging.warning(
590 'Hostname routing is not supported by the development server. The '
591 'following dispatch entries will not match any requests:\n%s',
592 '\n\t'.join(str(entry) for entry in hostname_entries))
593 self._entries = path_only_entries
594
595 @property
596 def dispatch(self):
597 return self._entries
598
599
[email protected]3c703672013-03-19 23:06:51600class ApplicationConfiguration(object):
601 """Stores application configuration information."""
602
[email protected]21ed1cd2014-03-19 22:38:35603 def __init__(self, config_paths):
[email protected]3c703672013-03-19 23:06:51604 """Initializer for ApplicationConfiguration.
605
606 Args:
[email protected]21ed1cd2014-03-19 22:38:35607 config_paths: A list of strings containing the paths to yaml files,
608 or to directories containing them.
[email protected]3c703672013-03-19 23:06:51609 """
[email protected]200fcb72013-07-20 01:18:36610 self.modules = []
[email protected]6da8fcb2013-04-09 23:18:42611 self.dispatch = None
[email protected]21ed1cd2014-03-19 22:38:35612 # It's really easy to add a test case that passes in a string rather than
613 # a list of strings, so guard against that.
614 assert not isinstance(config_paths, basestring)
615 config_paths = self._config_files_from_paths(config_paths)
616 for config_path in config_paths:
617 # TODO: add support for backends.xml and dispatch.xml here
618 if (config_path.endswith('backends.yaml') or
619 config_path.endswith('backends.yml')):
[email protected]200fcb72013-07-20 01:18:36620 # TODO: Reuse the ModuleConfiguration created for the app.yaml
[email protected]3c703672013-03-19 23:06:51621 # instead of creating another one for the same file.
[email protected]200fcb72013-07-20 01:18:36622 self.modules.extend(
[email protected]21ed1cd2014-03-19 22:38:35623 BackendsConfiguration(config_path.replace('backends.y', 'app.y'),
624 config_path).get_backend_configurations())
625 elif (config_path.endswith('dispatch.yaml') or
626 config_path.endswith('dispatch.yml')):
[email protected]6da8fcb2013-04-09 23:18:42627 if self.dispatch:
628 raise errors.InvalidAppConfigError(
629 'Multiple dispatch.yaml files specified')
[email protected]21ed1cd2014-03-19 22:38:35630 self.dispatch = DispatchConfiguration(config_path)
[email protected]3c703672013-03-19 23:06:51631 else:
[email protected]21ed1cd2014-03-19 22:38:35632 module_configuration = ModuleConfiguration(config_path)
[email protected]200fcb72013-07-20 01:18:36633 self.modules.append(module_configuration)
634 application_ids = set(module.application
635 for module in self.modules)
[email protected]3c703672013-03-19 23:06:51636 if len(application_ids) > 1:
637 raise errors.InvalidAppConfigError(
638 'More than one application ID found: %s' %
639 ', '.join(sorted(application_ids)))
640
641 self._app_id = application_ids.pop()
[email protected]200fcb72013-07-20 01:18:36642 module_names = set()
643 for module in self.modules:
644 if module.module_name in module_names:
645 raise errors.InvalidAppConfigError('Duplicate module: %s' %
646 module.module_name)
647 module_names.add(module.module_name)
[email protected]6da8fcb2013-04-09 23:18:42648 if self.dispatch:
[email protected]2e4b3c82014-02-26 20:08:51649 if appinfo.DEFAULT_MODULE not in module_names:
[email protected]6da8fcb2013-04-09 23:18:42650 raise errors.InvalidAppConfigError(
[email protected]200fcb72013-07-20 01:18:36651 'A default module must be specified.')
652 missing_modules = (
653 set(module_name for _, module_name in self.dispatch.dispatch) -
654 module_names)
655 if missing_modules:
[email protected]6da8fcb2013-04-09 23:18:42656 raise errors.InvalidAppConfigError(
[email protected]200fcb72013-07-20 01:18:36657 'Modules %s specified in dispatch.yaml are not defined by a yaml '
658 'file.' % sorted(missing_modules))
[email protected]3c703672013-03-19 23:06:51659
[email protected]21ed1cd2014-03-19 22:38:35660 def _config_files_from_paths(self, config_paths):
661 """Return a list of the configuration files found in the given paths.
662
663 For any path that is a directory, the returned list will contain the
664 configuration files (app.yaml and optionally backends.yaml) found in that
665 directory. If the directory is a Java app (contains a subdirectory
666 WEB-INF with web.xml and application-web.xml files), then the returned
667 list will contain the path to the application-web.xml file, which is treated
668 as if it included web.xml. Paths that are not directories are added to the
669 returned list as is.
670
671 Args:
672 config_paths: a list of strings that are file or directory paths.
673
674 Returns:
675 A list of strings that are file paths.
676 """
677 config_files = []
678 for path in config_paths:
679 config_files += (
680 self._config_files_from_dir(path) if os.path.isdir(path) else [path])
681 return config_files
682
683 def _config_files_from_dir(self, dir_path):
684 """Return a list of the configuration files found in the given directory.
685
686 If the directory contains a subdirectory WEB-INF then we expect to find
687 web.xml and application-web.xml in that subdirectory. The returned list
688 will consist of the path to application-web.xml, which we treat as if it
689 included xml.
690
691 Otherwise, we expect to find an app.yaml and optionally a backends.yaml,
692 and we return those in the list.
693
694 Args:
695 dir_path: a string that is the path to a directory.
696
697 Returns:
698 A list of strings that are file paths.
699 """
700 web_inf = os.path.join(dir_path, 'WEB-INF')
701 if java_supported() and os.path.isdir(web_inf):
702 return self._config_files_from_web_inf_dir(web_inf)
703 app_yamls = self._files_in_dir_matching(dir_path, ['app.yaml', 'app.yml'])
704 if not app_yamls:
705 or_web_inf = ' or a WEB-INF subdirectory' if java_supported() else ''
706 raise errors.AppConfigNotFoundError(
707 '"%s" is a directory but does not contain app.yaml or app.yml%s' %
708 (dir_path, or_web_inf))
709 backend_yamls = self._files_in_dir_matching(
710 dir_path, ['backends.yaml', 'backends.yml'])
711 return app_yamls + backend_yamls
712
713 def _config_files_from_web_inf_dir(self, web_inf):
714 required = ['appengine-web.xml', 'web.xml']
715 missing = [f for f in required
716 if not os.path.exists(os.path.join(web_inf, f))]
717 if missing:
718 raise errors.AppConfigNotFoundError(
719 'The "%s" subdirectory exists but is missing %s' %
720 (web_inf, ' and '.join(missing)))
721 return [os.path.join(web_inf, required[0])]
722
723 @staticmethod
724 def _files_in_dir_matching(dir_path, names):
725 abs_names = [os.path.join(dir_path, name) for name in names]
726 files = [f for f in abs_names if os.path.exists(f)]
727 if len(files) > 1:
728 raise errors.InvalidAppConfigError(
729 'Directory "%s" contains %s' % (dir_path, ' and '.join(names)))
730 return files
731
[email protected]3c703672013-03-19 23:06:51732 @property
733 def app_id(self):
734 return self._app_id
[email protected]21ed1cd2014-03-19 22:38:35735
736
737def get_app_error_file(module_configuration):
738 """Returns application specific file to handle errors.
739
740 Dev AppServer only supports 'default' error code.
741
742 Args:
743 module_configuration: ModuleConfiguration.
744
745 Returns:
746 A string containing full path to error handler file or
747 None if no 'default' error handler is specified.
748 """
749 for error_handler in module_configuration.error_handlers or []:
750 if not error_handler.error_code or error_handler.error_code == 'default':
751 return os.path.join(module_configuration.application_root,
752 error_handler.file)
753 return None