From: Salvatore Orlando Date: Mon, 23 May 2011 20:51:00 +0000 (+0100) Subject: Adding first files for quantum API X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=702e64fc525535e8ed5052bd6ec55d61ab7f0aee;p=openstack-build%2Fneutron-build.git Adding first files for quantum API --- 702e64fc525535e8ed5052bd6ec55d61ab7f0aee diff --cc bin/quantum index 16ef7346d,d19fa91ef..780ccc61a mode 100644,100644..100755 --- a/bin/quantum +++ b/bin/quantum @@@ -19,6 -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,6 -32,6 +33,7 @@@ possible_topdir = os.path.normpath(os.p 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 @@@ -52,11 -52,10 +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 --cc etc/quantum.conf index 000000000,000000000..91904603d new file mode 100644 --- /dev/null +++ b/etc/quantum.conf @@@ -1,0 -1,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 --cc quantum/api/__init__.py index 000000000,000000000..9602374ca new file mode 100644 --- /dev/null +++ b/quantum/api/__init__.py @@@ -1,0 -1,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. diff --cc quantum/api/versions.py index 000000000,000000000..36cd274d1 new file mode 100644 --- /dev/null +++ b/quantum/api/versions.py @@@ -1,0 -1,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 diff --cc quantum/api/views/__init__.py index 000000000,000000000..ea9103400 new file mode 100644 --- /dev/null +++ b/quantum/api/views/__init__.py @@@ -1,0 -1,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. diff --cc quantum/api/views/versions.py index 000000000,000000000..d0145c94a new file mode 100644 --- /dev/null +++ b/quantum/api/views/versions.py @@@ -1,0 -1,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 --cc quantum/common/config.py index dbbcd260f,21c1b5951..2d858ed35 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@@ -31,11 -31,11 +31,15 @@@ import sy from paste import deploy - import quantum.common.exception as exception -import quantum.common.exceptions 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 -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 -204,9 +208,10 @@@ 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 -276,6 +281,8 @@@ def load_paste_app(app_name, options, a 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 -288,17 +295,15 @@@ 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 --cc quantum/common/exceptions.py index c434e736e,c434e736e..bcc7696a2 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@@ -69,6 -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 --cc quantum/common/flags.py index 51cbe58be,51cbe58be..947999d0f --- a/quantum/common/flags.py +++ b/quantum/common/flags.py @@@ -23,6 -23,6 +23,7 @@@ Global flags should be defined here, th """ import getopt ++import os import string import sys @@@ -245,3 -245,3 +246,7 @@@ def DECLARE(name, module_string, flag_v # __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 --cc quantum/common/wsgi.py index 6c7caa1dc,6c7caa1dc..73b826ef9 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@@ -32,6 -32,6 +32,9 @@@ import routes.middlewar 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 -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 -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 -302,6 +426,7 @@@ 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: