Adding first files for quantum API
This commit is contained in:
commit
702e64fc52
17
bin/quantum
Normal file → Executable file
17
bin/quantum
Normal file → Executable 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
19
etc/quantum.conf
Normal 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
16
quantum/api/__init__.py
Normal 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
62
quantum/api/versions.py
Normal 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
|
16
quantum/api/views/__init__.py
Normal file
16
quantum/api/views/__init__.py
Normal 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.
|
59
quantum/api/views/versions.py
Normal file
59
quantum/api/views/versions.py
Normal 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)
|
@ -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 "
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user