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. | ||||
|  | ||||
|         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 | ||||
|  | ||||
|  | ||||
| class RackspaceAuthenticationApi(object): | ||||
|  | ||||
|     def process(self, path, env): | ||||
|         return self.index(path, env) | ||||
| class AuthenticationAPI(wsgi.Application): | ||||
|     """Handle all authorization requests through WSGI applications.""" | ||||
|  | ||||
|     @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 | ||||
|     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) | ||||
|         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 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
	 Eric Day
					Eric Day