diff options
author | Jay Pipes <jaypipes@gmail.com> | 2011-07-26 09:05:53 -0400 |
---|---|---|
committer | Jay Pipes <jaypipes@gmail.com> | 2011-07-26 09:05:53 -0400 |
commit | c85e1f7b4e4e8ea7a4173188123c01eb2b165627 (patch) | |
tree | b1c2142a51100671102c50c990a8b351fdf5389d /openstack | |
download | oslo-c85e1f7b4e4e8ea7a4173188123c01eb2b165627.tar.gz oslo-c85e1f7b4e4e8ea7a4173188123c01eb2b165627.tar.xz oslo-c85e1f7b4e4e8ea7a4173188123c01eb2b165627.zip |
Initial skeleton project
Diffstat (limited to 'openstack')
-rw-r--r-- | openstack/common/__init__.py | 19 | ||||
-rw-r--r-- | openstack/common/config.py | 325 | ||||
-rw-r--r-- | openstack/common/exception.py | 145 | ||||
-rw-r--r-- | openstack/common/wsgi.py | 346 |
4 files changed, 835 insertions, 0 deletions
diff --git a/openstack/common/__init__.py b/openstack/common/__init__.py new file mode 100644 index 0000000..b22496f --- /dev/null +++ b/openstack/common/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +# TODO(jaypipes) Code in this module is intended to be ported to the eventual +# openstack-common library diff --git a/openstack/common/config.py b/openstack/common/config.py new file mode 100644 index 0000000..c9280f0 --- /dev/null +++ b/openstack/common/config.py @@ -0,0 +1,325 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Routines for configuring Openstack Projects +""" + +import ConfigParser +import logging +import logging.config +import logging.handlers +import optparse +import os +import re +import sys + +from paste import deploy + +DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" +DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +def parse_options(parser, cli_args=None): + """ + Returns the parsed CLI options, command to run and its arguments, merged + with any same-named options found in a configuration file. + + The function returns a tuple of (options, args), where options is a + mapping of option key/str(value) pairs, and args is the set of arguments + (not options) supplied on the command-line. + + The reason that the option values are returned as strings only is that + ConfigParser and paste.deploy only accept string values... + + :param parser: The option parser + :param cli_args: (Optional) Set of arguments to process. If not present, + sys.argv[1:] is used. + :retval tuple of (options, args) + """ + + (options, args) = parser.parse_args(cli_args) + + return (vars(options), args) + + +def add_common_options(parser): + """ + Given a supplied optparse.OptionParser, adds an OptionGroup that + represents all common configuration options. + + :param parser: optparse.OptionParser + """ + help_text = "The following configuration options are common to "\ + "all glance programs." + + group = optparse.OptionGroup(parser, "Common Options", help_text) + group.add_option('-v', '--verbose', default=False, dest="verbose", + action="store_true", + help="Print more verbose output") + group.add_option('-d', '--debug', default=False, dest="debug", + action="store_true", + help="Print debugging output") + group.add_option('--config-file', default=None, metavar="PATH", + help="Path to the config file to use. When not specified " + "(the default), we generally look at the first " + "argument specified to be a config file, and if " + "that is also missing, we search standard " + "directories for a config file.") + parser.add_option_group(group) + + +def add_log_options(parser): + """ + Given a supplied optparse.OptionParser, adds an OptionGroup that + represents all the configuration options around logging. + + :param parser: optparse.OptionParser + """ + help_text = "The following configuration options are specific to logging "\ + "functionality for this program." + + group = optparse.OptionGroup(parser, "Logging Options", help_text) + group.add_option('--log-config', default=None, metavar="PATH", + help="If this option is specified, the logging " + "configuration file specified is used and overrides " + "any other logging options specified. Please see " + "the Python logging module documentation for " + "details on logging configuration files.") + group.add_option('--log-date-format', metavar="FORMAT", + default=DEFAULT_LOG_DATE_FORMAT, + help="Format string for %(asctime)s in log records. " + "Default: %default") + group.add_option('--log-file', default=None, metavar="PATH", + help="(Optional) Name of log file to output to. " + "If not set, logging will go to stdout.") + group.add_option("--log-dir", default=None, + help="(Optional) The directory to keep log files in " + "(will be prepended to --logfile)") + parser.add_option_group(group) + + +def setup_logging(options, conf): + """ + Sets up the logging options for a log with supplied name + + :param options: Mapping of typed option key/values + :param conf: Mapping of untyped key/values from config file + """ + + if options.get('log_config', None): + # Use a logging configuration file for all settings... + if os.path.exists(options['log_config']): + logging.config.fileConfig(options['log_config']) + return + else: + raise RuntimeError("Unable to locate specified logging " + "config file: %s" % options['log_config']) + + # If either the CLI option or the conf value + # is True, we set to True + debug = options.get('debug') or \ + get_option(conf, 'debug', type='bool', default=False) + verbose = options.get('verbose') or \ + get_option(conf, 'verbose', type='bool', default=False) + root_logger = logging.root + if debug: + root_logger.setLevel(logging.DEBUG) + elif verbose: + root_logger.setLevel(logging.INFO) + else: + root_logger.setLevel(logging.WARNING) + + # Set log configuration from options... + # Note that we use a hard-coded log format in the options + # because of Paste.Deploy bug #379 + # http://trac.pythonpaste.org/pythonpaste/ticket/379 + log_format = options.get('log_format', DEFAULT_LOG_FORMAT) + log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT) + formatter = logging.Formatter(log_format, log_date_format) + + logfile = options.get('log_file') + if not logfile: + logfile = conf.get('log_file') + + if logfile: + logdir = options.get('log_dir') + if not logdir: + logdir = conf.get('log_dir') + if logdir: + logfile = os.path.join(logdir, logfile) + logfile = logging.FileHandler(logfile) + logfile.setFormatter(formatter) + logfile.setFormatter(formatter) + root_logger.addHandler(logfile) + else: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + +def find_config_file(app_name, options, args): + """ + Return the first config file found for an application. + + We search for the paste config file in the following order: + * If --config-file option is used, use that + * If args[0] is a file, use that + * Search for $app.conf in standard directories: + * . + * ~.glance/ + * ~ + * /etc/glance + * /etc + + :retval Full path to config file, or None if no config file found + """ + + fix_path = lambda p: os.path.abspath(os.path.expanduser(p)) + if options.get('config_file'): + if os.path.exists(options['config_file']): + return fix_path(options['config_file']) + elif args: + if os.path.exists(args[0]): + return fix_path(args[0]) + + # Handle standard directory search for $app_name.conf + config_file_dirs = [fix_path(os.getcwd()), + fix_path(os.path.join('~', '.glance')), + fix_path('~'), + '/etc/glance/', + '/etc'] + + for cfg_dir in config_file_dirs: + cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name) + if os.path.exists(cfg_file): + return cfg_file + + +def load_paste_config(app_name, options, args): + """ + Looks for a config file to use for an app and returns the + config file path and a configuration mapping from a paste config file. + + We search for the paste config file in the following order: + * If --config-file option is used, use that + * If args[0] is a file, use that + * Search for $app_name.conf in standard directories: + * . + * ~.glance/ + * ~ + * /etc/glance + * /etc + + :param app_name: Name of the application to load config for, or None. + None signifies to only load the [DEFAULT] section of + the config file. + :param options: Set of typed options returned from parse_options() + :param args: Command line arguments from argv[1:] + :retval Tuple of (conf_file, conf) + + :raises RuntimeError when config file cannot be located or there was a + problem loading the configuration file. + """ + conf_file = find_config_file(app_name, options, args) + if not conf_file: + raise RuntimeError("Unable to locate any configuration file. " + "Cannot load application %s" % app_name) + try: + conf = deploy.appconfig("config:%s" % conf_file, name=app_name) + return conf_file, conf + except Exception, e: + raise RuntimeError("Error trying to load config %s: %s" + % (conf_file, e)) + + +def load_paste_app(app_name, options, args): + """ + Builds and returns a WSGI app from a paste config file. + + We search for the paste config file in the following order: + * If --config-file option is used, use that + * If args[0] is a file, use that + * Search for $app_name.conf in standard directories: + * . + * ~.glance/ + * ~ + * /etc/glance + * /etc + + :param app_name: Name of the application to load + :param options: Set of typed options returned from parse_options() + :param args: Command line arguments from argv[1:] + + :raises RuntimeError when config file cannot be located or application + cannot be loaded from config file + """ + conf_file, conf = load_paste_config(app_name, options, args) + + try: + # Setup logging early, supplying both the CLI options and the + # configuration mapping from the config file + setup_logging(options, conf) + + # We only update the conf dict for the verbose and debug + # flags. Everything else must be set up in the conf file... + debug = options.get('debug') or \ + get_option(conf, 'debug', type='bool', default=False) + verbose = options.get('verbose') or \ + get_option(conf, 'verbose', type='bool', default=False) + conf['debug'] = debug + conf['verbose'] = verbose + + # Log the options used when starting if we're in debug mode... + if debug: + logger = logging.getLogger(app_name) + logger.debug("*" * 80) + logger.debug("Configuration options gathered from config file:") + logger.debug(conf_file) + logger.debug("================================================") + items = dict([(k, v) for k, v in conf.items() + if k not in ('__file__', 'here')]) + for key, value in sorted(items.items()): + logger.debug("%(key)-30s %(value)s" % locals()) + logger.debug("*" * 80) + app = deploy.loadapp("config:%s" % conf_file, name=app_name) + except (LookupError, ImportError), e: + raise RuntimeError("Unable to load %(app_name)s from " + "configuration file %(conf_file)s." + "\nGot: %(e)r" % locals()) + return conf, app + + +def get_option(options, option, **kwargs): + if option in options: + value = options[option] + type_ = kwargs.get('type', 'str') + if type_ == 'bool': + if hasattr(value, 'lower'): + return value.lower() == 'true' + else: + return value + elif type_ == 'int': + return int(value) + elif type_ == 'float': + return float(value) + else: + return value + elif 'default' in kwargs: + return kwargs['default'] + else: + raise KeyError("option '%s' not found" % option) diff --git a/openstack/common/exception.py b/openstack/common/exception.py new file mode 100644 index 0000000..ed6b039 --- /dev/null +++ b/openstack/common/exception.py @@ -0,0 +1,145 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Exceptions common to OpenStack projects +""" + +import logging +import sys +import traceback + + +class ProcessExecutionError(IOError): + def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, + description=None): + if description is None: + description = "Unexpected error while running command." + if exit_code is None: + exit_code = '-' + message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( + description, cmd, exit_code, stdout, stderr) + IOError.__init__(self, message) + + +class Error(Exception): + def __init__(self, message=None): + super(Error, self).__init__(message) + + +class ApiError(Error): + def __init__(self, message='Unknown', code='Unknown'): + self.message = message + self.code = code + super(ApiError, self).__init__('%s: %s' % (code, message)) + + +class NotFound(Error): + pass + + +class UnknownScheme(Error): + + msg = "Unknown scheme '%s' found in URI" + + def __init__(self, scheme): + msg = self.__class__.msg % scheme + super(UnknownScheme, self).__init__(msg) + + +class BadStoreUri(Error): + + msg = "The Store URI %s was malformed. Reason: %s" + + def __init__(self, uri, reason): + msg = self.__class__.msg % (uri, reason) + super(BadStoreUri, self).__init__(msg) + + +class Duplicate(Error): + pass + + +class NotAuthorized(Error): + pass + + +class NotEmpty(Error): + pass + + +class Invalid(Error): + pass + + +class BadInputError(Exception): + """Error resulting from a client sending bad input to a server""" + pass + + +class MissingArgumentError(Error): + pass + + +class DatabaseMigrationError(Error): + pass + + +class ClientConnectionError(Exception): + """Error resulting from a client connecting to a server""" + pass + + +def wrap_exception(f): + def _wrap(*args, **kw): + try: + return f(*args, **kw) + except Exception, e: + if not isinstance(e, Error): + #exc_type, exc_value, exc_traceback = sys.exc_info() + logging.exception('Uncaught exception') + #logging.error(traceback.extract_stack(exc_traceback)) + raise Error(str(e)) + raise + _wrap.func_name = f.func_name + return _wrap + + +class GlanceException(Exception): + """ + Base Glance Exception + + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + message = "An unknown exception occurred" + + def __init__(self, **kwargs): + try: + self._error_string = self.message % kwargs + + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self): + return self._error_string + + +class InvalidContentType(GlanceException): + message = "Invalid content type %(content_type)s" diff --git a/openstack/common/wsgi.py b/openstack/common/wsgi.py new file mode 100644 index 0000000..ef132dc --- /dev/null +++ b/openstack/common/wsgi.py @@ -0,0 +1,346 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Utility methods for working with WSGI servers +""" + +import json +import logging +import sys +import datetime + +import eventlet +import eventlet.wsgi +eventlet.patcher.monkey_patch(all=False, socket=True) +import routes +import routes.middleware +import webob.dec +import webob.exc + +from openstack.common import exception + + +class WritableLogger(object): + """A thin wrapper that responds to `write` and logs.""" + + def __init__(self, logger, level=logging.DEBUG): + self.logger = logger + self.level = level + + def write(self, msg): + self.logger.log(self.level, msg.strip("\n")) + + +def run_server(application, port): + """Run a WSGI server with the given application.""" + sock = eventlet.listen(('0.0.0.0', port)) + eventlet.wsgi.server(sock, application) + + +class Server(object): + """Server class to manage multiple WSGI sockets and applications.""" + + def __init__(self, threads=1000): + self.pool = eventlet.GreenPool(threads) + + def start(self, application, port, host='0.0.0.0', backlog=128): + """Run a WSGI server with the given application.""" + socket = eventlet.listen((host, port), backlog=backlog) + self.pool.spawn_n(self._run, application, socket) + + def wait(self): + """Wait until all servers have completed running.""" + try: + self.pool.waitall() + except KeyboardInterrupt: + pass + + def _run(self, application, socket): + """Start a WSGI server in a new green thread.""" + logger = logging.getLogger('eventlet.wsgi.server') + eventlet.wsgi.server(socket, application, custom_pool=self.pool, + log=WritableLogger(logger)) + + +class Middleware(object): + """ + Base WSGI middleware wrapper. These classes require an application to be + initialized that will be called next. By default the middleware will + simply call its wrapped app, or you can override __call__ to customize its + behavior. + """ + + def __init__(self, application): + self.application = application + + def process_request(self, req): + """ + Called on each request. + + If this returns None, the next application down the stack will be + executed. If it returns a response then that response will be returned + and execution will stop here. + """ + return None + + def process_response(self, response): + """Do whatever you'd like to the response.""" + return response + + @webob.dec.wsgify + def __call__(self, req): + response = self.process_request(req) + if response: + return response + response = req.get_response(self.application) + return self.process_response(response) + + +class Debug(Middleware): + """ + Helper class that can be inserted into any WSGI application chain + to get information about the request and response. + """ + + @webob.dec.wsgify + def __call__(self, req): + print ("*" * 40) + " REQUEST ENVIRON" + for key, value in req.environ.items(): + print key, "=", value + print + resp = req.get_response(self.application) + + print ("*" * 40) + " RESPONSE HEADERS" + for (key, value) in resp.headers.iteritems(): + print key, "=", value + print + + resp.app_iter = self.print_generator(resp.app_iter) + + return resp + + @staticmethod + def print_generator(app_iter): + """ + Iterator that prints the contents of a wrapper string iterator + when iterated. + """ + print ("*" * 40) + " BODY" + for part in app_iter: + sys.stdout.write(part) + sys.stdout.flush() + yield part + print + + +class Router(object): + + """ + WSGI middleware that maps incoming requests to WSGI apps. + """ + + def __init__(self, mapper): + """ + Create a router for the given routes.Mapper. + + Each route in `mapper` must specify a 'controller', which is a + WSGI app to call. You'll probably want to specify an 'action' as + well and have your controller be a wsgi.Controller, who will route + the request to the action method. + + Examples: + mapper = routes.Mapper() + sc = ServerController() + + # Explicit mapping of one route to a controller+action + mapper.connect(None, "/svrlist", controller=sc, action="list") + + # Actions are all implicitly defined + mapper.resource("server", "servers", controller=sc) + + # Pointing to an arbitrary WSGI app. You can specify the + # {path_info:.*} parameter so the target app can be handed just that + # section of the URL. + mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) + """ + self.map = mapper + self._router = routes.middleware.RoutesMiddleware(self._dispatch, + self.map) + + @webob.dec.wsgify + def __call__(self, req): + """ + Route the incoming request to a controller based on self.map. + If no match, return a 404. + """ + return self._router + + @staticmethod + @webob.dec.wsgify + def _dispatch(req): + """ + Called by self._router after matching the incoming request to a route + and putting the information into req.environ. Either returns 404 + or the routed WSGI app's response. + """ + match = req.environ['wsgiorg.routing_args'][1] + if not match: + return webob.exc.HTTPNotFound() + app = match['controller'] + return app + + +class Request(webob.Request): + + """Add some Openstack API-specific logic to the base webob.Request.""" + + def best_match_content_type(self): + """Determine the requested response content-type.""" + supported = ('application/json',) + bm = self.accept.best_match(supported) + return bm or 'application/json' + + def get_content_type(self, allowed_content_types): + """Determine content type of the request body.""" + if not "Content-Type" in self.headers: + raise exception.InvalidContentType(content_type=None) + + content_type = self.content_type + + if content_type not in allowed_content_types: + raise exception.InvalidContentType(content_type=content_type) + else: + return content_type + + +class JSONRequestDeserializer(object): + def has_body(self, request): + """ + Returns whether a Webob.Request object will possess an entity body. + + :param request: Webob.Request object + """ + if 'transfer-encoding' in request.headers: + return True + elif request.content_length > 0: + return True + + return False + + def from_json(self, datastring): + return json.loads(datastring) + + def default(self, request): + if self.has_body(request): + return {'body': self.from_json(request.body)} + else: + return {} + + +class JSONResponseSerializer(object): + + def to_json(self, data): + def sanitizer(obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + return obj + + return json.dumps(data, default=sanitizer) + + def default(self, response, result): + response.headers.add('Content-Type', 'application/json') + response.body = self.to_json(result) + + +class Resource(object): + """ + WSGI app that handles (de)serialization and controller dispatch. + + Reads routing information supplied by RoutesMiddleware and calls + the requested action method upon its deserializer, controller, + and serializer. Those three objects may implement any of the basic + controller action methods (create, update, show, index, delete) + along with any that may be specified in the api router. A 'default' + method may also be implemented to be used in place of any + non-implemented actions. Deserializer methods must accept a request + argument and return a dictionary. Controller methods must accept a + request argument. Additionally, they must also accept keyword + arguments that represent the keys returned by the Deserializer. They + may raise a webob.exc exception or return a dict, which will be + serialized by requested content type. + """ + def __init__(self, controller, deserializer, serializer): + """ + :param controller: object that implement methods created by routes lib + :param deserializer: object that supports webob request deserialization + through controller-like actions + :param serializer: object that supports webob response serialization + through controller-like actions + """ + self.controller = controller + self.serializer = serializer + self.deserializer = deserializer + + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, request): + """WSGI method that controls (de)serialization and method dispatch.""" + action_args = self.get_action_args(request.environ) + action = action_args.pop('action', None) + + deserialized_request = self.dispatch(self.deserializer, + action, request) + action_args.update(deserialized_request) + + action_result = self.dispatch(self.controller, action, + request, **action_args) + try: + response = webob.Response() + self.dispatch(self.serializer, action, response, action_result) + return response + + # return unserializable result (typically a webob exc) + except Exception: + return action_result + + def dispatch(self, obj, action, *args, **kwargs): + """Find action-specific method on self and call it.""" + try: + method = getattr(obj, action) + except AttributeError: + method = getattr(obj, 'default') + + return method(*args, **kwargs) + + def get_action_args(self, request_environment): + """Parse dictionary created by routes library.""" + try: + args = request_environment['wsgiorg.routing_args'][1].copy() + except Exception: + return {} + + try: + del args['controller'] + except KeyError: + pass + + try: + del args['format'] + except KeyError: + pass + + return args |