Adding first files for quantum API

This commit is contained in:
Salvatore Orlando 2011-05-23 21:51:00 +01:00
commit 702e64fc52
10 changed files with 348 additions and 36 deletions

17
bin/quantum Normal file → Executable file
View File

@ -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)

19
etc/quantum.conf Normal file
View File

@ -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

16
quantum/api/__init__.py Normal file
View File

@ -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.

62
quantum/api/versions.py Normal file
View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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 "

View File

@ -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

View File

@ -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")

View File

@ -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: