blob: 285bf9dd80113c6bebc23c103cfa06622d04f9f5 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
A library for working with BackendInfoExternal records, describing backends
configured for an application. Supports loading the records from backend.yaml.
"""
import os
import yaml
from yaml import representer
if os.environ.get('APPENGINE_RUNTIME') == 'python27':
from google.appengine.api import validation
from google.appengine.api import yaml_builder
from google.appengine.api import yaml_listener
from google.appengine.api import yaml_object
else:
from google.appengine.api import validation
from google.appengine.api import yaml_builder
from google.appengine.api import yaml_listener
from google.appengine.api import yaml_object
NAME_REGEX = r'(?!-)[a-z\d\-]{1,100}'
FILE_REGEX = r'(?!\^).*(?!\$).{1,256}'
CLASS_REGEX = r'^[bB](1|2|4|8|4_1G)$'
OPTIONS_REGEX = r'^[a-z, ]*$'
STATE_REGEX = r'^(START|STOP|DISABLED)$'
BACKENDS = 'backends'
NAME = 'name'
CLASS = 'class'
INSTANCES = 'instances'
OPTIONS = 'options'
PUBLIC = 'public'
DYNAMIC = 'dynamic'
FAILFAST = 'failfast'
MAX_CONCURRENT_REQUESTS = 'max_concurrent_requests'
START = 'start'
VALID_OPTIONS = frozenset([PUBLIC, DYNAMIC, FAILFAST])
STATE = 'state'
class BadConfig(Exception):
"""An invalid configuration was provided."""
class ListWithoutSort(list):
def sort(self):
pass
class SortedDict(dict):
def __init__(self, keys, data):
super(SortedDict, self).__init__()
self.keys = keys
self.update(data)
def items(self):
result = ListWithoutSort()
for key in self.keys:
if type(self.get(key)) != type(None):
result.append((key, self.get(key)))
return result
representer.SafeRepresenter.add_representer(
SortedDict, representer.SafeRepresenter.represent_dict)
class BackendEntry(validation.Validated):
"""A backend entry describes a single backend."""
ATTRIBUTES = {
NAME: NAME_REGEX,
CLASS: validation.Optional(CLASS_REGEX),
INSTANCES: validation.Optional(validation.TYPE_INT),
MAX_CONCURRENT_REQUESTS: validation.Optional(validation.TYPE_INT),
OPTIONS: validation.Optional(OPTIONS_REGEX),
PUBLIC: validation.Optional(validation.TYPE_BOOL),
DYNAMIC: validation.Optional(validation.TYPE_BOOL),
FAILFAST: validation.Optional(validation.TYPE_BOOL),
START: validation.Optional(FILE_REGEX),
STATE: validation.Optional(STATE_REGEX),
}
def __init__(self, *args, **kwargs):
super(BackendEntry, self).__init__(*args, **kwargs)
self.Init()
def Init(self):
if self.public:
raise BadConfig("Illegal field: 'public'")
if self.dynamic:
raise BadConfig("Illegal field: 'dynamic'")
if self.failfast:
raise BadConfig("Illegal field: 'failfast'")
self.ParseOptions()
return self
def set_class(self, Class):
"""Setter for 'class', since an attribute reference is an error."""
self.Set(CLASS, Class)
def get_class(self):
"""Accessor for 'class', since an attribute reference is an error."""
return self.Get(CLASS)
def ToDict(self):
"""Returns a sorted dictionary representing the backend entry."""
self.ParseOptions().WriteOptions()
result = super(BackendEntry, self).ToDict()
return SortedDict([NAME,
CLASS,
INSTANCES,
START,
OPTIONS,
MAX_CONCURRENT_REQUESTS,
STATE],
result)
def ParseOptions(self):
"""Parses the 'options' field and sets appropriate fields."""
if self.options:
options = [option.strip() for option in self.options.split(',')]
else:
options = []
for option in options:
if option not in VALID_OPTIONS:
raise BadConfig('Unrecognized option: %s', option)
self.public = PUBLIC in options
self.dynamic = DYNAMIC in options
self.failfast = FAILFAST in options
return self
def WriteOptions(self):
"""Writes the 'options' field based on other settings."""
options = []
if self.public:
options.append('public')
if self.dynamic:
options.append('dynamic')
if self.failfast:
options.append('failfast')
if options:
self.options = ', '.join(options)
else:
self.options = None
return self
def LoadBackendEntry(backend_entry):
"""Parses a BackendEntry object from a string.
Args:
backend_entry: a backend entry, as a string
Returns:
A BackendEntry object.
"""
builder = yaml_object.ObjectBuilder(BackendEntry)
handler = yaml_builder.BuilderHandler(builder)
listener = yaml_listener.EventListener(handler)
listener.Parse(backend_entry)
entries = handler.GetResults()
if len(entries) < 1:
raise BadConfig('Empty backend configuration.')
if len(entries) > 1:
raise BadConfig('Multiple backend entries were found in configuration.')
return entries[0].Init()
class BackendInfoExternal(validation.Validated):
"""BackendInfoExternal describes all backend entries for an application."""
ATTRIBUTES = {
BACKENDS: validation.Optional(validation.Repeated(BackendEntry)),
}
def LoadBackendInfo(backend_info, open_fn=None):
"""Parses a BackendInfoExternal object from a string.
Args:
backend_info: a backends stanza (list of backends) as a string
open_fn: Function for opening files. Unused.
Returns:
A BackendInfoExternal object.
"""
builder = yaml_object.ObjectBuilder(BackendInfoExternal)
handler = yaml_builder.BuilderHandler(builder)
listener = yaml_listener.EventListener(handler)
listener.Parse(backend_info)
backend_info = handler.GetResults()
if len(backend_info) < 1:
return BackendInfoExternal(backends=[])
if len(backend_info) > 1:
raise BadConfig("Only one 'backends' clause is allowed.")
info = backend_info[0]
if not info.backends:
return BackendInfoExternal(backends=[])
for backend in info.backends:
backend.Init()
return info