diff --git a/bin/quantum b/bin/quantum old mode 100644 new mode 100755 index 16ef7346d2c..780ccc61a84 --- a/bin/quantum +++ b/bin/quantum @@ -19,6 +19,7 @@ # If ../quantum/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... +import gettext import optparse import os import re @@ -32,14 +33,15 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): sys.path.insert(0, possible_topdir) +gettext.install('quantum', unicode=1) from quantum.common import wsgi from quantum.common import config def create_options(parser): - """ + """ Sets up the CLI and config-file options that may be - parsed and program commands. + parsed and program commands. :param parser: The option parser """ config.add_common_options(parser) @@ -52,11 +54,10 @@ if __name__ == '__main__': (options, args) = config.parse_options(oparser) try: - conf, app = config.load_paste_app('quantum', options, args) - - server = wsgi.Server() - server.start(app, int(conf['bind_port']), conf['bind_host']) - server.wait() + conf, app = config.load_paste_app('quantumversionapp', options, args) + server = wsgi.Server() + server.start(app, int(conf['bind_port']), conf['bind_host']) + server.wait() except RuntimeError, e: - sys.exit("ERROR: %s" % e) + sys.exit("ERROR: %s" % e) diff --git a/etc/quantum.conf b/etc/quantum.conf new file mode 100644 index 00000000000..91904603d34 --- /dev/null +++ b/etc/quantum.conf @@ -0,0 +1,19 @@ +[DEFAULT] +# Show more verbose log output (sets INFO log level output) +verbose = True + +# Show debugging output in logs (sets DEBUG log level output) +debug = True + +# Address to bind the API server +bind_host = 0.0.0.0 + +# Port the bind the API server to +bind_port = 9696 + +#[app:quantum] +#paste.app_factory = quantum.service:app_factory + +[app:quantumversionapp] +paste.app_factory = quantum.api.versions:Versions.factory + diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py new file mode 100644 index 00000000000..9602374ca26 --- /dev/null +++ b/quantum/api/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Citrix Systems +# 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. +# @author: Somik Behera, Nicira Networks, Inc. \ No newline at end of file diff --git a/quantum/api/versions.py b/quantum/api/versions.py new file mode 100644 index 00000000000..36cd274d1f5 --- /dev/null +++ b/quantum/api/versions.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems. +# 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. + +import logging +import webob.dec + +from quantum.common import wsgi +from quantum.api.views import versions as versions_view + +LOG = logging.getLogger('quantum.api.versions') + +class Versions(wsgi.Application): + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """Respond to a request for all Quantum API versions.""" + version_objs = [ + { + "id": "v0.1", + "status": "CURRENT", + }, + { + "id": "v1.0", + "status": "FUTURE", + }, + ] + + builder = versions_view.get_view_builder(req) + versions = [builder.build(version) for version in version_objs] + response = dict(versions=versions) + LOG.debug("response:%s",response) + metadata = { + "application/xml": { + "attributes": { + "version": ["status", "id"], + "link": ["rel", "href"], + } + } + } + + content_type = req.best_match_content_type() + body = wsgi.Serializer(metadata=metadata).serialize(response, content_type) + + response = webob.Response() + response.content_type = content_type + response.body = body + + return response \ No newline at end of file diff --git a/quantum/api/views/__init__.py b/quantum/api/views/__init__.py new file mode 100644 index 00000000000..ea910340037 --- /dev/null +++ b/quantum/api/views/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Citrix Systems, Inc. +# 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. +# @author: Somik Behera, Nicira Networks, Inc. \ No newline at end of file diff --git a/quantum/api/views/versions.py b/quantum/api/views/versions.py new file mode 100644 index 00000000000..d0145c94a38 --- /dev/null +++ b/quantum/api/views/versions.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-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. + +import os + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(object): + + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ + self.base_url = base_url + + def build(self, version_data): + """Generic method used to generate a version entity.""" + version = { + "id": version_data["id"], + "status": version_data["status"], + "links": self._build_links(version_data), + } + + return version + + def _build_links(self, version_data): + """Generate a container of links that refer to the provided version.""" + href = self.generate_href(version_data["id"]) + + links = [ + { + "rel": "self", + "href": href, + }, + ] + + return links + + def generate_href(self, version_number): + """Create an url that refers to a specific version_number.""" + return os.path.join(self.base_url, version_number) diff --git a/quantum/common/config.py b/quantum/common/config.py index dbbcd260fc9..2d858ed357f 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -31,11 +31,15 @@ import sys from paste import deploy -import quantum.common.exception as exception +from quantum.common import flags +from quantum.common import exceptions as exception DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +FLAGS = flags.FLAGS +LOG = logging.getLogger('quantum.common.wsgi') + def parse_options(parser, cli_args=None): """ @@ -186,8 +190,8 @@ def find_config_file(options, args): * . * ~.quantum/ * ~ - * /etc/quantum - * /etc + * $FLAGS.state_path/etc/quantum + * $FLAGS.state_path/etc :retval Full path to config file, or None if no config file found """ @@ -204,9 +208,10 @@ def find_config_file(options, args): config_file_dirs = [fix_path(os.getcwd()), fix_path(os.path.join('~', '.quantum')), fix_path('~'), + os.path.join(FLAGS.state_path, 'etc'), + os.path.join(FLAGS.state_path, 'etc','quantum'), '/etc/quantum/', '/etc'] - for cfg_dir in config_file_dirs: cfg_file = os.path.join(cfg_dir, 'quantum.conf') if os.path.exists(cfg_file): @@ -276,6 +281,8 @@ def load_paste_app(app_name, options, args): try: # Setup logging early, supplying both the CLI options and the # configuration mapping from the config file + print "OPTIONS:%s" %options + print "CONF:%s" %conf setup_logging(options, conf) # We only update the conf dict for the verbose and debug @@ -288,17 +295,15 @@ def load_paste_app(app_name, options, args): 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) + LOG.debug("*" * 80) + LOG.debug("Configuration options gathered from config file:") + LOG.debug(conf_file) + LOG.debug("================================================") + items = dict([(k, v) for k, v in conf.items() + if k not in ('__file__', 'here')]) + for key, value in sorted(items.items()): + LOG.debug("%(key)-30s %(value)s" % locals()) + LOG.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 " diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index c434e736e7d..bcc7696a216 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -69,6 +69,10 @@ class Invalid(Error): pass +class InvalidContentType(Invalid): + message = _("Invalid content type %(content_type)s.") + + class BadInputError(Exception): """Error resulting from a client sending bad input to a server""" pass diff --git a/quantum/common/flags.py b/quantum/common/flags.py index 51cbe58be05..947999d0f22 100644 --- a/quantum/common/flags.py +++ b/quantum/common/flags.py @@ -23,6 +23,7 @@ Global flags should be defined here, the rest are defined where they're used. """ import getopt +import os import string import sys @@ -245,3 +246,7 @@ def DECLARE(name, module_string, flag_values=FLAGS): # __GLOBAL FLAGS ONLY__ # Define any app-specific flags in their own files, docs at: # http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#a9 + +DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../../'), + "Top-level directory for maintaining quantum's state") + diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 6c7caa1dc90..73b826ef924 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -32,6 +32,9 @@ import routes.middleware import webob.dec import webob.exc +from quantum.common import exceptions as exception + +LOG = logging.getLogger('quantum.common.wsgi') class WritableLogger(object): """A thin wrapper that responds to `write` and logs.""" @@ -110,6 +113,104 @@ class Middleware(object): return self.process_response(response) +class Request(webob.Request): + + def best_match_content_type(self): + """Determine the most acceptable content-type. + + Based on the query extension then the Accept header. + + """ + parts = self.path.rsplit('.', 1) + + if len(parts) > 1: + format = parts[1] + if format in ['json', 'xml']: + return 'application/{0}'.format(parts[1]) + + ctypes = ['application/json', 'application/xml'] + bm = self.accept.best_match(ctypes) + + return bm or 'application/json' + + def get_content_type(self): + allowed_types = ("application/xml", "application/json") + if not "Content-Type" in self.headers: + msg = _("Missing Content-Type") + LOG.debug(msg) + raise webob.exc.HTTPBadRequest(msg) + type = self.content_type + if type in allowed_types: + return type + LOG.debug(_("Wrong Content-Type: %s") % type) + raise webob.exc.HTTPBadRequest("Invalid content type") + + +class Application(object): + """Base WSGI application wrapper. Subclasses need to implement __call__.""" + + @classmethod + def factory(cls, global_config, **local_config): + """Used for paste app factories in paste.deploy config files. + + Any local configuration (that is, values under the [app:APPNAME] + section of the paste config) will be passed into the `__init__` method + as kwargs. + + A hypothetical configuration would look like: + + [app:wadl] + latest_version = 1.3 + paste.app_factory = nova.api.fancy_api:Wadl.factory + + which would result in a call to the `Wadl` class as + + import quantum.api.fancy_api + fancy_api.Wadl(latest_version='1.3') + + You could of course re-implement the `factory` method in subclasses, + but using the kwarg passing it shouldn't be necessary. + + """ + return cls(**local_config) + + def __call__(self, environ, start_response): + r"""Subclasses will probably want to implement __call__ like this: + + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, req): + # Any of the following objects work as responses: + + # Option 1: simple string + res = 'message\n' + + # Option 2: a nicely formatted HTTP exception page + res = exc.HTTPForbidden(detail='Nice try') + + # Option 3: a webob Response object (in case you need to play with + # headers, or you want to be treated like an iterable, or or or) + res = Response(); + res.app_iter = open('somefile') + + # Option 4: any wsgi app to be run next + res = self.application + + # Option 5: you can get a Response object for a wsgi app, too, to + # play with headers etc + res = req.get_response(self.application) + + # You can then just return your response... + return res + # ... or set req.response and return None. + req.response = res + + See the end of http://pythonpaste.org/webob/modules/dec.html + for more info. + + """ + raise NotImplementedError(_('You must implement __call__')) + + class Debug(Middleware): """ Helper class that can be inserted into any WSGI application chain @@ -240,35 +341,58 @@ class Controller(object): return serializer.to_content_type(data) + + class Serializer(object): """ Serializes a dictionary to a Content Type specified by a WSGI environment. """ - def __init__(self, environ, metadata=None): + def __init__(self,metadata=None): """ Create a serializer based on the given WSGI environment. 'metadata' is an optional dict mapping MIME types to information needed to serialize a dictionary to that type. """ - self.environ = environ self.metadata = metadata or {} self._methods = { 'application/json': self._to_json, 'application/xml': self._to_xml} - def to_content_type(self, data): - """ - Serialize a dictionary into a string. The format of the string - will be decided based on the Content Type requested in self.environ: - by Accept: header, or by URL suffix. - """ - # FIXME(sirp): for now, supporting json only - #mimetype = 'application/xml' - mimetype = 'application/json' - # TODO(gundlach): determine mimetype from request - return self._methods.get(mimetype, repr)(data) + def _get_serialize_handler(self, content_type): + handlers = { + 'application/json': self._to_json, + 'application/xml': self._to_xml, + } + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_type) + + def serialize(self, data, content_type): + """Serialize a dictionary into the specified content type.""" + return self._get_serialize_handler(content_type)(data) + + def get_deserialize_handler(self, content_type): + handlers = { + 'application/json': self._from_json, + 'application/xml': self._from_xml, + } + + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_type) + + def deserialize(self, datastring, content_type): + """Deserialize a string to a dictionary. + + The string must be in the format of a supported MIME type. + + """ + return self.get_deserialize_handler(content_type)(datastring) + def _to_json(self, data): def sanitizer(obj): if isinstance(obj, datetime.datetime): @@ -302,6 +426,7 @@ class Serializer(object): elif type(data) is dict: attrs = metadata.get('attributes', {}).get(nodename, {}) for k, v in data.items(): + LOG.debug("K:%s - V:%s",k,v) if k in attrs: result.setAttribute(k, str(v)) else: