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, index.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
.client
.services
import port_manager
36 from google
.appengine
.tools
import queue_xml_parser
37 from google
.appengine
.tools
import yaml_translator
38 from google
.appengine
.tools
.devappserver2
import errors
40 # Constants passed to functions registered with
41 # ModuleConfiguration.add_change_callback.
42 NORMALIZED_LIBRARIES_CHANGED
= 1
43 SKIP_FILES_CHANGED
= 2
45 INBOUND_SERVICES_CHANGED
= 4
46 ENV_VARIABLES_CHANGED
= 5
47 ERROR_HANDLERS_CHANGED
= 6
48 NOBUILD_FILES_CHANGED
= 7
51 _HEALTH_CHECK_DEFAULTS
= {
52 'enable_health_check': True,
53 'check_interval_sec': 5,
55 'unhealthy_threshold': 2,
56 'healthy_threshold': 2,
57 'restart_threshold': 60,
63 """True if this SDK supports running Java apps in the dev appserver."""
64 java_dir
= os
.path
.join(os
.path
.dirname(os
.path
.dirname(__file__
)), 'java')
65 return os
.path
.isdir(java_dir
)
68 class ModuleConfiguration(object):
69 """Stores module configuration information.
71 Most configuration options are mutable and may change any time
72 check_for_updates is called. Client code must be able to cope with these
75 Other properties are immutable (see _IMMUTABLE_PROPERTIES) and are guaranteed
76 to be constant for the lifetime of the instance.
79 _IMMUTABLE_PROPERTIES
= [
80 ('application', 'application'),
81 ('version', 'major_version'),
82 ('runtime', 'runtime'),
83 ('threadsafe', 'threadsafe'),
84 ('module', 'module_name'),
85 ('basic_scaling', 'basic_scaling'),
86 ('manual_scaling', 'manual_scaling'),
87 ('automatic_scaling', 'automatic_scaling')]
89 def __init__(self
, config_path
, app_id
=None):
90 """Initializer for ModuleConfiguration.
93 config_path: A string containing the full path of the yaml or xml file
94 containing the configuration for this module.
95 app_id: A string that is the application id, or None if the application id
96 from the yaml or xml file should be used.
98 self
._config
_path
= config_path
99 self
._forced
_app
_id
= app_id
100 root
= os
.path
.dirname(config_path
)
101 self
._is
_java
= os
.path
.normpath(config_path
).endswith(
102 os
.sep
+ 'WEB-INF' + os
.sep
+ 'appengine-web.xml')
104 # We assume Java's XML-based config files only if config_path is
105 # something like /foo/bar/WEB-INF/appengine-web.xml. In this case,
106 # the application root is /foo/bar. Other apps, configured with YAML,
107 # have something like /foo/bar/app.yaml, with application root /foo/bar.
108 root
= os
.path
.dirname(root
)
109 self
._application
_root
= os
.path
.realpath(root
)
110 self
._last
_failure
_message
= None
112 self
._app
_info
_external
, files_to_check
= self
._parse
_configuration
(
114 self
._mtimes
= self
._get
_mtimes
(files_to_check
)
115 self
._application
= '%s~%s' % (self
.partition
,
116 self
.application_external_name
)
117 self
._api
_version
= self
._app
_info
_external
.api_version
118 self
._module
_name
= self
._app
_info
_external
.module
119 self
._version
= self
._app
_info
_external
.version
120 self
._threadsafe
= self
._app
_info
_external
.threadsafe
121 self
._basic
_scaling
= self
._app
_info
_external
.basic_scaling
122 self
._manual
_scaling
= self
._app
_info
_external
.manual_scaling
123 self
._automatic
_scaling
= self
._app
_info
_external
.automatic_scaling
124 self
._runtime
= self
._app
_info
_external
.runtime
125 if self
._runtime
== 'python':
127 'The "python" runtime specified in "%s" is not supported - the '
128 '"python27" runtime will be used instead. A description of the '
129 'differences between the two can be found here:\n'
130 'https://developers.google.com/appengine/docs/python/python25/diff27',
132 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
135 self
._forwarded
_ports
= {}
136 if self
.runtime
== 'vm':
137 vm_settings
= self
._app
_info
_external
.vm_settings
139 ports
= vm_settings
.get('forwarded_ports')
141 logging
.debug('setting forwarded ports %s', ports
)
142 pm
= port_manager
.PortManager()
143 pm
.Add(ports
, 'forwarded')
144 self
._forwarded
_ports
= pm
.GetAllMappedPorts()
146 self
._translate
_configuration
_files
()
148 self
._vm
_health
_check
= _set_health_check_defaults(
149 self
._app
_info
_external
.vm_health_check
)
152 def application_root(self
):
153 """The directory containing the application e.g. "/home/user/myapp"."""
154 return self
._application
_root
157 def application(self
):
158 return self
._application
165 def application_external_name(self
):
166 return self
._app
_info
_external
.application
169 def api_version(self
):
170 return self
._api
_version
173 def module_name(self
):
174 return self
._module
_name
or appinfo
.DEFAULT_MODULE
177 def major_version(self
):
181 def minor_version(self
):
182 return self
._minor
_version
_id
185 def version_id(self
):
186 if self
.module_name
== appinfo
.DEFAULT_MODULE
:
189 self
._minor
_version
_id
)
191 return '%s:%s.%s' % (
194 self
._minor
_version
_id
)
201 def effective_runtime(self
):
202 return self
._app
_info
_external
.GetEffectiveRuntime()
205 def forwarded_ports(self
):
206 """A dictionary with forwarding rules as host_port => container_port."""
207 return self
._forwarded
_ports
210 def threadsafe(self
):
211 return self
._threadsafe
214 def basic_scaling(self
):
215 return self
._basic
_scaling
218 def manual_scaling(self
):
219 return self
._manual
_scaling
222 def automatic_scaling(self
):
223 return self
._automatic
_scaling
226 def normalized_libraries(self
):
227 return self
._app
_info
_external
.GetNormalizedLibraries()
230 def skip_files(self
):
231 return self
._app
_info
_external
.skip_files
234 def nobuild_files(self
):
235 return self
._app
_info
_external
.nobuild_files
238 def error_handlers(self
):
239 return self
._app
_info
_external
.error_handlers
243 return self
._app
_info
_external
.handlers
246 def inbound_services(self
):
247 return self
._app
_info
_external
.inbound_services
250 def env_variables(self
):
251 return self
._app
_info
_external
.env_variables
254 def is_backend(self
):
258 def config_path(self
):
259 return self
._config
_path
262 def vm_health_check(self
):
263 return self
._vm
_health
_check
265 def check_for_updates(self
):
266 """Return any configuration changes since the last check_for_updates call.
269 A set containing the changes that occured. See the *_CHANGED module
272 new_mtimes
= self
._get
_mtimes
(self
._mtimes
.keys())
273 if new_mtimes
== self
._mtimes
:
277 app_info_external
, files_to_check
= self
._parse
_configuration
(
280 failure_message
= str(e
)
281 if failure_message
!= self
._last
_failure
_message
:
282 logging
.error('Configuration is not valid: %s', failure_message
)
283 self
._last
_failure
_message
= failure_message
285 self
._last
_failure
_message
= None
287 self
._mtimes
= self
._get
_mtimes
(files_to_check
)
289 for app_info_attribute
, self_attribute
in self
._IMMUTABLE
_PROPERTIES
:
290 app_info_value
= getattr(app_info_external
, app_info_attribute
)
291 self_value
= getattr(self
, self_attribute
)
292 if (app_info_value
== self_value
or
293 app_info_value
== getattr(self
._app
_info
_external
,
294 app_info_attribute
)):
295 # Only generate a warning if the value is both different from the
296 # immutable value *and* different from the last loaded value.
299 if isinstance(app_info_value
, types
.StringTypes
):
300 logging
.warning('Restart the development module to see updates to "%s" '
306 logging
.warning('Restart the development module to see updates to "%s"',
310 if (app_info_external
.GetNormalizedLibraries() !=
311 self
.normalized_libraries
):
312 changes
.add(NORMALIZED_LIBRARIES_CHANGED
)
313 if app_info_external
.skip_files
!= self
.skip_files
:
314 changes
.add(SKIP_FILES_CHANGED
)
315 if app_info_external
.nobuild_files
!= self
.nobuild_files
:
316 changes
.add(NOBUILD_FILES_CHANGED
)
317 if app_info_external
.handlers
!= self
.handlers
:
318 changes
.add(HANDLERS_CHANGED
)
319 if app_info_external
.inbound_services
!= self
.inbound_services
:
320 changes
.add(INBOUND_SERVICES_CHANGED
)
321 if app_info_external
.env_variables
!= self
.env_variables
:
322 changes
.add(ENV_VARIABLES_CHANGED
)
323 if app_info_external
.error_handlers
!= self
.error_handlers
:
324 changes
.add(ERROR_HANDLERS_CHANGED
)
326 self
._app
_info
_external
= app_info_external
328 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
333 def _get_mtimes(filenames
):
334 filename_to_mtime
= {}
335 for filename
in filenames
:
337 filename_to_mtime
[filename
] = os
.path
.getmtime(filename
)
339 # Ignore deleted includes.
340 if e
.errno
!= errno
.ENOENT
:
342 return filename_to_mtime
344 def _parse_configuration(self
, configuration_path
):
345 """Parse a configuration file (like app.yaml or appengine-web.xml).
348 configuration_path: A string containing the full path of the yaml file
349 containing the configuration for this module.
352 A tuple where the first element is the parsed appinfo.AppInfoExternal
353 object and the second element is a list of the paths of the files that
354 were used to produce it, namely the input configuration_path and any
355 other file that was included from that one.
358 config
, files
= self
._parse
_java
_configuration
(configuration_path
)
360 with
open(configuration_path
) as f
:
361 config
, files
= appinfo_includes
.ParseAndReturnIncludePaths(f
)
362 if self
._forced
_app
_id
:
363 config
.application
= self
._forced
_app
_id
364 return config
, [configuration_path
] + files
366 def _parse_java_configuration(self
, app_engine_web_xml_path
):
367 """Parse appengine-web.xml and web.xml.
370 app_engine_web_xml_path: A string containing the full path of the
371 .../WEB-INF/appengine-web.xml file. The corresponding
372 .../WEB-INF/web.xml file must also be present.
375 A tuple where the first element is the parsed appinfo.AppInfoExternal
376 object and the second element is a list of the paths of the files that
377 were used to produce it, namely the input appengine-web.xml file and the
378 corresponding web.xml file.
380 with
open(app_engine_web_xml_path
) as f
:
381 app_engine_web_xml_str
= f
.read()
382 web_inf_dir
= os
.path
.dirname(app_engine_web_xml_path
)
383 web_xml_path
= os
.path
.join(web_inf_dir
, 'web.xml')
384 with
open(web_xml_path
) as f
:
385 web_xml_str
= f
.read()
387 for _
, _
, filenames
in os
.walk(self
.application_root
):
388 if any(f
.endswith('.jsp') for f
in filenames
):
391 app_yaml_str
= yaml_translator
.TranslateXmlToYamlForDevAppServer(
392 app_engine_web_xml_str
, web_xml_str
, has_jsps
, self
.application_root
)
393 config
= appinfo
.LoadSingleAppInfo(app_yaml_str
)
394 return config
, [app_engine_web_xml_path
, web_xml_path
]
396 def _translate_configuration_files(self
):
397 """Writes YAML equivalents of certain XML configuration files."""
398 # For the most part we translate files in memory rather than writing out
399 # translations. But since the task queue stub (taskqueue_stub.py)
400 # reads queue.yaml directly rather than being configured with it, we need
401 # to write a translation for the stub to find.
402 # This means that we won't detect a change to the queue.xml, but we don't
403 # currently have logic to react to changes to queue.yaml either.
404 web_inf
= os
.path
.join(self
._application
_root
, 'WEB-INF')
405 queue_xml_file
= os
.path
.join(web_inf
, 'queue.xml')
406 if os
.path
.exists(queue_xml_file
):
407 appengine_generated
= os
.path
.join(web_inf
, 'appengine-generated')
408 if not os
.path
.exists(appengine_generated
):
409 os
.mkdir(appengine_generated
)
410 queue_yaml_file
= os
.path
.join(appengine_generated
, 'queue.yaml')
411 with
open(queue_xml_file
) as f
:
413 queue_yaml
= queue_xml_parser
.GetQueueYaml(None, queue_xml
)
414 with
open(queue_yaml_file
, 'w') as f
:
418 def _set_health_check_defaults(vm_health_check
):
419 """Sets default values for any missing attributes in VmHealthCheck.
421 These defaults need to be kept up to date with the production values in
425 vm_health_check: An instance of appinfo.VmHealthCheck or None.
428 An instance of appinfo.VmHealthCheck
430 if not vm_health_check
:
431 vm_health_check
= appinfo
.VmHealthCheck()
432 for k
, v
in _HEALTH_CHECK_DEFAULTS
.iteritems():
433 if getattr(vm_health_check
, k
) is None:
434 setattr(vm_health_check
, k
, v
)
435 return vm_health_check
438 class BackendsConfiguration(object):
439 """Stores configuration information for a backends.yaml file."""
441 def __init__(self
, app_config_path
, backend_config_path
, app_id
=None):
442 """Initializer for BackendsConfiguration.
445 app_config_path: A string containing the full path of the yaml file
446 containing the configuration for this module.
447 backend_config_path: A string containing the full path of the
448 backends.yaml file containing the configuration for backends.
449 app_id: A string that is the application id, or None if the application id
450 from the yaml or xml file should be used.
452 self
._update
_lock
= threading
.RLock()
453 self
._base
_module
_configuration
= ModuleConfiguration(
454 app_config_path
, app_id
)
455 backend_info_external
= self
._parse
_configuration
(
458 self
._backends
_name
_to
_backend
_entry
= {}
459 for backend
in backend_info_external
.backends
or []:
460 self
._backends
_name
_to
_backend
_entry
[backend
.name
] = backend
461 self
._changes
= dict(
462 (backend_name
, set())
463 for backend_name
in self
._backends
_name
_to
_backend
_entry
)
466 def _parse_configuration(configuration_path
):
467 # TODO: It probably makes sense to catch the exception raised
468 # by Parse() and re-raise it using a module-specific exception.
469 with
open(configuration_path
) as f
:
470 return backendinfo
.LoadBackendInfo(f
)
472 def get_backend_configurations(self
):
473 return [BackendConfiguration(self
._base
_module
_configuration
, self
, entry
)
474 for entry
in self
._backends
_name
_to
_backend
_entry
.values()]
476 def check_for_updates(self
, backend_name
):
477 """Return any configuration changes since the last check_for_updates call.
480 backend_name: A str containing the name of the backend to be checked for
484 A set containing the changes that occured. See the *_CHANGED module
487 with self
._update
_lock
:
488 module_changes
= self
._base
_module
_configuration
.check_for_updates()
490 for backend_changes
in self
._changes
.values():
491 backend_changes
.update(module_changes
)
492 changes
= self
._changes
[backend_name
]
493 self
._changes
[backend_name
] = set()
497 class BackendConfiguration(object):
498 """Stores backend configuration information.
500 This interface is and must remain identical to ModuleConfiguration.
503 def __init__(self
, module_configuration
, backends_configuration
,
505 """Initializer for BackendConfiguration.
508 module_configuration: A ModuleConfiguration to use.
509 backends_configuration: The BackendsConfiguration that tracks updates for
510 this BackendConfiguration.
511 backend_entry: A backendinfo.BackendEntry containing the backend
514 self
._module
_configuration
= module_configuration
515 self
._backends
_configuration
= backends_configuration
516 self
._backend
_entry
= backend_entry
518 if backend_entry
.dynamic
:
519 self
._basic
_scaling
= appinfo
.BasicScaling(
520 max_instances
=backend_entry
.instances
or 1)
521 self
._manual
_scaling
= None
523 self
._basic
_scaling
= None
524 self
._manual
_scaling
= appinfo
.ManualScaling(
525 instances
=backend_entry
.instances
or 1)
526 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
530 def application_root(self
):
531 """The directory containing the application e.g. "/home/user/myapp"."""
532 return self
._module
_configuration
.application_root
535 def application(self
):
536 return self
._module
_configuration
.application
540 return self
._module
_configuration
.partition
543 def application_external_name(self
):
544 return self
._module
_configuration
.application_external_name
547 def api_version(self
):
548 return self
._module
_configuration
.api_version
551 def module_name(self
):
552 return self
._backend
_entry
.name
555 def major_version(self
):
556 return self
._module
_configuration
.major_version
559 def minor_version(self
):
560 return self
._minor
_version
_id
563 def version_id(self
):
564 return '%s:%s.%s' % (
567 self
._minor
_version
_id
)
571 return self
._module
_configuration
.runtime
574 def effective_runtime(self
):
575 return self
._module
_configuration
.effective_runtime
578 def forwarded_ports(self
):
579 return self
._module
_configuration
.forwarded_ports
582 def threadsafe(self
):
583 return self
._module
_configuration
.threadsafe
586 def basic_scaling(self
):
587 return self
._basic
_scaling
590 def manual_scaling(self
):
591 return self
._manual
_scaling
594 def automatic_scaling(self
):
598 def normalized_libraries(self
):
599 return self
._module
_configuration
.normalized_libraries
602 def skip_files(self
):
603 return self
._module
_configuration
.skip_files
606 def nobuild_files(self
):
607 return self
._module
_configuration
.nobuild_files
610 def error_handlers(self
):
611 return self
._module
_configuration
.error_handlers
615 if self
._backend
_entry
.start
:
616 return [appinfo
.URLMap(
618 script
=self
._backend
_entry
.start
,
619 login
='admin')] + self
._module
_configuration
.handlers
620 return self
._module
_configuration
.handlers
623 def inbound_services(self
):
624 return self
._module
_configuration
.inbound_services
627 def env_variables(self
):
628 return self
._module
_configuration
.env_variables
631 def is_backend(self
):
635 def config_path(self
):
636 return self
._module
_configuration
.config_path
639 def vm_health_check(self
):
640 return self
._module
_configuration
.vm_health_check
642 def check_for_updates(self
):
643 """Return any configuration changes since the last check_for_updates call.
646 A set containing the changes that occured. See the *_CHANGED module
649 changes
= self
._backends
_configuration
.check_for_updates(
650 self
._backend
_entry
.name
)
652 self
._minor
_version
_id
= ''.join(random
.choice(string
.digits
) for _
in
657 class DispatchConfiguration(object):
658 """Stores dispatcher configuration information."""
660 def __init__(self
, config_path
):
661 self
._config
_path
= config_path
662 self
._mtime
= os
.path
.getmtime(self
._config
_path
)
663 self
._process
_dispatch
_entries
(self
._parse
_configuration
(self
._config
_path
))
666 def _parse_configuration(configuration_path
):
667 # TODO: It probably makes sense to catch the exception raised
668 # by LoadSingleDispatch() and re-raise it using a module-specific exception.
669 with
open(configuration_path
) as f
:
670 return dispatchinfo
.LoadSingleDispatch(f
)
672 def check_for_updates(self
):
673 mtime
= os
.path
.getmtime(self
._config
_path
)
674 if mtime
> self
._mtime
:
677 dispatch_info_external
= self
._parse
_configuration
(self
._config
_path
)
679 failure_message
= str(e
)
680 logging
.error('Configuration is not valid: %s', failure_message
)
682 self
._process
_dispatch
_entries
(dispatch_info_external
)
684 def _process_dispatch_entries(self
, dispatch_info_external
):
685 path_only_entries
= []
686 hostname_entries
= []
687 for entry
in dispatch_info_external
.dispatch
:
688 parsed_url
= dispatchinfo
.ParsedURL(entry
.url
)
690 hostname_entries
.append(entry
)
692 path_only_entries
.append((parsed_url
, entry
.module
))
695 'Hostname routing is not supported by the development server. The '
696 'following dispatch entries will not match any requests:\n%s',
697 '\n\t'.join(str(entry
) for entry
in hostname_entries
))
698 self
._entries
= path_only_entries
705 class ApplicationConfiguration(object):
706 """Stores application configuration information."""
708 def __init__(self
, config_paths
, app_id
=None):
709 """Initializer for ApplicationConfiguration.
712 config_paths: A list of strings containing the paths to yaml files,
713 or to directories containing them.
714 app_id: A string that is the application id, or None if the application id
715 from the yaml or xml file should be used.
719 # It's really easy to add a test case that passes in a string rather than
720 # a list of strings, so guard against that.
721 assert not isinstance(config_paths
, basestring
)
722 config_paths
= self
._config
_files
_from
_paths
(config_paths
)
723 for config_path
in config_paths
:
724 # TODO: add support for backends.xml and dispatch.xml here
725 if (config_path
.endswith('backends.yaml') or
726 config_path
.endswith('backends.yml')):
727 # TODO: Reuse the ModuleConfiguration created for the app.yaml
728 # instead of creating another one for the same file.
729 app_yaml
= config_path
.replace('backends.y', 'app.y')
731 BackendsConfiguration(
732 app_yaml
, config_path
, app_id
).get_backend_configurations())
733 elif (config_path
.endswith('dispatch.yaml') or
734 config_path
.endswith('dispatch.yml')):
736 raise errors
.InvalidAppConfigError(
737 'Multiple dispatch.yaml files specified')
738 self
.dispatch
= DispatchConfiguration(config_path
)
740 module_configuration
= ModuleConfiguration(config_path
, app_id
)
741 self
.modules
.append(module_configuration
)
742 application_ids
= set(module
.application
743 for module
in self
.modules
)
744 if len(application_ids
) > 1:
745 raise errors
.InvalidAppConfigError(
746 'More than one application ID found: %s' %
747 ', '.join(sorted(application_ids
)))
749 self
._app
_id
= application_ids
.pop()
751 for module
in self
.modules
:
752 if module
.module_name
in module_names
:
753 raise errors
.InvalidAppConfigError('Duplicate module: %s' %
755 module_names
.add(module
.module_name
)
757 if appinfo
.DEFAULT_MODULE
not in module_names
:
758 raise errors
.InvalidAppConfigError(
759 'A default module must be specified.')
761 set(module_name
for _
, module_name
in self
.dispatch
.dispatch
) -
764 raise errors
.InvalidAppConfigError(
765 'Modules %s specified in dispatch.yaml are not defined by a yaml '
766 'file.' % sorted(missing_modules
))
768 def _config_files_from_paths(self
, config_paths
):
769 """Return a list of the configuration files found in the given paths.
771 For any path that is a directory, the returned list will contain the
772 configuration files (app.yaml and optionally backends.yaml) found in that
773 directory. If the directory is a Java app (contains a subdirectory
774 WEB-INF with web.xml and application-web.xml files), then the returned
775 list will contain the path to the application-web.xml file, which is treated
776 as if it included web.xml. Paths that are not directories are added to the
780 config_paths: a list of strings that are file or directory paths.
783 A list of strings that are file paths.
786 for path
in config_paths
:
788 self
._config
_files
_from
_dir
(path
) if os
.path
.isdir(path
) else [path
])
791 def _config_files_from_dir(self
, dir_path
):
792 """Return a list of the configuration files found in the given directory.
794 If the directory contains a subdirectory WEB-INF then we expect to find
795 web.xml and application-web.xml in that subdirectory. The returned list
796 will consist of the path to application-web.xml, which we treat as if it
799 Otherwise, we expect to find an app.yaml and optionally a backends.yaml,
800 and we return those in the list.
803 dir_path: a string that is the path to a directory.
806 A list of strings that are file paths.
808 web_inf
= os
.path
.join(dir_path
, 'WEB-INF')
809 if java_supported() and os
.path
.isdir(web_inf
):
810 return self
._config
_files
_from
_web
_inf
_dir
(web_inf
)
811 app_yamls
= self
._files
_in
_dir
_matching
(dir_path
, ['app.yaml', 'app.yml'])
813 or_web_inf
= ' or a WEB-INF subdirectory' if java_supported() else ''
814 raise errors
.AppConfigNotFoundError(
815 '"%s" is a directory but does not contain app.yaml or app.yml%s' %
816 (dir_path
, or_web_inf
))
817 backend_yamls
= self
._files
_in
_dir
_matching
(
818 dir_path
, ['backends.yaml', 'backends.yml'])
819 return app_yamls
+ backend_yamls
821 def _config_files_from_web_inf_dir(self
, web_inf
):
822 required
= ['appengine-web.xml', 'web.xml']
823 missing
= [f
for f
in required
824 if not os
.path
.exists(os
.path
.join(web_inf
, f
))]
826 raise errors
.AppConfigNotFoundError(
827 'The "%s" subdirectory exists but is missing %s' %
828 (web_inf
, ' and '.join(missing
)))
829 return [os
.path
.join(web_inf
, required
[0])]
832 def _files_in_dir_matching(dir_path
, names
):
833 abs_names
= [os
.path
.join(dir_path
, name
) for name
in names
]
834 files
= [f
for f
in abs_names
if os
.path
.exists(f
)]
836 raise errors
.InvalidAppConfigError(
837 'Directory "%s" contains %s' % (dir_path
, ' and '.join(names
)))
845 def get_app_error_file(module_configuration
):
846 """Returns application specific file to handle errors.
848 Dev AppServer only supports 'default' error code.
851 module_configuration: ModuleConfiguration.
854 A string containing full path to error handler file or
855 None if no 'default' error handler is specified.
857 for error_handler
in module_configuration
.error_handlers
or []:
858 if not error_handler
.error_code
or error_handler
.error_code
== 'default':
859 return os
.path
.join(module_configuration
.application_root
,