
2. Added router to Route /datacenters/ and /services/ URLs 3. Added stubs for windc/core/api. 4. Fixed start-up process for service ------------------------------------------------- Now it is working service which will reply for curl http://localhost:8181/tenant_id/datacenters/ curl http://localhost:8181/tenant_id/datacenters/dc_id/services curl http://localhost:8181/tenant_id/datacenters/dc_id/services/service_id
539 lines
19 KiB
Python
539 lines
19 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2011 OpenStack LLC.
|
|
# Copyright 2011 Justin Santa Barbara
|
|
# 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 imp
|
|
import os
|
|
import routes
|
|
import webob.dec
|
|
import webob.exc
|
|
import logging
|
|
from lxml import etree
|
|
|
|
from openstack.common import exception
|
|
from openstack.common import wsgi
|
|
|
|
LOG = logging.getLogger('extensions')
|
|
DEFAULT_XMLNS = "http://docs.openstack.org/"
|
|
XMLNS_ATOM = "http://www.w3.org/2005/Atom"
|
|
|
|
|
|
class ExtensionDescriptor(object):
|
|
"""Base class that defines the contract for extensions.
|
|
|
|
Note that you don't have to derive from this class to have a valid
|
|
extension; it is purely a convenience.
|
|
|
|
"""
|
|
|
|
def get_name(self):
|
|
"""The name of the extension.
|
|
|
|
e.g. 'Fox In Socks'
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_alias(self):
|
|
"""The alias for the extension.
|
|
|
|
e.g. 'FOXNSOX'
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_description(self):
|
|
"""Friendly description for the extension.
|
|
|
|
e.g. 'The Fox In Socks Extension'
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_namespace(self):
|
|
"""The XML namespace for the extension.
|
|
|
|
e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_updated(self):
|
|
"""The timestamp when the extension was last updated.
|
|
|
|
e.g. '2011-01-22T13:25:27-06:00'
|
|
|
|
"""
|
|
# NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
|
|
raise NotImplementedError()
|
|
|
|
def get_resources(self):
|
|
"""List of extensions.ResourceExtension extension objects.
|
|
|
|
Resources define new nouns, and are accessible through URLs.
|
|
|
|
"""
|
|
resources = []
|
|
return resources
|
|
|
|
def get_actions(self):
|
|
"""List of extensions.ActionExtension extension objects.
|
|
|
|
Actions are verbs callable from the API.
|
|
|
|
"""
|
|
actions = []
|
|
return actions
|
|
|
|
def get_request_extensions(self):
|
|
"""List of extensions.RequestException extension objects.
|
|
|
|
Request extensions are used to handle custom request data.
|
|
|
|
"""
|
|
request_exts = []
|
|
return request_exts
|
|
|
|
|
|
class ActionExtensionController(object):
|
|
def __init__(self, application):
|
|
self.application = application
|
|
self.action_handlers = {}
|
|
|
|
def add_action(self, action_name, handler):
|
|
self.action_handlers[action_name] = handler
|
|
|
|
def action(self, req, id, body):
|
|
for action_name, handler in self.action_handlers.iteritems():
|
|
if action_name in body:
|
|
return handler(body, req, id)
|
|
# no action handler found (bump to downstream application)
|
|
res = self.application
|
|
return res
|
|
|
|
|
|
class ActionExtensionResource(wsgi.Resource):
|
|
|
|
def __init__(self, application):
|
|
controller = ActionExtensionController(application)
|
|
wsgi.Resource.__init__(self, controller)
|
|
|
|
def add_action(self, action_name, handler):
|
|
self.controller.add_action(action_name, handler)
|
|
|
|
|
|
class RequestExtensionController(object):
|
|
|
|
def __init__(self, application):
|
|
self.application = application
|
|
self.handlers = []
|
|
|
|
def add_handler(self, handler):
|
|
self.handlers.append(handler)
|
|
|
|
def process(self, req, *args, **kwargs):
|
|
res = req.get_response(self.application)
|
|
# currently request handlers are un-ordered
|
|
for handler in self.handlers:
|
|
res = handler(req, res)
|
|
return res
|
|
|
|
|
|
class RequestExtensionResource(wsgi.Resource):
|
|
|
|
def __init__(self, application):
|
|
controller = RequestExtensionController(application)
|
|
wsgi.Resource.__init__(self, controller)
|
|
|
|
def add_handler(self, handler):
|
|
self.controller.add_handler(handler)
|
|
|
|
|
|
class ExtensionsResource(wsgi.Resource):
|
|
|
|
def __init__(self, extension_manager):
|
|
self.extension_manager = extension_manager
|
|
body_serializers = {'application/xml': ExtensionsXMLSerializer()}
|
|
serializer = wsgi.ResponseSerializer(body_serializers=body_serializers)
|
|
super(ExtensionsResource, self).__init__(self, None, serializer)
|
|
|
|
def _translate(self, ext):
|
|
ext_data = {}
|
|
ext_data['name'] = ext.get_name()
|
|
ext_data['alias'] = ext.get_alias()
|
|
ext_data['description'] = ext.get_description()
|
|
ext_data['namespace'] = ext.get_namespace()
|
|
ext_data['updated'] = ext.get_updated()
|
|
ext_data['links'] = [] # TODO(dprince): implement extension links
|
|
return ext_data
|
|
|
|
def index(self, req):
|
|
extensions = []
|
|
for _alias, ext in self.extension_manager.extensions.iteritems():
|
|
extensions.append(self._translate(ext))
|
|
return dict(extensions=extensions)
|
|
|
|
def show(self, req, id):
|
|
# NOTE(dprince): the extensions alias is used as the 'id' for show
|
|
ext = self.extension_manager.extensions.get(id, None)
|
|
if not ext:
|
|
raise webob.exc.HTTPNotFound(
|
|
_("Extension with alias %s does not exist") % id)
|
|
|
|
return dict(extension=self._translate(ext))
|
|
|
|
def delete(self, req, id):
|
|
raise webob.exc.HTTPNotFound()
|
|
|
|
def create(self, req):
|
|
raise webob.exc.HTTPNotFound()
|
|
|
|
|
|
class ExtensionMiddleware(wsgi.Middleware):
|
|
"""Extensions middleware for WSGI."""
|
|
|
|
@classmethod
|
|
def factory(cls, global_config, **local_config):
|
|
"""Paste factory."""
|
|
def _factory(app):
|
|
return cls(app, global_config, **local_config)
|
|
return _factory
|
|
|
|
def _action_ext_resources(self, application, ext_mgr, mapper):
|
|
"""Return a dict of ActionExtensionResource-s by collection."""
|
|
action_resources = {}
|
|
for action in ext_mgr.get_actions():
|
|
if not action.collection in action_resources.keys():
|
|
resource = ActionExtensionResource(application)
|
|
mapper.connect("/%s/:(id)/action.:(format)" %
|
|
action.collection,
|
|
action='action',
|
|
controller=resource,
|
|
conditions=dict(method=['POST']))
|
|
mapper.connect("/%s/:(id)/action" %
|
|
action.collection,
|
|
action='action',
|
|
controller=resource,
|
|
conditions=dict(method=['POST']))
|
|
action_resources[action.collection] = resource
|
|
|
|
return action_resources
|
|
|
|
def _request_ext_resources(self, application, ext_mgr, mapper):
|
|
"""Returns a dict of RequestExtensionResource-s by collection."""
|
|
request_ext_resources = {}
|
|
for req_ext in ext_mgr.get_request_extensions():
|
|
if not req_ext.key in request_ext_resources.keys():
|
|
resource = RequestExtensionResource(application)
|
|
mapper.connect(req_ext.url_route + '.:(format)',
|
|
action='process',
|
|
controller=resource,
|
|
conditions=req_ext.conditions)
|
|
|
|
mapper.connect(req_ext.url_route,
|
|
action='process',
|
|
controller=resource,
|
|
conditions=req_ext.conditions)
|
|
request_ext_resources[req_ext.key] = resource
|
|
|
|
return request_ext_resources
|
|
|
|
def __init__(self, application, config, ext_mgr=None):
|
|
ext_mgr = ext_mgr or ExtensionManager(
|
|
config['api_extensions_path'])
|
|
mapper = routes.Mapper()
|
|
|
|
# extended resources
|
|
for resource_ext in ext_mgr.get_resources():
|
|
LOG.debug(_('Extended resource: %s'), resource_ext.collection)
|
|
controller_resource = wsgi.Resource(resource_ext.controller,
|
|
resource_ext.deserializer,
|
|
resource_ext.serializer)
|
|
self._map_custom_collection_actions(resource_ext, mapper,
|
|
controller_resource)
|
|
kargs = dict(controller=controller_resource,
|
|
collection=resource_ext.collection_actions,
|
|
member=resource_ext.member_actions)
|
|
if resource_ext.parent:
|
|
kargs['parent_resource'] = resource_ext.parent
|
|
mapper.resource(resource_ext.collection,
|
|
resource_ext.collection, **kargs)
|
|
|
|
# extended actions
|
|
action_resources = self._action_ext_resources(application, ext_mgr,
|
|
mapper)
|
|
for action in ext_mgr.get_actions():
|
|
LOG.debug(_('Extended action: %s'), action.action_name)
|
|
resource = action_resources[action.collection]
|
|
resource.add_action(action.action_name, action.handler)
|
|
|
|
# extended requests
|
|
req_controllers = self._request_ext_resources(application, ext_mgr,
|
|
mapper)
|
|
for request_ext in ext_mgr.get_request_extensions():
|
|
LOG.debug(_('Extended request: %s'), request_ext.key)
|
|
controller = req_controllers[request_ext.key]
|
|
controller.add_handler(request_ext.handler)
|
|
|
|
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
|
mapper)
|
|
|
|
super(ExtensionMiddleware, self).__init__(application)
|
|
|
|
def _map_custom_collection_actions(self, resource_ext, mapper,
|
|
controller_resource):
|
|
for action, method in resource_ext.collection_actions.iteritems():
|
|
parent = resource_ext.parent
|
|
conditions = dict(method=[method])
|
|
path = "/%s/%s" % (resource_ext.collection, action)
|
|
|
|
path_prefix = ""
|
|
if parent:
|
|
path_prefix = "/%s/{%s_id}" % (parent["collection_name"],
|
|
parent["member_name"])
|
|
|
|
with mapper.submapper(controller=controller_resource,
|
|
action=action,
|
|
path_prefix=path_prefix,
|
|
conditions=conditions) as submap:
|
|
submap.connect(path)
|
|
submap.connect("%s.:(format)" % path)
|
|
|
|
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
|
def __call__(self, req):
|
|
"""Route the incoming request with router."""
|
|
req.environ['extended.app'] = self.application
|
|
return self._router
|
|
|
|
@staticmethod
|
|
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
|
def _dispatch(req):
|
|
"""Dispatch the request.
|
|
|
|
Returns the routed WSGI app's response or defers to the extended
|
|
application.
|
|
|
|
"""
|
|
match = req.environ['wsgiorg.routing_args'][1]
|
|
if not match:
|
|
return req.environ['extended.app']
|
|
app = match['controller']
|
|
return app
|
|
|
|
|
|
class ExtensionManager(object):
|
|
"""Load extensions from the configured extension path.
|
|
|
|
See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an
|
|
example extension implementation.
|
|
|
|
"""
|
|
|
|
def __init__(self, path):
|
|
LOG.debug(_('Initializing extension manager.'))
|
|
|
|
self.path = path
|
|
self.extensions = {}
|
|
self._load_all_extensions()
|
|
|
|
def get_resources(self):
|
|
"""Returns a list of ResourceExtension objects."""
|
|
resources = []
|
|
extension_resource = ExtensionsResource(self)
|
|
res_ext = ResourceExtension('extensions',
|
|
extension_resource,
|
|
serializer=extension_resource.serializer)
|
|
resources.append(res_ext)
|
|
for alias, ext in self.extensions.iteritems():
|
|
try:
|
|
resources.extend(ext.get_resources())
|
|
except AttributeError:
|
|
# NOTE(dprince): Extension aren't required to have resource
|
|
# extensions
|
|
pass
|
|
return resources
|
|
|
|
def get_actions(self):
|
|
"""Returns a list of ActionExtension objects."""
|
|
actions = []
|
|
for alias, ext in self.extensions.iteritems():
|
|
try:
|
|
actions.extend(ext.get_actions())
|
|
except AttributeError:
|
|
# NOTE(dprince): Extension aren't required to have action
|
|
# extensions
|
|
pass
|
|
return actions
|
|
|
|
def get_request_extensions(self):
|
|
"""Returns a list of RequestExtension objects."""
|
|
request_exts = []
|
|
for alias, ext in self.extensions.iteritems():
|
|
try:
|
|
request_exts.extend(ext.get_request_extensions())
|
|
except AttributeError:
|
|
# NOTE(dprince): Extension aren't required to have request
|
|
# extensions
|
|
pass
|
|
return request_exts
|
|
|
|
def _check_extension(self, extension):
|
|
"""Checks for required methods in extension objects."""
|
|
try:
|
|
LOG.debug(_('Ext name: %s'), extension.get_name())
|
|
LOG.debug(_('Ext alias: %s'), extension.get_alias())
|
|
LOG.debug(_('Ext description: %s'), extension.get_description())
|
|
LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
|
|
LOG.debug(_('Ext updated: %s'), extension.get_updated())
|
|
except AttributeError as ex:
|
|
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
|
|
return False
|
|
return True
|
|
|
|
def _load_all_extensions(self):
|
|
"""Load extensions from the configured path.
|
|
|
|
Load extensions from the configured path. The extension name is
|
|
constructed from the module_name. If your extension module was named
|
|
widgets.py the extension class within that module should be
|
|
'Widgets'.
|
|
|
|
In addition, extensions are loaded from the 'contrib' directory.
|
|
|
|
See nova/tests/api/openstack/extensions/foxinsocks.py for an example
|
|
extension implementation.
|
|
|
|
"""
|
|
if os.path.exists(self.path):
|
|
self._load_all_extensions_from_path(self.path)
|
|
|
|
contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
|
|
if os.path.exists(contrib_path):
|
|
self._load_all_extensions_from_path(contrib_path)
|
|
|
|
def _load_all_extensions_from_path(self, path):
|
|
for f in os.listdir(path):
|
|
LOG.debug(_('Loading extension file: %s'), f)
|
|
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
|
|
ext_path = os.path.join(path, f)
|
|
if file_ext.lower() == '.py' and not mod_name.startswith('_'):
|
|
mod = imp.load_source(mod_name, ext_path)
|
|
ext_name = mod_name[0].upper() + mod_name[1:]
|
|
new_ext_class = getattr(mod, ext_name, None)
|
|
if not new_ext_class:
|
|
LOG.warn(_('Did not find expected name '
|
|
'"%(ext_name)s" in %(file)s'),
|
|
{'ext_name': ext_name,
|
|
'file': ext_path})
|
|
continue
|
|
new_ext = new_ext_class()
|
|
self.add_extension(new_ext)
|
|
|
|
def add_extension(self, ext):
|
|
# Do nothing if the extension doesn't check out
|
|
if not self._check_extension(ext):
|
|
return
|
|
|
|
alias = ext.get_alias()
|
|
LOG.debug(_('Loaded extension: %s'), alias)
|
|
|
|
if alias in self.extensions:
|
|
raise exception.Error("Found duplicate extension: %s" % alias)
|
|
self.extensions[alias] = ext
|
|
|
|
|
|
class RequestExtension(object):
|
|
"""Extend requests and responses of core nova OpenStack API resources.
|
|
|
|
Provide a way to add data to responses and handle custom request data
|
|
that is sent to core nova OpenStack API controllers.
|
|
|
|
"""
|
|
def __init__(self, method, url_route, handler):
|
|
self.url_route = url_route
|
|
self.handler = handler
|
|
self.conditions = dict(method=[method])
|
|
self.key = "%s-%s" % (method, url_route)
|
|
|
|
|
|
class ActionExtension(object):
|
|
"""Add custom actions to core nova OpenStack API resources."""
|
|
|
|
def __init__(self, collection, action_name, handler):
|
|
self.collection = collection
|
|
self.action_name = action_name
|
|
self.handler = handler
|
|
|
|
|
|
class ResourceExtension(object):
|
|
"""Add top level resources to the OpenStack API in nova."""
|
|
|
|
def __init__(self, collection, controller, parent=None,
|
|
collection_actions=None, member_actions=None,
|
|
deserializer=None, serializer=None):
|
|
if not collection_actions:
|
|
collection_actions = {}
|
|
if not member_actions:
|
|
member_actions = {}
|
|
self.collection = collection
|
|
self.controller = controller
|
|
self.parent = parent
|
|
self.collection_actions = collection_actions
|
|
self.member_actions = member_actions
|
|
self.deserializer = deserializer
|
|
self.serializer = serializer
|
|
|
|
|
|
class ExtensionsXMLSerializer(wsgi.XMLDictSerializer):
|
|
|
|
def __init__(self):
|
|
self.nsmap = {None: DEFAULT_XMLNS, 'atom': XMLNS_ATOM}
|
|
|
|
def show(self, ext_dict):
|
|
ext = etree.Element('extension', nsmap=self.nsmap)
|
|
self._populate_ext(ext, ext_dict['extension'])
|
|
return self._to_xml(ext)
|
|
|
|
def index(self, exts_dict):
|
|
exts = etree.Element('extensions', nsmap=self.nsmap)
|
|
for ext_dict in exts_dict['extensions']:
|
|
ext = etree.SubElement(exts, 'extension')
|
|
self._populate_ext(ext, ext_dict)
|
|
return self._to_xml(exts)
|
|
|
|
def _populate_ext(self, ext_elem, ext_dict):
|
|
"""Populate an extension xml element from a dict."""
|
|
|
|
ext_elem.set('name', ext_dict['name'])
|
|
ext_elem.set('namespace', ext_dict['namespace'])
|
|
ext_elem.set('alias', ext_dict['alias'])
|
|
ext_elem.set('updated', ext_dict['updated'])
|
|
desc = etree.Element('description')
|
|
desc.text = ext_dict['description']
|
|
ext_elem.append(desc)
|
|
for link in ext_dict.get('links', []):
|
|
elem = etree.SubElement(ext_elem, '{%s}link' % XMLNS_ATOM)
|
|
elem.set('rel', link['rel'])
|
|
elem.set('href', link['href'])
|
|
elem.set('type', link['type'])
|
|
return ext_elem
|
|
|
|
def _to_xml(self, root):
|
|
"""Convert the xml object to an xml string."""
|
|
|
|
return etree.tostring(root, encoding='UTF-8')
|