Reworked WSGI helper module and converted rackspace API endpoint to use it.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable-msg=C0103
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
@@ -17,31 +18,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
WSGI daemon for the main API endpoint.
|
||||
Daemon for the Rackspace API endpoint.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from tornado import ioloop
|
||||
from wsgiref import simple_server
|
||||
|
||||
from nova import flags
|
||||
from nova import rpc
|
||||
from nova import server
|
||||
from nova import utils
|
||||
from nova.auth import manager
|
||||
from nova import wsgi
|
||||
from nova.endpoint import rackspace
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
|
||||
|
||||
def main(_argv):
|
||||
api_instance = rackspace.Api()
|
||||
http_server = simple_server.WSGIServer(('0.0.0.0', FLAGS.cc_port), simple_server.WSGIRequestHandler)
|
||||
http_server.set_app(api_instance.handler)
|
||||
logging.debug('Started HTTP server on port %i' % FLAGS.cc_port)
|
||||
while True:
|
||||
http_server.handle_request()
|
||||
|
||||
if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
server.serve('nova-rsapi', main)
|
||||
wsgi.run_server(rackspace.API(), FLAGS.cc_port)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import eventlet
|
||||
import eventlet.wsgi
|
||||
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||
|
||||
def serve(app, port):
|
||||
sock = eventlet.listen(('0.0.0.0', port))
|
||||
eventlet.wsgi.server(sock, app)
|
||||
@@ -1,136 +0,0 @@
|
||||
import eventletserver
|
||||
import carrot.connection
|
||||
import carrot.messaging
|
||||
import itertools
|
||||
import routes
|
||||
|
||||
|
||||
# See http://pythonpaste.org/webob/ for usage
|
||||
from webob.dec import wsgify
|
||||
from webob import exc, Request, Response
|
||||
|
||||
class WSGILayer(object):
|
||||
def __init__(self, application=None):
|
||||
self.application = application
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# Subclasses will probably want to implement __call__ like this:
|
||||
#
|
||||
# @wsgify
|
||||
# def __call__(self, req):
|
||||
# # Any of the following objects work as responses:
|
||||
#
|
||||
# # Option 1: simple string
|
||||
# resp = 'message\n'
|
||||
#
|
||||
# # Option 2: a nicely formatted HTTP exception page
|
||||
# resp = 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)
|
||||
# resp = Response(); resp.app_iter = open('somefile')
|
||||
#
|
||||
# # Option 4: any wsgi app to be run next
|
||||
# resp = self.application
|
||||
#
|
||||
# # Option 5: you can get a Response object for a wsgi app, too, to
|
||||
# # play with headers etc
|
||||
# resp = req.get_response(self.application)
|
||||
#
|
||||
#
|
||||
# # You can then just return your response...
|
||||
# return resp # option 1
|
||||
# # ... or set req.response and return None.
|
||||
# req.response = resp # option 2
|
||||
#
|
||||
# See the end of http://pythonpaste.org/webob/modules/dec.html
|
||||
# for more info.
|
||||
raise NotImplementedError("You must implement __call__")
|
||||
|
||||
|
||||
class WsgiStack(WSGILayer):
|
||||
def __init__(self, wsgi_layers):
|
||||
bottom_up = list(reversed(wsgi_layers))
|
||||
app, remaining = bottom_up[0], bottom_up[1:]
|
||||
for layer in remaining:
|
||||
layer.application = app
|
||||
app = layer
|
||||
super(WsgiStack, self).__init__(app)
|
||||
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
return self.application
|
||||
|
||||
class Debug(WSGILayer):
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
for k, v in req.environ.items():
|
||||
print k, "=", v
|
||||
return self.application
|
||||
|
||||
class Auth(WSGILayer):
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
if not 'openstack.auth.token' in req.environ:
|
||||
# Check auth params here
|
||||
if True:
|
||||
req.environ['openstack.auth.token'] = '12345'
|
||||
else:
|
||||
return exc.HTTPForbidden(detail="Go away")
|
||||
|
||||
response = req.get_response(self.application)
|
||||
response.headers['X-Openstack-Auth'] = 'Success'
|
||||
return response
|
||||
|
||||
class Router(WSGILayer):
|
||||
def __init__(self, application=None):
|
||||
super(Router, self).__init__(application)
|
||||
self.map = routes.Mapper()
|
||||
self._connect()
|
||||
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
match = self.map.match(req.path_info)
|
||||
if match is None:
|
||||
return self.application
|
||||
req.environ['openstack.match'] = match
|
||||
return match['controller']
|
||||
|
||||
def _connect(self):
|
||||
raise NotImplementedError("You must implement _connect")
|
||||
|
||||
class FileRouter(Router):
|
||||
def _connect(self):
|
||||
self.map.connect(None, '/files/{file}', controller=File())
|
||||
self.map.connect(None, '/rfiles/{file}', controller=Reverse(File()))
|
||||
|
||||
class Message(WSGILayer):
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
return 'message\n'
|
||||
|
||||
class Reverse(WSGILayer):
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
inner_resp = req.get_response(self.application)
|
||||
resp = Response()
|
||||
resp.app_iter = itertools.imap(lambda x: x[::-1], inner_resp.app_iter)
|
||||
return resp
|
||||
|
||||
class File(WSGILayer):
|
||||
@wsgify
|
||||
def __call__(self, req):
|
||||
try:
|
||||
myfile = open(req.environ['openstack.match']['file'])
|
||||
except IOError, e:
|
||||
raise exc.HTTPNotFound()
|
||||
req.response = Response()
|
||||
req.response.app_iter = myfile
|
||||
|
||||
wsgi_layers = [
|
||||
Auth(),
|
||||
Debug(),
|
||||
FileRouter(),
|
||||
Message(),
|
||||
]
|
||||
eventletserver.serve(app=WsgiStack(wsgi_layers), port=12345)
|
||||
@@ -17,206 +17,95 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Rackspace API
|
||||
Rackspace API Endpoint
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import time
|
||||
|
||||
from nova import datastore
|
||||
from nova import exception
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova import flags
|
||||
from nova import rpc
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
from nova.auth import manager
|
||||
from nova.compute import model
|
||||
from nova.compute import model as compute
|
||||
from nova.network import model as network
|
||||
from nova.endpoint import images
|
||||
from nova.endpoint import wsgi
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on')
|
||||
|
||||
|
||||
class Unauthorized(Exception):
|
||||
pass
|
||||
|
||||
class NotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Api(object):
|
||||
class API(wsgi.Middleware):
|
||||
"""Entry point for all requests."""
|
||||
|
||||
def __init__(self):
|
||||
"""build endpoints here"""
|
||||
self.controllers = {
|
||||
"v1.0": RackspaceAuthenticationApi(),
|
||||
"servers": RackspaceCloudServerApi()
|
||||
}
|
||||
super(API, self).__init__(Router(webob.exc.HTTPNotFound()))
|
||||
|
||||
def handler(self, environ, responder):
|
||||
"""
|
||||
This is the entrypoint from wsgi. Read PEP 333 and wsgi.org for
|
||||
more intormation. The key points are responder is a callback that
|
||||
needs to run before you return, and takes two arguments, response
|
||||
code string ("200 OK") and headers (["X-How-Cool-Am-I: Ultra-Suede"])
|
||||
and the return value is the body of the response.
|
||||
"""
|
||||
environ['nova.context'] = self.build_context(environ)
|
||||
controller, path = wsgi.Util.route(
|
||||
environ['PATH_INFO'],
|
||||
self.controllers
|
||||
)
|
||||
logging.debug("Route %s to %s", str(path), str(controller))
|
||||
if not controller:
|
||||
responder("404 Not Found", [])
|
||||
return ""
|
||||
try:
|
||||
rv = controller.process(path, environ)
|
||||
if type(rv) is tuple:
|
||||
responder(rv[0], rv[1])
|
||||
rv = rv[2]
|
||||
else:
|
||||
responder("200 OK", [])
|
||||
return rv
|
||||
except Unauthorized:
|
||||
responder("401 Unauthorized", [])
|
||||
return ""
|
||||
except NotFound:
|
||||
responder("404 Not Found", [])
|
||||
return ""
|
||||
def __call__(self, environ, start_response):
|
||||
context = {}
|
||||
if "HTTP_X_AUTH_TOKEN" in environ:
|
||||
context['user'] = manager.AuthManager().get_user_from_access_key(
|
||||
environ['HTTP_X_AUTH_TOKEN'])
|
||||
if context['user']:
|
||||
context['project'] = manager.AuthManager().get_project(
|
||||
context['user'].name)
|
||||
if "user" not in context:
|
||||
return webob.exc.HTTPForbidden()(environ, start_response)
|
||||
environ['nova.context'] = context
|
||||
return self.application(environ, start_response)
|
||||
|
||||
|
||||
def build_context(self, env):
|
||||
rv = {}
|
||||
if env.has_key("HTTP_X_AUTH_TOKEN"):
|
||||
rv['user'] = manager.AuthManager().get_user_from_access_key(
|
||||
env['HTTP_X_AUTH_TOKEN']
|
||||
)
|
||||
if rv['user']:
|
||||
rv['project'] = manager.AuthManager().get_project(
|
||||
rv['user'].name
|
||||
)
|
||||
return rv
|
||||
class Router(wsgi.Router):
|
||||
"""Route requests to the next WSGI application."""
|
||||
|
||||
def _build_map(self):
|
||||
"""Build routing map for authentication and cloud."""
|
||||
self._connect("/v1.0", controller=AuthenticationAPI())
|
||||
cloud = CloudServerAPI()
|
||||
self._connect("/servers", controller=cloud.launch_server,
|
||||
conditions={"method": ["POST"]})
|
||||
self._connect("/servers/{server_id}", controller=cloud.delete_server,
|
||||
conditions={'method': ["DELETE"]})
|
||||
self._connect("/servers", controller=cloud)
|
||||
|
||||
|
||||
class RackspaceApiEndpoint(object):
|
||||
def process(self, path, env):
|
||||
"""
|
||||
Main entrypoint for all controllers (what gets run by the wsgi handler).
|
||||
Check authentication based on key, raise Unauthorized if invalid.
|
||||
class AuthenticationAPI(wsgi.Application):
|
||||
"""Handle all authorization requests through WSGI applications."""
|
||||
|
||||
Select the most appropriate action based on request type GET, POST, etc,
|
||||
then pass it through to the implementing controller. Defalut to GET if
|
||||
the implementing child doesn't respond to a particular type.
|
||||
"""
|
||||
if not self.check_authentication(env):
|
||||
raise Unauthorized("Unable to authenticate")
|
||||
|
||||
method = env['REQUEST_METHOD'].lower()
|
||||
callback = getattr(self, method, None)
|
||||
if not callback:
|
||||
callback = getattr(self, "get")
|
||||
logging.debug("%s processing %s with %s", self, method, callback)
|
||||
return callback(path, env)
|
||||
|
||||
def get(self, path, env):
|
||||
"""
|
||||
The default GET will look at the path and call an appropriate
|
||||
action within this controller based on the the structure of the path.
|
||||
|
||||
Given the following path lengths (with the first part stripped of by
|
||||
router, as it is the controller name):
|
||||
= 0 -> index
|
||||
= 1 -> first component (/servers/details -> details)
|
||||
>= 2 -> second path component (/servers/ID/ips/* -> ips)
|
||||
|
||||
This should return
|
||||
A String if 200 OK and no additional headers
|
||||
(CODE, HEADERS, BODY) for custom response code and headers
|
||||
"""
|
||||
if len(path) == 0 and hasattr(self, "index"):
|
||||
logging.debug("%s running index", self)
|
||||
return self.index(env)
|
||||
if len(path) >= 2:
|
||||
action = path[1]
|
||||
else:
|
||||
action = path.pop(0)
|
||||
|
||||
logging.debug("%s running action %s", self, action)
|
||||
if hasattr(self, action):
|
||||
method = getattr(self, action)
|
||||
return method(path, env)
|
||||
else:
|
||||
raise NotFound("Missing method %s" % path[0])
|
||||
|
||||
def check_authentication(self, env):
|
||||
if not env['nova.context']['user']:
|
||||
return False
|
||||
return True
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req): # pylint: disable-msg=W0221
|
||||
# TODO(todd): make a actual session with a unique token
|
||||
# just pass the auth key back through for now
|
||||
res = webob.Response()
|
||||
res.status = '204 No Content'
|
||||
res.headers.add('X-Server-Management-Url', req.host_url)
|
||||
res.headers.add('X-Storage-Url', req.host_url)
|
||||
res.headers.add('X-CDN-Managment-Url', req.host_url)
|
||||
res.headers.add('X-Auth-Token', req.headers['X-Auth-Key'])
|
||||
return res
|
||||
|
||||
|
||||
class RackspaceAuthenticationApi(object):
|
||||
|
||||
def process(self, path, env):
|
||||
return self.index(path, env)
|
||||
|
||||
# TODO(todd): make a actual session with a unique token
|
||||
# just pass the auth key back through for now
|
||||
def index(self, _path, env):
|
||||
response = '204 No Content'
|
||||
headers = [
|
||||
('X-Server-Management-Url', 'http://%s' % env['HTTP_HOST']),
|
||||
('X-Storage-Url', 'http://%s' % env['HTTP_HOST']),
|
||||
('X-CDN-Managment-Url', 'http://%s' % env['HTTP_HOST']),
|
||||
('X-Auth-Token', env['HTTP_X_AUTH_KEY'])
|
||||
]
|
||||
body = ""
|
||||
return (response, headers, body)
|
||||
|
||||
|
||||
class RackspaceCloudServerApi(RackspaceApiEndpoint):
|
||||
class CloudServerAPI(wsgi.Application):
|
||||
"""Handle all server requests through WSGI applications."""
|
||||
|
||||
def __init__(self):
|
||||
self.instdir = model.InstanceDirectory()
|
||||
super(CloudServerAPI, self).__init__()
|
||||
self.instdir = compute.InstanceDirectory()
|
||||
self.network = network.PublicNetworkController()
|
||||
|
||||
def post(self, path, env):
|
||||
if len(path) == 0:
|
||||
return self.launch_server(env)
|
||||
|
||||
def delete(self, path_parts, env):
|
||||
if self.delete_server(path_parts[0]):
|
||||
return ("202 Accepted", [], "")
|
||||
else:
|
||||
return ("404 Not Found", [],
|
||||
"Did not find image, or it was not in a running state")
|
||||
|
||||
|
||||
def index(self, env):
|
||||
return self.detail(env)
|
||||
|
||||
def detail(self, args, env):
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req): # pylint: disable-msg=W0221
|
||||
value = {"servers": []}
|
||||
for inst in self.instdir.all:
|
||||
value["servers"].append(self.instance_details(inst))
|
||||
return json.dumps(value)
|
||||
|
||||
##
|
||||
##
|
||||
|
||||
def launch_server(self, env):
|
||||
data = json.loads(env['wsgi.input'].read(int(env['CONTENT_LENGTH'])))
|
||||
inst = self.build_server_instance(data, env['nova.context'])
|
||||
self.schedule_launch_of_instance(inst)
|
||||
return json.dumps({"server": self.instance_details(inst)})
|
||||
|
||||
def instance_details(self, inst):
|
||||
def instance_details(self, inst): # pylint: disable-msg=R0201
|
||||
"Build the data structure to represent details for an instance."
|
||||
return {
|
||||
"id": inst.get("instance_id", None),
|
||||
"imageId": inst.get("image_id", None),
|
||||
@@ -224,11 +113,9 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint):
|
||||
"hostId": inst.get("node_name", None),
|
||||
"status": inst.get("state", "pending"),
|
||||
"addresses": {
|
||||
"public": [self.network.get_public_ip_for_instance(
|
||||
inst.get("instance_id", None)
|
||||
)],
|
||||
"private": [inst.get("private_dns_name", None)]
|
||||
},
|
||||
"public": [network.get_public_ip_for_instance(
|
||||
inst.get("instance_id", None))],
|
||||
"private": [inst.get("private_dns_name", None)]},
|
||||
|
||||
# implemented only by Rackspace, not AWS
|
||||
"name": inst.get("name", "Not-Specified"),
|
||||
@@ -237,11 +124,22 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint):
|
||||
"progress": "Not-Supported",
|
||||
"metadata": {
|
||||
"Server Label": "Not-Supported",
|
||||
"Image Version": "Not-Supported"
|
||||
}
|
||||
}
|
||||
"Image Version": "Not-Supported"}}
|
||||
|
||||
@webob.dec.wsgify
|
||||
def launch_server(self, req):
|
||||
"""Launch a new instance."""
|
||||
data = json.loads(req.body)
|
||||
inst = self.build_server_instance(data, req.environ['nova.context'])
|
||||
rpc.cast(
|
||||
FLAGS.compute_topic, {
|
||||
"method": "run_instance",
|
||||
"args": {"instance_id": inst.instance_id}})
|
||||
|
||||
return json.dumps({"server": self.instance_details(inst)})
|
||||
|
||||
def build_server_instance(self, env, context):
|
||||
"""Build instance data structure and save it to the data store."""
|
||||
reservation = utils.generate_uid('r')
|
||||
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
inst = self.instdir.new()
|
||||
@@ -253,45 +151,33 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint):
|
||||
inst['reservation_id'] = reservation
|
||||
inst['launch_time'] = ltime
|
||||
inst['mac_address'] = utils.generate_mac()
|
||||
address = network.allocate_ip(
|
||||
address = self.network.allocate_ip(
|
||||
inst['user_id'],
|
||||
inst['project_id'],
|
||||
mac=inst['mac_address']
|
||||
)
|
||||
mac=inst['mac_address'])
|
||||
inst['private_dns_name'] = str(address)
|
||||
inst['bridge_name'] = network.BridgedNetwork.get_network_for_project(
|
||||
inst['user_id'],
|
||||
inst['project_id'],
|
||||
'default' # security group
|
||||
)['bridge_name']
|
||||
'default')['bridge_name']
|
||||
# key_data, key_name, ami_launch_index
|
||||
# TODO(todd): key data or root password
|
||||
inst.save()
|
||||
return inst
|
||||
|
||||
def schedule_launch_of_instance(self, inst):
|
||||
rpc.cast(
|
||||
FLAGS.compute_topic,
|
||||
{
|
||||
"method": "run_instance",
|
||||
"args": {"instance_id": inst.instance_id}
|
||||
}
|
||||
)
|
||||
|
||||
def delete_server(self, instance_id):
|
||||
owner_hostname = self.host_for_instance(instance_id)
|
||||
# it isn't launched?
|
||||
@webob.dec.wsgify
|
||||
@wsgi.route_args
|
||||
def delete_server(self, req, route_args): # pylint: disable-msg=R0201
|
||||
"""Delete an instance."""
|
||||
owner_hostname = None
|
||||
instance = compute.Instance.lookup(route_args['server_id'])
|
||||
if instance:
|
||||
owner_hostname = instance["node_name"]
|
||||
if not owner_hostname:
|
||||
return None
|
||||
return webob.exc.HTTPNotFound("Did not find image, or it was "
|
||||
"not in a running state.")
|
||||
rpc_transport = "%s:%s" % (FLAGS.compute_topic, owner_hostname)
|
||||
rpc.cast(rpc_transport,
|
||||
{"method": "reboot_instance",
|
||||
"args": {"instance_id": instance_id}})
|
||||
return True
|
||||
|
||||
def host_for_instance(self, instance_id):
|
||||
instance = model.Instance.lookup(instance_id)
|
||||
if not instance:
|
||||
return None
|
||||
return instance["node_name"]
|
||||
|
||||
"args": {"instance_id": route_args['server_id']}})
|
||||
req.status = "202 Accepted"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
'''
|
||||
Utility methods for working with WSGI servers
|
||||
'''
|
||||
|
||||
class Util(object):
|
||||
|
||||
@staticmethod
|
||||
def route(reqstr, controllers):
|
||||
if len(reqstr) == 0:
|
||||
return Util.select_root_controller(controllers), []
|
||||
parts = [x for x in reqstr.split("/") if len(x) > 0]
|
||||
if len(parts) == 0:
|
||||
return Util.select_root_controller(controllers), []
|
||||
return controllers[parts[0]], parts[1:]
|
||||
|
||||
@staticmethod
|
||||
def select_root_controller(controllers):
|
||||
if '' in controllers:
|
||||
return controllers['']
|
||||
else:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user