]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Work in progress on network API
authorSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Tue, 24 May 2011 16:45:16 +0000 (17:45 +0100)
committerSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Tue, 24 May 2011 16:45:16 +0000 (17:45 +0100)
bin/quantum
etc/quantum.conf
quantum/api/__init__.py
quantum/api/api_common.py [new file with mode: 0644]
quantum/api/faults.py [new file with mode: 0644]
quantum/api/networks.py [new file with mode: 0644]
quantum/common/config.py
quantum/common/utils.py
quantum/common/wsgi.py
quantum/service.py

index 780ccc61a844be30f77dfeb44e2d430a60aa281c..9971e795add0da830a2ebf7d9dcc931eb78e3d32 100755 (executable)
@@ -35,6 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')):
 
 gettext.install('quantum', unicode=1)
 
+from quantum import service 
 from quantum.common import wsgi
 from quantum.common import config
 
@@ -54,10 +55,18 @@ if __name__ == '__main__':
     (options, args) = config.parse_options(oparser)
 
     try:
-        conf, app = config.load_paste_app('quantumversionapp', options, args)
-        server = wsgi.Server()
-        server.start(app, int(conf['bind_port']), conf['bind_host'])
-        server.wait()
+        print "HERE-1"
+        service = service.serve_wsgi(service.QuantumApiService,
+                                     options=options,
+                                     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:
         sys.exit("ERROR: %s" % e)
 
index 91904603d347f54671d3593c9eb9da076784989b..ba96a9a275f7e1abd490d4dcc31827fd255cf249 100644 (file)
@@ -11,9 +11,15 @@ 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
+[composite:quantum]
+use = egg:Paste#urlmap
+/: quantumversions
+/v0.1: quantumapi
 
-[app:quantumversionapp]
+[app:quantumversions]
 paste.app_factory = quantum.api.versions:Versions.factory
 
+[app:quantumapi]
+paste.app_factory = quantum.api:APIRouterV01.factory
+
+
index 9602374ca26dd6ee63c8ca349447991ff26237e6..9e7d5549758c586dea415f6169d8b1d27e8b883f 100644 (file)
 #    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
+# @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'))
+
diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py
new file mode 100644 (file)
index 0000000..b33987b
--- /dev/null
@@ -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'
+
diff --git a/quantum/api/faults.py b/quantum/api/faults.py
new file mode 100644 (file)
index 0000000..d61ae79
--- /dev/null
@@ -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
diff --git a/quantum/api/networks.py b/quantum/api/networks.py
new file mode 100644 (file)
index 0000000..f98aa43
--- /dev/null
@@ -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 ""
index 2d858ed357fa737beed74823f3b5fce0280d55cc..cda7650781ff917d06f37fe7dfa0965c45d64d30 100644 (file)
@@ -244,10 +244,12 @@ def load_paste_config(app_name, options, args):
             problem loading the configuration file.
     """
     conf_file = find_config_file(options, args)
+    print "Conf_file:%s" %conf_file
     if not conf_file:
         raise RuntimeError("Unable to locate any configuration file. "
                             "Cannot load application %s" % app_name)
     try:
+        print "App_name:%s" %app_name
         conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
         return conf_file, conf
     except Exception, e:
@@ -255,7 +257,7 @@ def load_paste_config(app_name, options, args):
                            % (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.
 
@@ -276,40 +278,16 @@ def load_paste_app(app_name, options, args):
     :raises RuntimeError when config file cannot be located or application
             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:
-        # 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
-        # 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)
+        conf_file = os.path.abspath(conf_file)
         app = deploy.loadapp("config:%s" % conf_file, name=app_name)
     except (LookupError, ImportError), e:
         raise RuntimeError("Unable to load %(app_name)s from "
                            "configuration file %(conf_file)s."
                            "\nGot: %(e)r" % locals())
-    return conf, app
+    return app
 
 
 def get_option(options, option, **kwargs):
index 435ec7b878834e1b0526549a11af09c686388821..c56a53ac135009d680256432e67a4eeb30ed0993 100644 (file)
@@ -29,7 +29,7 @@ import socket
 import sys
 import ConfigParser
 
-from common import exceptions
+from quantum.common import exceptions
 from exceptions import ProcessExecutionError
 
 
index 73b826ef92417604d2b32237564800d922286b30..9a2b5bb580da0b803550654da46fb0e1626370ad 100644 (file)
@@ -1,3 +1,4 @@
+
 # vim: tabstop=4 shiftwidth=4 softtabstop=4
 #
 # Copyright 2011, Nicira Networks, Inc.
@@ -253,6 +254,13 @@ class Router(object):
     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):
         """
         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.
         """
         _metadata = getattr(type(self), "_serialization_metadata", {})
-        serializer = Serializer(request.environ, _metadata)
+        serializer = Serializer(_metadata)
         return serializer.to_content_type(data)
 
 
index 50a8effa3bce81627207cf62ddd217f5d633c39f..760263bf935458cebdc33791c56df75cf0c8c365 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import logging
 import json
 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
 
+LOG = logging.getLogger('quantum.service')
 
-class NetworkController(wsgi.Controller):
+class WsgiService(object):
+    """Base class for WSGI based services.
 
-    def version(self, request):
-        return "Quantum version 0.1"
+    For each api you define, you must also define these flags:
+    :<api>_listen: The address on which to listen
+    :<api>_listen_port: The port on which to listen
 
+    """
 
-class API(wsgi.Router):
-    def __init__(self, options):
-        self.options = options
-        mapper = routes.Mapper()
-        network_controller = NetworkController()
-        mapper.resource("net_controller", "/network",
-                        controller=network_controller)
-        mapper.connect("/", controller=network_controller, action="version")
-        super(API, self).__init__(mapper)
+    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 app_factory(global_conf, **local_conf):
-    conf = global_conf.copy()
-    conf.update(local_conf)
-    return API(conf)
+    def wait(self):
+        self.wsgi_app.wait()
+
+
+class QuantumApiService(WsgiService):
+    """Class for quantum-api service."""
+
+    @classmethod
+    def create(cls, conf=None, options=None, args=None):
+        app_name = "quantum"
+        if not conf:
+            conf_file, conf = config.load_paste_config(
+                              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 serve_wsgi(cls, conf=None, options = None, args = None):
+    try:
+        service = cls.create(conf, options, args)
+    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
+