[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 1 | #!/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 | |
| 22 | import errno |
| 23 | import logging |
| 24 | import os |
| 25 | import os.path |
| 26 | import random |
| 27 | import string |
| 28 | import threading |
| 29 | import types |
| 30 | |
| 31 | from google.appengine.api import appinfo |
| 32 | from google.appengine.api import appinfo_includes |
| 33 | from google.appengine.api import backendinfo |
[email protected] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 34 | from google.appengine.api import dispatchinfo |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 35 | from google.appengine.tools import yaml_translator |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 36 | from google.appengine.tools.devappserver2 import errors |
| 37 | |
| 38 | # Constants passed to functions registered with |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 39 | # ModuleConfiguration.add_change_callback. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 40 | NORMALIZED_LIBRARIES_CHANGED = 1 |
| 41 | SKIP_FILES_CHANGED = 2 |
| 42 | HANDLERS_CHANGED = 3 |
| 43 | INBOUND_SERVICES_CHANGED = 4 |
| 44 | ENV_VARIABLES_CHANGED = 5 |
| 45 | ERROR_HANDLERS_CHANGED = 6 |
| 46 | NOBUILD_FILES_CHANGED = 7 |
| 47 | |
| 48 | |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 49 | def 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 55 | class ModuleConfiguration(object): |
| 56 | """Stores module configuration information. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 57 | |
| 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 71 | ('module', 'module_name'), |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 72 | ('basic_scaling', 'basic_scaling'), |
| 73 | ('manual_scaling', 'manual_scaling'), |
| 74 | ('automatic_scaling', 'automatic_scaling')] |
| 75 | |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 76 | def __init__(self, config_path): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 77 | """Initializer for ModuleConfiguration. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 78 | |
| 79 | Args: |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 80 | config_path: A string containing the full path of the yaml or xml file |
| 81 | containing the configuration for this module. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 82 | """ |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 83 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 94 | self._last_failure_message = None |
| 95 | |
| 96 | self._app_info_external, files_to_check = self._parse_configuration( |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 97 | self._config_path) |
| 98 | self._mtimes = self._get_mtimes(files_to_check) |
[email protected] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 99 | self._application = '%s~%s' % (self.partition, |
| 100 | self.application_external_name) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 101 | self._api_version = self._app_info_external.api_version |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 102 | self._module_name = self._app_info_external.module |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 103 | 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] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 115 | self._config_path) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 116 | 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] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 129 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 137 | def api_version(self): |
| 138 | return self._api_version |
| 139 | |
| 140 | @property |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 141 | def module_name(self): |
[email protected] | 2e4b3c8 | 2014-02-26 20:08:51 | [diff] [blame] | 142 | return self._module_name or appinfo.DEFAULT_MODULE |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 143 | |
| 144 | @property |
| 145 | def major_version(self): |
| 146 | return self._version |
| 147 | |
| 148 | @property |
[email protected] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 149 | def minor_version(self): |
| 150 | return self._minor_version_id |
| 151 | |
| 152 | @property |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 153 | def version_id(self): |
[email protected] | 2e4b3c8 | 2014-02-26 20:08:51 | [diff] [blame] | 154 | if self.module_name == appinfo.DEFAULT_MODULE: |
[email protected] | 9832549 | 2014-01-15 21:40:06 | [diff] [blame] | 155 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 163 | |
| 164 | @property |
| 165 | def runtime(self): |
| 166 | return self._runtime |
| 167 | |
| 168 | @property |
[email protected] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 169 | def effective_runtime(self): |
| 170 | return self._app_info_external.GetEffectiveRuntime() |
| 171 | |
| 172 | @property |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 173 | 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] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 233 | self._config_path) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 234 | 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] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 242 | self._mtimes = self._get_mtimes(files_to_check) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 243 | |
| 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 255 | logging.warning('Restart the development module to see updates to "%s" ' |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 256 | '["%s" => "%s"]', |
| 257 | app_info_attribute, |
| 258 | self_value, |
| 259 | app_info_value) |
| 260 | else: |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 261 | logging.warning('Restart the development module to see updates to "%s"', |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 262 | 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] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 299 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 319 | @staticmethod |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 320 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 346 | |
| 347 | |
| 348 | class BackendsConfiguration(object): |
| 349 | """Stores configuration information for a backends.yaml file.""" |
| 350 | |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 351 | def __init__(self, app_config_path, backend_config_path): |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 352 | """Initializer for BackendsConfiguration. |
| 353 | |
| 354 | Args: |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 355 | app_config_path: A string containing the full path of the yaml file |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 356 | containing the configuration for this module. |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 357 | backend_config_path: A string containing the full path of the |
| 358 | backends.yaml file containing the configuration for backends. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 359 | """ |
| 360 | self._update_lock = threading.RLock() |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 361 | self._base_module_configuration = ModuleConfiguration(app_config_path) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 362 | backend_info_external = self._parse_configuration( |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 363 | backend_config_path) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 364 | |
| 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 380 | return [BackendConfiguration(self._base_module_configuration, self, entry) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 381 | 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 395 | module_changes = self._base_module_configuration.check_for_updates() |
| 396 | if module_changes: |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 397 | for backend_changes in self._changes.values(): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 398 | backend_changes.update(module_changes) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 399 | changes = self._changes[backend_name] |
| 400 | self._changes[backend_name] = set() |
| 401 | return changes |
| 402 | |
| 403 | |
| 404 | class BackendConfiguration(object): |
| 405 | """Stores backend configuration information. |
| 406 | |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 407 | This interface is and must remain identical to ModuleConfiguration. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 408 | """ |
| 409 | |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 410 | def __init__(self, module_configuration, backends_configuration, |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 411 | backend_entry): |
| 412 | """Initializer for BackendConfiguration. |
| 413 | |
| 414 | Args: |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 415 | module_configuration: A ModuleConfiguration to use. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 416 | 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 421 | self._module_configuration = module_configuration |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 422 | 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 439 | return self._module_configuration.application_root |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 440 | |
| 441 | @property |
| 442 | def application(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 443 | return self._module_configuration.application |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 444 | |
| 445 | @property |
[email protected] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 446 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 454 | def api_version(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 455 | return self._module_configuration.api_version |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 456 | |
| 457 | @property |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 458 | def module_name(self): |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 459 | return self._backend_entry.name |
| 460 | |
| 461 | @property |
| 462 | def major_version(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 463 | return self._module_configuration.major_version |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 464 | |
| 465 | @property |
[email protected] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 466 | def minor_version(self): |
| 467 | return self._minor_version_id |
| 468 | |
| 469 | @property |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 470 | def version_id(self): |
[email protected] | 9832549 | 2014-01-15 21:40:06 | [diff] [blame] | 471 | return '%s:%s.%s' % ( |
| 472 | self.module_name, |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 473 | self.major_version, |
| 474 | self._minor_version_id) |
| 475 | |
| 476 | @property |
| 477 | def runtime(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 478 | return self._module_configuration.runtime |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 479 | |
| 480 | @property |
[email protected] | 982cc4d | 2014-04-04 22:36:48 | [diff] [blame^] | 481 | def effective_runtime(self): |
| 482 | return self._module_configuration.effective_runtime |
| 483 | |
| 484 | @property |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 485 | def threadsafe(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 486 | return self._module_configuration.threadsafe |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 487 | |
| 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 502 | return self._module_configuration.normalized_libraries |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 503 | |
| 504 | @property |
| 505 | def skip_files(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 506 | return self._module_configuration.skip_files |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 507 | |
| 508 | @property |
| 509 | def nobuild_files(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 510 | return self._module_configuration.nobuild_files |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 511 | |
| 512 | @property |
| 513 | def error_handlers(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 514 | return self._module_configuration.error_handlers |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 515 | |
| 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 522 | login='admin')] + self._module_configuration.handlers |
| 523 | return self._module_configuration.handlers |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 524 | |
| 525 | @property |
| 526 | def inbound_services(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 527 | return self._module_configuration.inbound_services |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 528 | |
| 529 | @property |
| 530 | def env_variables(self): |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 531 | return self._module_configuration.env_variables |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 532 | |
| 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] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 552 | class DispatchConfiguration(object): |
| 553 | """Stores dispatcher configuration information.""" |
| 554 | |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 555 | 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] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 559 | |
| 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] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 568 | mtime = os.path.getmtime(self._config_path) |
[email protected] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 569 | if mtime > self._mtime: |
| 570 | self._mtime = mtime |
| 571 | try: |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 572 | dispatch_info_external = self._parse_configuration(self._config_path) |
[email protected] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 573 | 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 587 | path_only_entries.append((parsed_url, entry.module)) |
[email protected] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 588 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 600 | class ApplicationConfiguration(object): |
| 601 | """Stores application configuration information.""" |
| 602 | |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 603 | def __init__(self, config_paths): |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 604 | """Initializer for ApplicationConfiguration. |
| 605 | |
| 606 | Args: |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 607 | config_paths: A list of strings containing the paths to yaml files, |
| 608 | or to directories containing them. |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 609 | """ |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 610 | self.modules = [] |
[email protected] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 611 | self.dispatch = None |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 612 | # 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 620 | # TODO: Reuse the ModuleConfiguration created for the app.yaml |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 621 | # instead of creating another one for the same file. |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 622 | self.modules.extend( |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 623 | 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] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 627 | if self.dispatch: |
| 628 | raise errors.InvalidAppConfigError( |
| 629 | 'Multiple dispatch.yaml files specified') |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 630 | self.dispatch = DispatchConfiguration(config_path) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 631 | else: |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 632 | module_configuration = ModuleConfiguration(config_path) |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 633 | self.modules.append(module_configuration) |
| 634 | application_ids = set(module.application |
| 635 | for module in self.modules) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 636 | 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] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 642 | 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] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 648 | if self.dispatch: |
[email protected] | 2e4b3c8 | 2014-02-26 20:08:51 | [diff] [blame] | 649 | if appinfo.DEFAULT_MODULE not in module_names: |
[email protected] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 650 | raise errors.InvalidAppConfigError( |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 651 | '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] | 6da8fcb | 2013-04-09 23:18:42 | [diff] [blame] | 656 | raise errors.InvalidAppConfigError( |
[email protected] | 200fcb7 | 2013-07-20 01:18:36 | [diff] [blame] | 657 | 'Modules %s specified in dispatch.yaml are not defined by a yaml ' |
| 658 | 'file.' % sorted(missing_modules)) |
[email protected] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 659 | |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 660 | 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] | 3c70367 | 2013-03-19 23:06:51 | [diff] [blame] | 732 | @property |
| 733 | def app_id(self): |
| 734 | return self._app_id |
[email protected] | 21ed1cd | 2014-03-19 22:38:35 | [diff] [blame] | 735 | |
| 736 | |
| 737 | def 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 |