Work in progress on network API

This commit is contained in:
Salvatore Orlando 2011-05-24 17:45:16 +01:00
parent 702e64fc52
commit 3a421e759f
10 changed files with 478 additions and 55 deletions

View File

@ -35,6 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')):
gettext.install('quantum', unicode=1) gettext.install('quantum', unicode=1)
from quantum import service
from quantum.common import wsgi from quantum.common import wsgi
from quantum.common import config from quantum.common import config
@ -54,10 +55,18 @@ if __name__ == '__main__':
(options, args) = config.parse_options(oparser) (options, args) = config.parse_options(oparser)
try: try:
conf, app = config.load_paste_app('quantumversionapp', options, args) print "HERE-1"
server = wsgi.Server() service = service.serve_wsgi(service.QuantumApiService,
server.start(app, int(conf['bind_port']), conf['bind_host']) options=options,
server.wait() args=args)
#version_conf, version_app = config.load_paste_app('quantumversion', options, args)
print "HERE-2"
service.wait()
#api_conf, api_app = config.load_paste_app('quantum', options, args)
#server = wsgi.Server()
#server.start(version_app, int(version_conf['bind_port']), version_conf['bind_host'])
#server.start(api_app, int(api_conf['bind_port']), api_conf['bind_host'])
#server.wait()
except RuntimeError, e: except RuntimeError, e:
sys.exit("ERROR: %s" % e) sys.exit("ERROR: %s" % e)

View File

@ -11,9 +11,15 @@ bind_host = 0.0.0.0
# Port the bind the API server to # Port the bind the API server to
bind_port = 9696 bind_port = 9696
#[app:quantum] [composite:quantum]
#paste.app_factory = quantum.service:app_factory use = egg:Paste#urlmap
/: quantumversions
/v0.1: quantumapi
[app:quantumversionapp] [app:quantumversions]
paste.app_factory = quantum.api.versions:Versions.factory paste.app_factory = quantum.api.versions:Versions.factory
[app:quantumapi]
paste.app_factory = quantum.api:APIRouterV01.factory

View File

@ -13,4 +13,69 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
# @author: Somik Behera, Nicira Networks, Inc. # @author: Salvatore Orlando, Citrix Systems
"""
Quantum API controllers.
"""
import logging
import routes
import webob.dec
import webob.exc
from quantum.api import faults
from quantum.api import networks
from quantum.common import flags
from quantum.common import wsgi
LOG = logging.getLogger('quantum.api')
FLAGS = flags.FLAGS
class FaultWrapper(wsgi.Middleware):
"""Calls down the middleware stack, making exceptions into faults."""
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
try:
return req.get_response(self.application)
except Exception as ex:
LOG.exception(_("Caught error: %s"), unicode(ex))
exc = webob.exc.HTTPInternalServerError(explanation=unicode(ex))
return faults.Fault(exc)
class APIRouterV01(wsgi.Router):
"""
Routes requests on the Quantum API to the appropriate controller
"""
def __init__(self, ext_mgr=None):
mapper = routes.Mapper()
self._setup_routes(mapper)
super(APIRouterV01, self).__init__(mapper)
def _setup_routes(self, mapper):
#server_members = self.server_members
#server_members['action'] = 'POST'
#server_members['pause'] = 'POST'
#server_members['unpause'] = 'POST'
#server_members['diagnostics'] = 'GET'
#server_members['actions'] = 'GET'
#server_members['suspend'] = 'POST'
#server_members['resume'] = 'POST'
#server_members['rescue'] = 'POST'
#server_members['unrescue'] = 'POST'
#server_members['reset_network'] = 'POST'
#server_members['inject_network_info'] = 'POST'
mapper.resource("network", "networks", controller=networks.Controller(),
collection={'detail': 'GET'})
print mapper
#mapper.resource("port", "ports", controller=ports.Controller(),
# collection=dict(public='GET', private='GET'),
# parent_resource=dict(member_name='network',
# collection_name='networks'))

21
quantum/api/api_common.py Normal file
View File

@ -0,0 +1,21 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Citrix System.
# 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.
XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1'
XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0'

62
quantum/api/faults.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 webob.dec
import webob.exc
from quantum.api import api_common as common
from quantum.common import wsgi
class Fault(webob.exc.HTTPException):
"""Error codes for API faults"""
_fault_names = {
400: "malformedRequest",
401: "unauthorized",
402: "networkNotFound",
403: "requestedStateInvalid",
460: "networkInUse",
461: "alreadyAttached",
462: "portInUse",
470: "serviceUnavailable",
471: "pluginFault"
}
def __init__(self, exception):
"""Create a Fault for the given webob.exc.exception."""
self.wrapped_exc = exception
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Generate a WSGI response based on the exception passed to ctor."""
# Replace the body with fault details.
code = self.wrapped_exc.status_int
fault_name = self._fault_names.get(code, "quantumServiceFault")
fault_data = {
fault_name: {
'code': code,
'message': self.wrapped_exc.explanation}}
#TODO (salvatore-orlando): place over-limit stuff here
# 'code' is an attribute on the fault tag itself
metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
default_xmlns = common.XML_NS_V10
serializer = wsgi.Serializer(metadata, default_xmlns)
content_type = req.best_match_content_type()
self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
self.wrapped_exc.content_type = content_type
return self.wrapped_exc

200
quantum/api/networks.py Normal file
View File

@ -0,0 +1,200 @@
# 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 base64
import logging
import traceback
from webob import exc
from xml.dom import minidom
from quantum import manager
from quantum import quantum_plugin_base
from quantum.common import exceptions as exception
from quantum.common import flags
from quantum.common import wsgi
from quantum import utils
from quantum.api import api_common as common
from quantum.api import faults
import quantum.api
LOG = logging.getLogger('quantum.api.networks')
FLAGS = flags.FLAGS
class Controller(wsgi.Controller):
""" Network API controller for Quantum API """
#TODO (salvatore-orlando): adjust metadata for quantum
_serialization_metadata = {
"application/xml": {
"attributes": {
"server": ["id", "imageId", "name", "flavorId", "hostId",
"status", "progress", "adminPass", "flavorRef",
"imageRef"],
"link": ["rel", "type", "href"],
},
"dict_collections": {
"metadata": {"item_name": "meta", "item_key": "key"},
},
"list_collections": {
"public": {"item_name": "ip", "item_key": "addr"},
"private": {"item_name": "ip", "item_key": "addr"},
},
},
}
def index(self, request):
""" Returns a list of network names and ids """
#TODO: this should be for a given tenant!!!
print "PIPPO"
LOG.debug("HERE - index")
return self._items(request, is_detail=False)
def _items(self, req, is_detail):
""" Returns a list of networks. """
#TODO: we should return networks for a given tenant only
#TODO: network controller should be retrieved here!!!
test = { 'ciao':'bello','porco':'mondo' }
#builder = self._get_view_builder(req)
#servers = [builder.build(inst, is_detail)['server']
# for inst in limited_list]
#return dict(servers=servers)
return test
def show(self, req, id):
""" Returns network details by network id """
try:
return "TEST NETWORK DETAILS"
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
def delete(self, req, id):
""" Destroys the network with the given id """
try:
return "TEST NETWORK DELETE"
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
def create(self, req):
""" Creates a new network for a given tenant """
#env = self._deserialize_create(req)
#if not env:
# return faults.Fault(exc.HTTPUnprocessableEntity())
return "TEST NETWORK CREATE"
def _deserialize_create(self, request):
"""
Deserialize a create request
Overrides normal behavior in the case of xml content
"""
#if request.content_type == "application/xml":
# deserializer = ServerCreateRequestXMLDeserializer()
# return deserializer.deserialize(request.body)
#else:
# return self._deserialize(request.body, request.get_content_type())
pass
def update(self, req, id):
""" Updates the name for the network wit the given id """
if len(req.body) == 0:
raise exc.HTTPUnprocessableEntity()
inst_dict = self._deserialize(req.body, req.get_content_type())
if not inst_dict:
return faults.Fault(exc.HTTPUnprocessableEntity())
try:
return "TEST NETWORK UPDATE"
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPNoContent()
class NetworkCreateRequestXMLDeserializer(object):
"""
Deserializer to handle xml-formatted server create requests.
Handles standard server attributes as well as optional metadata
and personality attributes
"""
def deserialize(self, string):
"""Deserialize an xml-formatted server create request"""
dom = minidom.parseString(string)
server = self._extract_server(dom)
return {'server': server}
def _extract_server(self, node):
"""Marshal the server attribute of a parsed request"""
server = {}
server_node = self._find_first_child_named(node, 'server')
for attr in ["name", "imageId", "flavorId"]:
server[attr] = server_node.getAttribute(attr)
metadata = self._extract_metadata(server_node)
if metadata is not None:
server["metadata"] = metadata
personality = self._extract_personality(server_node)
if personality is not None:
server["personality"] = personality
return server
def _extract_metadata(self, server_node):
"""Marshal the metadata attribute of a parsed request"""
metadata_node = self._find_first_child_named(server_node, "metadata")
if metadata_node is None:
return None
metadata = {}
for meta_node in self._find_children_named(metadata_node, "meta"):
key = meta_node.getAttribute("key")
metadata[key] = self._extract_text(meta_node)
return metadata
def _extract_personality(self, server_node):
"""Marshal the personality attribute of a parsed request"""
personality_node = \
self._find_first_child_named(server_node, "personality")
if personality_node is None:
return None
personality = []
for file_node in self._find_children_named(personality_node, "file"):
item = {}
if file_node.hasAttribute("path"):
item["path"] = file_node.getAttribute("path")
item["contents"] = self._extract_text(file_node)
personality.append(item)
return personality
def _find_first_child_named(self, parent, name):
"""Search a nodes children for the first child with a given name"""
for node in parent.childNodes:
if node.nodeName == name:
return node
return None
def _find_children_named(self, parent, name):
"""Return all of a nodes children who have the given name"""
for node in parent.childNodes:
if node.nodeName == name:
yield node
def _extract_text(self, node):
"""Get the text field contained by the given node"""
if len(node.childNodes) == 1:
child = node.childNodes[0]
if child.nodeType == child.TEXT_NODE:
return child.nodeValue
return ""

View File

@ -244,10 +244,12 @@ def load_paste_config(app_name, options, args):
problem loading the configuration file. problem loading the configuration file.
""" """
conf_file = find_config_file(options, args) conf_file = find_config_file(options, args)
print "Conf_file:%s" %conf_file
if not conf_file: if not conf_file:
raise RuntimeError("Unable to locate any configuration file. " raise RuntimeError("Unable to locate any configuration file. "
"Cannot load application %s" % app_name) "Cannot load application %s" % app_name)
try: try:
print "App_name:%s" %app_name
conf = deploy.appconfig("config:%s" % conf_file, name=app_name) conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
return conf_file, conf return conf_file, conf
except Exception, e: except Exception, e:
@ -255,7 +257,7 @@ def load_paste_config(app_name, options, args):
% (conf_file, e)) % (conf_file, e))
def load_paste_app(app_name, options, args): def load_paste_app(conf_file, app_name):
""" """
Builds and returns a WSGI app from a paste config file. Builds and returns a WSGI app from a paste config file.
@ -276,40 +278,16 @@ def load_paste_app(app_name, options, args):
:raises RuntimeError when config file cannot be located or application :raises RuntimeError when config file cannot be located or application
cannot be loaded from config file cannot be loaded from config file
""" """
conf_file, conf = load_paste_config(app_name, options, args) #conf_file, conf = load_paste_config(app_name, options, args)
try: try:
# Setup logging early, supplying both the CLI options and the conf_file = os.path.abspath(conf_file)
# 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
# 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...
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) app = deploy.loadapp("config:%s" % conf_file, name=app_name)
except (LookupError, ImportError), e: except (LookupError, ImportError), e:
raise RuntimeError("Unable to load %(app_name)s from " raise RuntimeError("Unable to load %(app_name)s from "
"configuration file %(conf_file)s." "configuration file %(conf_file)s."
"\nGot: %(e)r" % locals()) "\nGot: %(e)r" % locals())
return conf, app return app
def get_option(options, option, **kwargs): def get_option(options, option, **kwargs):

View File

@ -29,7 +29,7 @@ import socket
import sys import sys
import ConfigParser import ConfigParser
from common import exceptions from quantum.common import exceptions
from exceptions import ProcessExecutionError from exceptions import ProcessExecutionError

View File

@ -1,3 +1,4 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
# #
# Copyright 2011, Nicira Networks, Inc. # Copyright 2011, Nicira Networks, Inc.
@ -253,6 +254,13 @@ class Router(object):
WSGI middleware that maps incoming requests to WSGI apps. WSGI middleware that maps incoming requests to WSGI apps.
""" """
@classmethod
def factory(cls, global_config, **local_config):
"""
Returns an instance of the WSGI Router class
"""
return cls()
def __init__(self, mapper): def __init__(self, mapper):
""" """
Create a router for the given routes.Mapper. Create a router for the given routes.Mapper.
@ -337,7 +345,7 @@ class Controller(object):
MIME types to information needed to serialize to that type. MIME types to information needed to serialize to that type.
""" """
_metadata = getattr(type(self), "_serialization_metadata", {}) _metadata = getattr(type(self), "_serialization_metadata", {})
serializer = Serializer(request.environ, _metadata) serializer = Serializer(_metadata)
return serializer.to_content_type(data) return serializer.to_content_type(data)

View File

@ -15,30 +15,104 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import logging
import json import json
import routes import routes
from common import wsgi from quantum.common import config
from quantum.common import wsgi
from quantum.common import exceptions as exception
from webob import Response from webob import Response
LOG = logging.getLogger('quantum.service')
class NetworkController(wsgi.Controller): class WsgiService(object):
"""Base class for WSGI based services.
def version(self, request): For each api you define, you must also define these flags:
return "Quantum version 0.1" :<api>_listen: The address on which to listen
:<api>_listen_port: The port on which to listen
"""
def __init__(self, app_name, conf_file, conf):
self.app_name = app_name
self.conf_file = conf_file
self.conf = conf
self.wsgi_app = None
def start(self):
self.wsgi_app = _run_wsgi(self.app_name, self.conf, self.conf_file)
def wait(self):
self.wsgi_app.wait()
class API(wsgi.Router): class QuantumApiService(WsgiService):
def __init__(self, options): """Class for quantum-api service."""
self.options = options
mapper = routes.Mapper() @classmethod
network_controller = NetworkController() def create(cls, conf=None, options=None, args=None):
mapper.resource("net_controller", "/network", app_name = "quantum"
controller=network_controller) if not conf:
mapper.connect("/", controller=network_controller, action="version") conf_file, conf = config.load_paste_config(
super(API, self).__init__(mapper) app_name, options, args)
if not conf:
message = (_('No paste configuration found for: %s'),
app_name)
raise exception.Error(message)
print "OPTIONS:%s" %options
print "CONF:%s" %conf
# Setup logging early, supplying both the CLI options and the
# configuration mapping from the config file
# We only update the conf dict for the verbose and debug
# flags. Everything else must be set up in the conf file...
# Log the options used when starting if we're in debug mode...
config.setup_logging(options, conf)
debug = options.get('debug') or \
config.get_option(conf, 'debug',
type='bool', default=False)
verbose = options.get('verbose') or \
config.get_option(conf, 'verbose',
type='bool', default=False)
conf['debug'] = debug
conf['verbose'] = verbose
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)
service = cls(app_name, conf_file, conf)
return service
def app_factory(global_conf, **local_conf): def serve_wsgi(cls, conf=None, options = None, args = None):
conf = global_conf.copy() try:
conf.update(local_conf) service = cls.create(conf, options, args)
return API(conf) except Exception:
logging.exception('in WsgiService.create()')
raise
service.start()
return service
def _run_wsgi(app_name, paste_conf, paste_config_file):
print "CICCIO"
LOG.info(_('Using paste.deploy config at: %s'), paste_config_file)
app = config.load_paste_app(paste_config_file, app_name)
if not app:
LOG.error(_('No known API applications configured in %s.'),
paste_config_file)
return
server = wsgi.Server()
server.start(app,
int(paste_conf['bind_port']),paste_conf['bind_host'])
return server