OpenStack nova-api服务启动

Posted by Jobin on June 14, 2018

/usr/bin/nova-api的启动代码:

import sys
from nova.cmd.api import main

if __name__ == "__main__":
	sys.exit(main())

接着调用/nova/cmd/api.py里的main函数,代码如下:

def main():
    config.parse_args(sys.argv)
    logging.setup(CONF, "nova")
    utils.monkey_patch()
    objects.register_all()
    gmr_opts.set_defaults(CONF)
    if 'osapi_compute' in CONF.enabled_apis:
        # NOTE(mriedem): This is needed for caching the nova-compute service
        # version.
        objects.Service.enable_min_version_cache()
    log = logging.getLogger(__name__)

    gmr.TextGuruMeditation.setup_autorun(version, conf=CONF)

    # launcher为oslo_service.service.ProcessLauncher对象实例
    launcher = service.process_launcher()
    started = 0
    for api in CONF.enabled_apis:
        should_use_ssl = api in CONF.enabled_ssl_apis
        try:
        	# server变量为nova.service.WSGIService对象实例
        	# api为osapi_compute,在/etc/nova/nova.conf中配置
            server = service.WSGIService(api, use_ssl=should_use_ssl)
            # launcher调用launch_service函数,并传入server参数
            launcher.launch_service(server, workers=server.workers or 1)
            started += 1
        except exception.PasteAppNotFound as ex:
            log.warning("%s. ``enabled_apis`` includes bad values. "
                        "Fix to remove this warning.", ex)

    if started == 0:
        log.error('No APIs were started. '
                  'Check the enabled_apis config option.')
        sys.exit(1)

    launcher.wait()

/nova/service.py,主要有self.app变量和self.server变量

class WSGIService(service.Service):
    """Provides ability to launch API from a 'paste' configuration."""

    def __init__(self, name, loader=None, use_ssl=False, max_url_len=None):
        """Initialize, but do not start the WSGI server.

        :param name: The name of the WSGI server given to the loader.
        :param loader: Loads the WSGI application using the given name.
        :returns: None

        """
        # self.name = osapi_compute
        self.name = name
        # NOTE(danms): Name can be metadata, osapi_compute, or ec2, per
        # nova.service's enabled_apis
        self.binary = 'nova-%s' % name
        self.topic = None
        self.manager = self._get_manager()
        self.loader = loader or wsgi.Loader()
        # self.app = nova.api.openstack.urlmap.URLMap
        self.app = self.loader.load_app(name)
        # inherit all compute_api worker counts from osapi_compute
        if name.startswith('openstack_compute_api'):
            wname = 'osapi_compute'
        else:
            wname = name
        self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0")
        self.port = getattr(CONF, '%s_listen_port' % name, 0)
        self.workers = (getattr(CONF, '%s_workers' % wname, None) or
                        processutils.get_worker_count())
        if self.workers and self.workers < 1:
            worker_name = '%s_workers' % name
            msg = (_("%(worker_name)s value of %(workers)s is invalid, "
                     "must be greater than 0") %
                   {'worker_name': worker_name,
                    'workers': str(self.workers)})
            raise exception.InvalidInput(msg)
        self.use_ssl = use_ssl
        # self.server = nova.wsgi.Server
        self.server = wsgi.Server(name,
                                  self.app,
                                  host=self.host,
                                  port=self.port,
                                  use_ssl=self.use_ssl,
                                  max_url_len=max_url_len)
        # Pull back actual port used
        self.port = self.server.port
        self.backdoor_port = None
        setup_profiler(name, self.host)

先来分析ProcessLauncher类的launch_service函数,launch_service主要调用了_start_child函数,后者又调用了 _child_process函数。

    def _start_child(self, wrap):
        if len(wrap.forktimes) > wrap.workers:
            # Limit ourselves to one process a second (over the period of
            # number of workers * 1 second). This will allow workers to
            # start up quickly but ensure we don't fork off children that
            # die instantly too quickly.
            if time.time() - wrap.forktimes[0] < wrap.workers:
                LOG.info('Forking too fast, sleeping')
                time.sleep(1)

            wrap.forktimes.pop(0)

        wrap.forktimes.append(time.time())

        pid = os.fork()
        if pid == 0:
            self.launcher = self._child_process(wrap.service)
            while True:
                self._child_process_handle_signal()
                status, signo = self._child_wait_for_exit_or_signal(
                    self.launcher)
                if not _is_sighup_and_daemon(signo):
                    self.launcher.wait()
                    break
                self.launcher.restart()

            os._exit(status)

        LOG.debug('Started child %d', pid)

        wrap.children.add(pid)
        self.children[pid] = wrap

        return pid

    def launch_service(self, service, workers=1):
        """Launch a service with a given number of workers.

       :param service: a service to launch, must be an instance of
              :class:`oslo_service.service.ServiceBase`
       :param workers: a number of processes in which a service
              will be running
        """
        _check_service_base(service)
        wrap = ServiceWrapper(service, workers)

        # Hide existing objects from the garbage collector, so that most
        # existing pages will remain in shared memory rather than being
        # duplicated between subprocesses in the GC mark-and-sweep. (Requires
        # Python 3.7 or later.)
        if hasattr(gc, 'freeze'):
            gc.freeze()

        LOG.info('Starting %d workers', wrap.workers)
        while self.running and len(wrap.children) < wrap.workers:
            self._start_child(wrap)

_child_process函数代码如下:其中launcher为oslo_service.service.Launcher对象,并调用该对象的launch_service方法,同时传递一个service参数,该参数实际为nova.service.WSGIService对象,也就是main函数中的server变量。

    def _child_process(self, service):
        self._child_process_handle_signal()

        # Reopen the eventlet hub to make sure we don't share an epoll
        # fd with parent and/or siblings, which would be bad
        eventlet.hubs.use_hub()

        # Close write to ensure only parent has it open
        os.close(self.writepipe)
        # Create greenthread to watch for parent to close pipe
        eventlet.spawn_n(self._pipe_watcher)

        # Reseed random number generator
        random.seed()

        # launcher为oslo_service.service.Launcher对象
        launcher = Launcher(self.conf, restart_method=self.restart_method)
        # service为nova.service.WSGIService对象
        launcher.launch_service(service)
        return launcher

launch_service函数实际调用了self.services.add(service),services实际为oslo_service.service.Services对象

    def launch_service(self, service, workers=1):
        """Load and start the given service.

        :param service: The service you would like to start, must be an
                        instance of :class:`oslo_service.service.ServiceBase`
        :param workers: This param makes this method compatible with
                        ProcessLauncher.launch_service. It must be None, 1 or
                        omitted.
        :returns: None

        """
        if workers is not None and workers != 1:
            raise ValueError(_("Launcher asked to start multiple workers"))
        _check_service_base(service)
        service.backdoor_port = self.backdoor_port
        self.services.add(service)

接下来看add函数,调用tg的add_thread函数,传递一个self.run_service静态方法,一个WSGIService对象和一个event.Event()对象。Services.run_service函数调用nova.service.WSGIService对象service的start()函数启动wsgi服务。

class Services(object):

    def __init__(self):
        # 保存各个WSGIService对象
        self.services = []
        self.tg = threadgroup.ThreadGroup()
        self.done = event.Event()

    def add(self, service):
        """Add a service to a list and create a thread to run it.

        :param service: service to run
        """
        self.services.append(service)
        self.tg.add_thread(self.run_service, service, self.done)

    def stop(self):
        """Wait for graceful shutdown of services and kill the threads."""
        for service in self.services:
            service.stop()

        # Each service has performed cleanup, now signal that the run_service
        # wrapper threads can now die:
        if not self.done.ready():
            self.done.send()

        # reap threads:
        self.tg.stop()

    def wait(self):
        """Wait for services to shut down."""
        for service in self.services:
            service.wait()
        self.tg.wait()

    def restart(self):
        """Reset services and start them in new threads."""
        self.stop()
        self.done = event.Event()
        for restart_service in self.services:
            restart_service.reset()
            self.tg.add_thread(self.run_service, restart_service, self.done)

    @staticmethod
    def run_service(service, done):
        """Service start wrapper.

        :param service: service to run
        :param done: event to wait on until a shutdown is triggered
        :returns: None

        """
        try:
            service.start()
        except Exception:
            LOG.exception('Error starting thread.')
            raise SystemExit(1)
        else:
            done.wait()

nova.service.WSGIService.start()

    def start(self):
        """Start serving this service using loaded configuration.

        Also, retrieve updated port number in case '0' was passed in, which
        indicates a random port should be used.

        :returns: None

        """
        ctxt = context.get_admin_context()
        service_ref = objects.Service.get_by_host_and_binary(ctxt, self.host,
                                                             self.binary)
        if service_ref:
            _update_service_ref(service_ref)
        else:
            try:
                service_ref = _create_service_ref(self, ctxt)
            except (exception.ServiceTopicExists,
                    exception.ServiceBinaryExists):
                # NOTE(danms): If we race to create a record wth a sibling,
                # don't fail here.
                service_ref = objects.Service.get_by_host_and_binary(
                    ctxt, self.host, self.binary)

        if self.manager:
            self.manager.init_host()
            self.manager.pre_start_hook()
            if self.backdoor_port is not None:
                self.manager.backdoor_port = self.backdoor_port
        self.server.start()
        if self.manager:
            self.manager.post_start_hook()

nova.wsgi.Server.start(),我们看到utils.spawn(wsgi_kwargs)函数被调用,实际调用了eventlet.spawn,传递了wsgi_kwargs字典,func为eventlet.wsgi.server,site为self.app,self.app为nova.service.WSGIService._init_构造函数通过self.loader.load_app(name)得到的,其中name值为osapi_compute。至此,一个作为nova-api服务的WSGI应用服务已经起来了,并且运行在一个greenthread中,将接受用户的各种请求。

    def start(self):
        """Start serving a WSGI application.

        :returns: None
        """
        # The server socket object will be closed after server exits,
        # but the underlying file descriptor will remain open, and will
        # give bad file descriptor error. So duplicating the socket object,
        # to keep file descriptor usable.

        dup_socket = self._socket.dup()
        dup_socket.setsockopt(socket.SOL_SOCKET,
                              socket.SO_REUSEADDR, 1)
        # sockets can hang around forever without keepalive
        dup_socket.setsockopt(socket.SOL_SOCKET,
                              socket.SO_KEEPALIVE, 1)

        # This option isn't available in the OS X version of eventlet
        if hasattr(socket, 'TCP_KEEPIDLE'):
            dup_socket.setsockopt(socket.IPPROTO_TCP,
                                  socket.TCP_KEEPIDLE,
                                  CONF.wsgi.tcp_keepidle)

        if self._use_ssl:
            try:
                ca_file = CONF.wsgi.ssl_ca_file
                cert_file = CONF.wsgi.ssl_cert_file
                key_file = CONF.wsgi.ssl_key_file

                if cert_file and not os.path.exists(cert_file):
                    raise RuntimeError(
                          _("Unable to find cert_file : %s") % cert_file)

                if ca_file and not os.path.exists(ca_file):
                    raise RuntimeError(
                          _("Unable to find ca_file : %s") % ca_file)

                if key_file and not os.path.exists(key_file):
                    raise RuntimeError(
                          _("Unable to find key_file : %s") % key_file)

                if self._use_ssl and (not cert_file or not key_file):
                    raise RuntimeError(
                          _("When running server in SSL mode, you must "
                            "specify both a cert_file and key_file "
                            "option value in your configuration file"))
                ssl_kwargs = {
                    'server_side': True,
                    'certfile': cert_file,
                    'keyfile': key_file,
                    'cert_reqs': ssl.CERT_NONE,
                }

                if CONF.wsgi.ssl_ca_file:
                    ssl_kwargs['ca_certs'] = ca_file
                    ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED

                dup_socket = eventlet.wrap_ssl(dup_socket,
                                               **ssl_kwargs)
            except Exception:
                with excutils.save_and_reraise_exception():
                    LOG.error(_LE("Failed to start %(name)s on %(host)s"
                                  ":%(port)s with SSL support"),
                              {'name': self.name, 'host': self.host,
                               'port': self.port})

        wsgi_kwargs = {
            'func': eventlet.wsgi.server,
            'sock': dup_socket,
            'site': self.app,
            'protocol': self._protocol,
            'custom_pool': self._pool,
            'log': self._logger,
            'log_format': CONF.wsgi.wsgi_log_format,
            'debug': False,
            'keepalive': CONF.wsgi.keep_alive,
            'socket_timeout': self.client_socket_timeout
            }

        if self._max_url_len:
            wsgi_kwargs['url_length_limit'] = self._max_url_len

        self._server = utils.spawn(**wsgi_kwargs)

Nova-api实现的是wsgi的Application,而不是Server,Server是由wsgi库来实现的,就是func:eventlet.wsgi.server,Application如何被调用呢?此时可以想到eventlet.spawn被调用时传入的那个site值,它是在nova.service.WSGIService.__init__构造函数中通过self.loader.load_app(name)得到的,name为“osapi_compute”,self.loader为wsgi.Loader实例,load_app调用了nova.wsgi.Loader.load_app()中的deploy.loadapp(),传入参数为:config:/etc/nova/api-paste.ini和name=osapi_compute。

/nova/service.py

class WSGIService(service.Service):
    """Provides ability to launch API from a 'paste' configuration."""

    def __init__(self, name, loader=None, use_ssl=False, max_url_len=None):
        """Initialize, but do not start the WSGI server.

        :param name: The name of the WSGI server given to the loader.
        :param loader: Loads the WSGI application using the given name.
        :returns: None

        """
        # self.name = osapi_compute
        self.name = name
        # NOTE(danms): Name can be metadata, osapi_compute, or ec2, per
        # nova.service's enabled_apis
        self.binary = 'nova-%s' % name
        self.topic = None
        self.manager = self._get_manager()
        self.loader = loader or wsgi.Loader()
        # self.app = nova.api.openstack.urlmap.URLMap
        self.app = self.loader.load_app(name)
        # inherit all compute_api worker counts from osapi_compute
        if name.startswith('openstack_compute_api'):
            wname = 'osapi_compute'
        else:
            wname = name
        self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0")
        self.port = getattr(CONF, '%s_listen_port' % name, 0)
        self.workers = (getattr(CONF, '%s_workers' % wname, None) or
                        processutils.get_worker_count())
        if self.workers and self.workers < 1:
            worker_name = '%s_workers' % name
            msg = (_("%(worker_name)s value of %(workers)s is invalid, "
                     "must be greater than 0") %
                   {'worker_name': worker_name,
                    'workers': str(self.workers)})
            raise exception.InvalidInput(msg)
        self.use_ssl = use_ssl
        # self.server = nova.wsgi.Server
        self.server = wsgi.Server(name,
                                  self.app,
                                  host=self.host,
                                  port=self.port,
                                  use_ssl=self.use_ssl,
                                  max_url_len=max_url_len)
        # Pull back actual port used
        self.port = self.server.port
        self.backdoor_port = None
        setup_profiler(name, self.host)

/nova/wsgi.py

class Loader(object):
    """Used to load WSGI applications from paste configurations."""

    def __init__(self, config_path=None):
        """Initialize the loader, and attempt to find the config.

        :param config_path: Full or relative path to the paste config.
        :returns: None

        """
        self.config_path = None

        config_path = config_path or CONF.wsgi.api_paste_config
        if not os.path.isabs(config_path):
            self.config_path = CONF.find_file(config_path)
        elif os.path.exists(config_path):
            self.config_path = config_path

        if not self.config_path:
            raise exception.ConfigNotFound(path=config_path)

    def load_app(self, name):
        """Return the paste URLMap wrapped WSGI application.

        :param name: Name of the application to load.
        :returns: Paste URLMap object wrapping the requested application.
        :raises: `nova.exception.PasteAppNotFound`

        """
        try:
            LOG.debug("Loading app %(name)s from %(path)s",
                      {'name': name, 'path': self.config_path})
            return deploy.loadapp("config:%s" % self.config_path, name=name)
        except LookupError:
            LOG.exception(_LE("Couldn't lookup app: %s"), name)
            raise exception.PasteAppNotFound(name=name, path=self.config_path)

再看下deploy.loadapp都调用了哪些函数,从下面pdb调试调试代码中,可以看到函数接收的三个参数,分别为loader,global_conf和local_conf。

(Pdb) l 148,163
148
149
150  	def urlmap_factory(loader, global_conf, **local_conf):
151  	    if 'not_found_app' in local_conf:
152  	        not_found_app = local_conf.pop('not_found_app')
153  	    else:
154  	        not_found_app = global_conf.get('not_found_app')
155  	    if not_found_app:
156  	        not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
157  	    urlmap = URLMap(not_found_app=not_found_app)
158  	    for path, app_name in local_conf.items():
159  	        path = paste.urlmap.parse_path_expression(path)
160  	        app = loader.get_app(app_name, global_conf=global_conf)
161  	        urlmap[path] = app
162  	    import pdb
163  	    pdb.set_trace()
(Pdb) p loader
<paste.deploy.loadwsgi.ConfigLoader object at 0x7f5fe4d71f50>
(Pdb) p local_conf
{'/v2': 'openstack_compute_api_v21_legacy_v2_compatible', '/v2.1': 'openstack_compute_api_v21', '/': 'oscomputeversions'}
(Pdb) p urlmap
<nova.api.openstack.urlmap.URLMap object at 0x7f5fe4d80650>
(Pdb) p global_conf
{'__file__': '/etc/nova/api-paste.ini', 'here': '/etc/nova'}

进一步分析/etc/nova/api-paste.ini,如下所示:

############
# Metadata #
############
[composite:metadata]
use = egg:Paste#urlmap
/: meta

[pipeline:meta]
pipeline = cors metaapp

[app:metaapp]
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory

#############
# OpenStack #
#############

[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
# v21 is an exactly feature match for v2, except it has more stringent
# input validation on the wsgi surface (prevents fuzzing early on the
# API). It also provides new features via API microversions which are
# opt into for clients. Unaware clients will receive the same frozen
# v2 API feature set, but with some relaxed validation
/v2: openstack_compute_api_v21_legacy_v2_compatible
/v2.1: openstack_compute_api_v21

[composite:openstack_compute_api_v21]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler noauth2 osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21

[composite:openstack_compute_api_v21_legacy_v2_compatible]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21

[filter:request_log]
paste.filter_factory = nova.api.openstack.requestlog:RequestLog.factory

[filter:compute_req_id]
paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory

[filter:faultwrap]
paste.filter_factory = nova.api.openstack:FaultWrapper.factory

[filter:noauth2]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory

[filter:osprofiler]
paste.filter_factory = nova.profiler:WsgiMiddleware.factory

[filter:sizelimit]
paste.filter_factory = oslo_middleware:RequestBodySizeLimiter.factory

[filter:http_proxy_to_wsgi]
paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory

[filter:legacy_v2_compatible]
paste.filter_factory = nova.api.openstack:LegacyV2CompatibleWrapper.factory

[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory

[pipeline:oscomputeversions]
pipeline = cors faultwrap request_log http_proxy_to_wsgi oscomputeversionapp

[app:oscomputeversionapp]
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory

##########
# Shared #
##########

[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = nova

[filter:keystonecontext]
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory

[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

根据deploy.loadapp传入的osapi_compute可以找到[composite:osapi_compute],依次找到urlmap_factory函数。下面以请求路径/v2为例子进行说明,其对应的app为openstack_compute_api_v21_legacy_v2_compatible,又依次找到osapi_compute_app_v21,最后找到了nova.api.openstack.compute:APIRouterV21.factory,调用该函数获取一个app,这一系列的调用关系是在nova.api.openstack.urlmap.urlmap_factory函数里面通过loader.get_app()调用实现的,如下所示,这里的loader的类型为paste.deploy.loadwsgi.ConfigLoader。

/nova/api/openstack/urlmap.py

def urlmap_factory(loader, global_conf, **local_conf):
    if 'not_found_app' in local_conf:
        not_found_app = local_conf.pop('not_found_app')
    else:
        not_found_app = global_conf.get('not_found_app')
    if not_found_app:
        not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
    urlmap = URLMap(not_found_app=not_found_app)
    for path, app_name in local_conf.items():
        path = paste.urlmap.parse_path_expression(path)
        app = loader.get_app(app_name, global_conf=global_conf)
        urlmap[path] = app
    return urlmap

我们来看一下nova.api.openstack.compute:APIRouterV21这个类,

/nova/api/openstack/compute/routes.py

class APIRouterV21(base_wsgi.Router):
    """Routes requests on the OpenStack API to the appropriate controller
    and method. The URL mapping based on the plain list `ROUTE_LIST` is built
    at here.
    """
    def __init__(self, custom_routes=None):
        """:param custom_routes: the additional routes can be added by this
               parameter. This parameter is used to test on some fake routes
               primarily.
        """
        super(APIRouterV21, self).__init__(nova.api.openstack.ProjectMapper())

        if custom_routes is None:
            custom_routes = tuple()

        for path, methods in ROUTE_LIST + custom_routes:
            # NOTE(alex_xu): The variable 'methods' is a dict in normal, since
            # the dict includes all the methods supported in the path. But
            # if the variable 'method' is a string, it means a redirection.
            # For example, the request to the '' will be redirect to the '/' in
            # the Nova API. To indicate that, using the target path instead of
            # a dict. The route entry just writes as "('', '/)".
            if isinstance(methods, str):
                self.map.redirect(path, methods)
                continue

            for method, controller_info in methods.items():
                # TODO(alex_xu): In the end, I want to create single controller
                # instance instead of create controller instance for each
                # route.
                controller = controller_info[0]()
                action = controller_info[1]
                self.map.create_route(path, method, controller, action)

    @classmethod
    def factory(cls, global_config, **local_config):
        """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
        return cls()

nova.api.openstack.compute.routes.py:APIRouterV21类中有个self.map变量,为nova.api.openstack.__init__.py.ProjectMapper()类实例,该map变量作为参数传递给APIRouterV21的基类nova.wsgi.Router,下面看下该基类:

class Router(object):
    """WSGI middleware that maps incoming requests to WSGI apps."""

    def __init__(self, mapper):
        """Create a router for the given routes.Mapper.

        Each route in `mapper` must specify a 'controller', which is a
        WSGI app to call.  You'll probably want to specify an 'action' as
        well and have your controller be an object that can route
        the request to the action-specific method.

        Examples:
          mapper = routes.Mapper()
          sc = ServerController()

          # Explicit mapping of one route to a controller+action
          mapper.connect(None, '/svrlist', controller=sc, action='list')

          # Actions are all implicitly defined
          mapper.resource('server', 'servers', controller=sc)

          # Pointing to an arbitrary WSGI app.  You can specify the
          # {path_info:.*} parameter so the target app can be handed just that
          # section of the URL.
          mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp())

        """
        self.map = mapper
        self._router = routes.middleware.RoutesMiddleware(self._dispatch,
                                                          self.map)

    @webob.dec.wsgify(RequestClass=Request)
    def __call__(self, req):
        """Route the incoming request to a controller based on self.map.

        If no match, return a 404.

        """
        return self._router

    @staticmethod
    @webob.dec.wsgify(RequestClass=Request)
    def _dispatch(req):
        """Dispatch the request to the appropriate controller.

        Called by self._router after matching the incoming request to a route
        and putting the information into req.environ.  Either returns 404
        or the routed WSGI app's response.

        """
        match = req.environ['wsgiorg.routing_args'][1]
        if not match:
            return webob.exc.HTTPNotFound()
        app = match['controller']
        return app

在基类的____init__()函数中,调用了routes.middleware.RoutesMiddleware()函数,这个map当做参数传递,同时作为参数的还有wsgi.Router的静态函数_dispatch(),返回值赋值给self._router变量。nova.wsgi.Router类有个____call__函数,当wsgi请求到来时,webob会调用到这个函数,函数将变量self._router变量返回。_dispatch函数将被调用,这个函数返回一个对应的app,这个app类型为nova.api.openstack.wsgi.Resource,_dispatch函数被调用时,通过pdb可以跟踪其局部变量的类型和值,下面以请求获取云主机detail方法为例说明,match变量是一个字典,action值为“detail”,controller值为nova.api.openstack.wsgi.Resource对象,还有一个project_id。

# 首先pdb打断点,执行/usr/bin/nova-api开启api服务监听8774端口
# 其次执行查询云主机详细信息请求:
curl -i 'http://192.168.158.10:8774/v2.1/1e12b1b23b7c4f2ba59a36d8484726ea/servers/detail' -H "X-Auth-Token:b3b139fa551d4fc2b537ecfe929b2e0b"
> /opt/stack/nova/nova/wsgi.py(461)_dispatch()
-> if not match:
(Pdb) l
456
457  	        """
458  	        import pdb
459  	        match = req.environ['wsgiorg.routing_args'][1]
460  	        pdb.set_trace()
461  ->	        if not match:
462  	            return webob.exc.HTTPNotFound()
463  	        app = match['controller']
464  	        return app
465
466
(Pdb) p match
{'action': u'detail', 'controller': <nova.api.openstack.wsgi.Resource object at 0x7faed6d02150>, 'project_id': u'1e12b1b23b7c4f2ba59a36d8484726ea'}
(Pdb) n
> /opt/stack/nova/nova/wsgi.py(463)_dispatch()
-> app = match['controller']
(Pdb) n
> /opt/stack/nova/nova/wsgi.py(464)_dispatch()
-> return app
(Pdb) p app
<nova.api.openstack.wsgi.Resource object at 0x7faed6d02150>
(Pdb)

作为一个wsgi的app,nova.api.openstack.wsgi.Resource类实现了__call__方法,该方法接着会被调用,如下所示:Resource的__call__函数调用_process_stack函数,后者又调用dispatch函数,dispatch函数调用了被当做参数传进来的method函数,这个函数就是nova.api.openstack.compute.servers.ServersController.detail函数。

DEBUG nova.api.openstack.wsgi [None req-280c619c-050a-4a9d-adea-a51cede2e312 admin admin] Calling method '<bound method ServersController.detail of <nova.api.openstack.compute.servers.ServersController object at 0x7f679488a910>>' 
> /opt/stack/nova/nova/api/openstack/wsgi.py(727)dispatch()
-> return method(req=request, **action_args)
(Pdb) l
722  	        """Dispatch a call to the action-specific method."""
723
724  	        try:
725  	            import pdb
726  	            pdb.set_trace()
727  ->	            return method(req=request, **action_args)
728  	        except exception.VersionNotFoundForAPIMethod:
729  	            # We deliberately don't return any message information
730  	            # about the exception to the user so it looks as if
731  	            # the method is simply not implemented.
732  	            return Fault(webob.exc.HTTPNotFound())
(Pdb) p method
<bound method ServersController.detail of <nova.api.openstack.compute.servers.ServersController object at 0x7f679488a910>>
(Pdb) p request
<Request at 0x7f6794486c90 GET http://192.168.158.10:8774/v2.1/1e12b1b23b7c4f2ba59a36d8484726ea/servers/detail>
(Pdb)

/nova/api/openstack/wsgi.py

class Resource(wsgi.Application):
    """WSGI app that handles (de)serialization and controller dispatch.

    WSGI app that reads routing information supplied by RoutesMiddleware
    and calls the requested action method upon its controller.  All
    controller action methods must accept a 'req' argument, which is the
    incoming wsgi.Request. If the operation is a PUT or POST, the controller
    method must also accept a 'body' argument (the deserialized request body).
    They may raise a webob.exc exception or return a dict, which will be
    serialized by requested content type.

    Exceptions derived from webob.exc.HTTPException will be automatically
    wrapped in Fault() to provide API friendly error responses.

    """
    ......
    @webob.dec.wsgify(RequestClass=Request)
    def __call__(self, request):
        """WSGI method that controls (de)serialization and method dispatch."""

        if self.support_api_request_version:
            # Set the version of the API requested based on the header
            try:
                request.set_api_version_request()
            except exception.InvalidAPIVersionString as e:
                return Fault(webob.exc.HTTPBadRequest(
                    explanation=e.format_message()))
            except exception.InvalidGlobalAPIVersion as e:
                return Fault(webob.exc.HTTPNotAcceptable(
                    explanation=e.format_message()))

        # Identify the action, its arguments, and the requested
        # content type
        action_args = self.get_action_args(request.environ)
        action = action_args.pop('action', None)

        # NOTE(sdague): we filter out InvalidContentTypes early so we
        # know everything is good from here on out.
        try:
            content_type, body = self.get_body(request)
            accept = request.best_match_content_type()
        except exception.InvalidContentType:
            msg = _("Unsupported Content-Type")
            return Fault(webob.exc.HTTPUnsupportedMediaType(explanation=msg))

        # NOTE(Vek): Splitting the function up this way allows for
        #            auditing by external tools that wrap the existing
        #            function.  If we try to audit __call__(), we can
        #            run into troubles due to the @webob.dec.wsgify()
        #            decorator.
        return self._process_stack(request, action, action_args,
                               content_type, body, accept)

    def _process_stack(self, request, action, action_args,
                       content_type, body, accept):
        """Implement the processing stack."""

        # Get the implementing method
        try:
            meth, extensions = self.get_method(request, action,
                                               content_type, body)
        except (AttributeError, TypeError):
            return Fault(webob.exc.HTTPNotFound())
        except KeyError as ex:
            msg = _("There is no such action: %s") % ex.args[0]
            return Fault(webob.exc.HTTPBadRequest(explanation=msg))
        except exception.MalformedRequestBody:
            msg = _("Malformed request body")
            return Fault(webob.exc.HTTPBadRequest(explanation=msg))

        if body:
            msg = _("Action: '%(action)s', calling method: %(meth)s, body: "
                    "%(body)s") % {'action': action,
                                   'body': six.text_type(body, 'utf-8'),
                                   'meth': str(meth)}
            LOG.debug(strutils.mask_password(msg))
        else:
            LOG.debug("Calling method '%(meth)s'",
                      {'meth': str(meth)})

        # Now, deserialize the request body...
        try:
            contents = self._get_request_content(body, request)
        except exception.MalformedRequestBody:
            msg = _("Malformed request body")
            return Fault(webob.exc.HTTPBadRequest(explanation=msg))

        # Update the action args
        action_args.update(contents)

        project_id = action_args.pop("project_id", None)
        context = request.environ.get('nova.context')
        if (context and project_id and (project_id != context.project_id)):
            msg = _("Malformed request URL: URL's project_id '%(project_id)s'"
                    " doesn't match Context's project_id"
                    " '%(context_project_id)s'") % \
                    {'project_id': project_id,
                     'context_project_id': context.project_id}
            return Fault(webob.exc.HTTPBadRequest(explanation=msg))

        response = None
        try:
            with ResourceExceptionHandler():
                action_result = self.dispatch(meth, request, action_args)
        except Fault as ex:
            response = ex

        if not response:
            # No exceptions; convert action_result into a
            # ResponseObject
            resp_obj = None
            if type(action_result) is dict or action_result is None:
                resp_obj = ResponseObject(action_result)
            elif isinstance(action_result, ResponseObject):
                resp_obj = action_result
            else:
                response = action_result

            # Run post-processing extensions
            if resp_obj:
                # Do a preserialize to set up the response object
                if hasattr(meth, 'wsgi_code'):
                    resp_obj._default_code = meth.wsgi_code
                # Process extensions
                response = self.process_extensions(extensions, resp_obj,
                                                        request, action_args)

            if resp_obj and not response:
                response = resp_obj.serialize(request, accept)

        if hasattr(response, 'headers'):
            for hdr, val in list(response.headers.items()):
                if six.PY2:
                    # In Py2.X Headers must be byte strings
                    response.headers[hdr] = utils.utf8(val)
                else:
                    # In Py3.X Headers must be utf-8 strings
                    response.headers[hdr] = encodeutils.safe_decode(
                            utils.utf8(val))

            if not request.api_version_request.is_null():
                response.headers[API_VERSION_REQUEST_HEADER] = \
                    'compute ' + request.api_version_request.get_string()
                response.headers[LEGACY_API_VERSION_REQUEST_HEADER] = \
                    request.api_version_request.get_string()
                response.headers.add('Vary', API_VERSION_REQUEST_HEADER)
                response.headers.add('Vary', LEGACY_API_VERSION_REQUEST_HEADER)

        return response
    ......
        def _get_method(self, request, action, content_type, body):
        """Look up the action-specific method and its extensions."""
        # Look up the method
        try:
            if not self.controller:
                meth = getattr(self, action)
            else:
                meth = getattr(self.controller, action)
        except AttributeError:
            if (not self.wsgi_actions or
                    action not in _ROUTES_METHODS + ['action']):
                # Propagate the error
                raise
        else:
            return meth, self.wsgi_extensions.get(action, [])

        if action == 'action':
            action_name = action_peek(body)
        else:
            action_name = action

        # Look up the action method
        return (self.wsgi_actions[action_name],
                self.wsgi_action_extensions.get(action_name, []))

    def dispatch(self, method, request, action_args):
        """Dispatch a call to the action-specific method."""

        try:
            return method(req=request, **action_args)
        except exception.VersionNotFoundForAPIMethod:
            # We deliberately don't return any message information
            # about the exception to the user so it looks as if
            # the method is simply not implemented.
            return Fault(webob.exc.HTTPNotFound())

nova.api.openstack.compute.server.ServersController

class ServersController(wsgi.Controller):
    """The Server API base controller class for the OpenStack API."""
    ......
    def __init__(self, **kwargs):

        super(ServersController, self).__init__(**kwargs)
        self.compute_api = compute.API()

        # TODO(alex_xu): The final goal is that merging all of
        # extended json-schema into server main json-schema.
        self._create_schema(self.schema_server_create_v257, '2.57')
        self._create_schema(self.schema_server_create_v252, '2.52')
        self._create_schema(self.schema_server_create_v242, '2.42')
        self._create_schema(self.schema_server_create_v237, '2.37')
        self._create_schema(self.schema_server_create_v232, '2.32')
        self._create_schema(self.schema_server_create_v219, '2.19')
        self._create_schema(self.schema_server_create, '2.1')
        self._create_schema(self.schema_server_create_v20, '2.0')
    ......
    @wsgi.expected_errors((400, 403))
    @validation.query_schema(schema_servers.query_params_v226, '2.26')
    @validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25')
    def detail(self, req):
        """Returns a list of server details for a given user."""
        context = req.environ['nova.context']
        context.can(server_policies.SERVERS % 'detail')
        try:
            servers = self._get_servers(req, is_detail=True)
        except exception.Invalid as err:
            raise exc.HTTPBadRequest(explanation=err.format_message())
        return servers
    ......

nova.api.openstack.compute.servers.ServersController的detail函数是怎么找到的呢?在Resource类的_process_stack函数中,调用dispatch函数之前,调用了get_method函数,get_method函数又调用了_get_method函数,_get_method函数中有meth = getattr(self.controller, action),这里的self.controller就是Resource类实例中聚合nova.api.openstack.compute.servers.ServersController对象,action就是detail函数,getattr返回的meth就是一个函数<bound method ServersController.detail of <nova.api.openstack.compute.servers.ServersController object at 0x7fa29e7967d0»,_process_stack再将这个meth作为参数传递给dispatch,在diapatch中真正调用了nova.api.openstack.compute.servers.ServersController的detail函数。

我们再看下nova.api.openstack.compute.servers.ServersController的__init__构造函数。在构造函数中有一行代码:self.compute_api = compute.API(),compute.API()通过调用importutils.import_object返回一个类对象,这个类对象的类型为nova.compute.api.API。

> /opt/stack/nova/nova/compute/__init__.py(41)API()
-> return importutils.import_object(class_name, *args, **kwargs)
(Pdb) l
 36
 37  	def API(*args, **kwargs):
 38  	    class_name = _get_compute_api_class_name()
 39  	    import pdb
 40  	    pdb.set_trace()
 41  ->	    return importutils.import_object(class_name, *args, **kwargs)
 42
 43
 44  	def HostAPI(*args, **kwargs):
 45  	    """Returns the 'HostAPI' class from the same module as the configured
 46  	    compute api
(Pdb) p class_name
'nova.compute.api.API'
(Pdb)

nova.compute.api.API类如下所示:nova.compute.api.API类聚合了很多对象,可以得知,nova.api.openstack.compute.servers.ServersController类的很多操作都通过nova.compute.api.API来完成。

> /opt/stack/nova/nova/compute/api.py(273)__init__()->None
-> pdb.set_trace()
(Pdb) l 245,275
245  	@profiler.trace_cls("compute_api")
246  	class API(base.Base):
247  	    """API for interacting with the compute manager."""
248
249  	    def __init__(self, image_api=None, network_api=None, volume_api=None,
250  	                 security_group_api=None, **kwargs):
251  	        self.image_api = image_api or image.API()
252  	        self.network_api = network_api or network.API()
253  	        self.volume_api = volume_api or cinder.API()
254  	        # NOTE(mriedem): This looks a bit weird but we get the reportclient
255  	        # via SchedulerClient since it lazy-loads SchedulerReportClient on
256  	        # the first usage which helps to avoid a bunch of lockutils spam in
257  	        # the nova-api logs every time the service is restarted (remember
258  	        # that pretty much all of the REST API controllers construct this
259  	        # API class).
260  	        self.placementclient = scheduler_client.SchedulerClient().reportclient
261  	        self.security_group_api = (security_group_api or
262  	            openstack_driver.get_openstack_security_group_driver())
263  	        self.consoleauth_rpcapi = consoleauth_rpcapi.ConsoleAuthAPI()
264  	        self.compute_rpcapi = compute_rpcapi.ComputeAPI()
265  	        self.compute_task_api = conductor.ComputeTaskAPI()
266  	        self.servicegroup_api = servicegroup.API()
267  	        self.notifier = rpc.get_notifier('compute', CONF.host)
268  	        if CONF.ephemeral_storage_encryption.enabled:
269  	            self.key_manager = key_manager.API()
270
271  	        super(API, self).__init__(**kwargs)
272  	        import pdb
273  ->	        pdb.set_trace()
274
275  	    @property
(Pdb) p self.image_api
<nova.image.api.API object at 0x7f9c7613d590>
(Pdb) p self.network_api
<nova.network.neutronv2.api.API object at 0x7f9c7613d5d0>
(Pdb) p self.volume_api
<nova.volume.cinder.API object at 0x7f9c760984d0>
(Pdb) p self.security_group_api
<nova.network.security_group.neutron_driver.SecurityGroupAPI object at 0x7f9c76098190>
(Pdb) p self.compute_rpcapi
<nova.compute.rpcapi.ComputeAPI object at 0x7f9c7612efd0>
(Pdb) p self.servicegroup_api
<nova.servicegroup.api.API object at 0x7f9c76098590>
(Pdb) p self.notifier
<nova.rpc.LegacyValidatingNotifier object at 0x7f9c76098e90>
(Pdb)