]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Adding first files for quantum API
authorSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Mon, 23 May 2011 20:51:00 +0000 (21:51 +0100)
committerSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Mon, 23 May 2011 20:51:00 +0000 (21:51 +0100)
1  2 
bin/quantum
etc/quantum.conf
quantum/api/__init__.py
quantum/api/versions.py
quantum/api/views/__init__.py
quantum/api/views/versions.py
quantum/common/config.py
quantum/common/exceptions.py
quantum/common/flags.py
quantum/common/wsgi.py

diff --cc bin/quantum
index 16ef7346d2c75a89289976fabce8d702113a9eca,d19fa91efa29c27e2735d1c68f0529566e2f8e70..780ccc61a844be30f77dfeb44e2d430a60aa281c
mode 100644,100644..100755
@@@ -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)
  
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..91904603d347f54671d3593c9eb9da076784989b
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
++
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..9602374ca26dd6ee63c8ca349447991ff26237e6
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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.
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..36cd274d1f5fbc4324d9b188d87c0734f94596ca
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..ea910340037c271817817af55c18edf0c47a8c34
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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.
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..d0145c94a381c7b482146f05681373fd009311a3
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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)
index dbbcd260fc932c03db1c44847d276f380484c229,21c1b59514b250b608e3ce16b9b2147ed43a3b28..2d858ed357fa737beed74823f3b5fce0280d55cc
@@@ -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
      """
      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
          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 "
index c434e736e7da7073c3213bff29fcdc672a96ef48,c434e736e7da7073c3213bff29fcdc672a96ef48..bcc7696a216ffcc136d8e58cb70444132e6f83c9
@@@ -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
index 51cbe58be054bbbaa13d2724b5255141048d44c5,51cbe58be054bbbaa13d2724b5255141048d44c5..947999d0f22e94123df5dda205632549d0f75c71
@@@ -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")
++
index 6c7caa1dc908dfcf641e0ea161fd57e5624edcbd,6c7caa1dc908dfcf641e0ea161fd57e5624edcbd..73b826ef92417604d2b32237564800d922286b30
@@@ -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):
          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: