nova分析(3)—— nova-api

nova-api是nova对外提供Restful API的服务,Horizon、novaclient等均通过该api与nova进行通信。

nova其实对外提供了多个api服务,包括下面这些服务:

nova-api
nova-api-ec2
nova-api-metadata
nova-api-os-compute
其中,nova-api用于启动其他三个服务。下面逐个分析下。

nova-api

入口在 nova.cmd.api:main ,主要是基于WSGI、PasteDeploy、Webob、Routes等框架实现Restful API:

    launcher = service.process_launcher()
    for api in CONF.enabled_apis:
        should_use_ssl = api in CONF.enabled_ssl_apis
        if api == 'ec2':
            server = service.WSGIService(api, use_ssl=should_use_ssl,
                                         max_url_len=16384)
        else:
            server = service.WSGIService(api, use_ssl=should_use_ssl)
        launcher.launch_service(server, workers=server.workers or 1)
    launcher.wait()

对每一个enabled_api,都会创建一个WSGIService,然后调用launch_service:

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(_LI('Forking too fast, sleeping'))
                time.sleep(1)

            wrap.forktimes.pop(0)

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

        pid = os.fork()
        if pid == 0:
            # create a green thread to run child
            # 实际上最终调用的service的start/wait方法
            launcher = self._child_process(wrap.service)
            while True:
                self._child_process_handle_signal()
                status, signo = self._child_wait_for_exit_or_signal(launcher)
                if not _is_sighup_and_daemon(signo):
                    break
                launcher.restart()

            os._exit(status)

        LOG.info(_LI('Started child %d'), pid)

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

        return pid

    def launch_service(self, service, workers=1):
        wrap = ServiceWrapper(service, workers)

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

既然每个服务都是 service.WSGIService ,那么WSGIService是如何初始化的呢

class WSGIService(object):
    """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 = name
        # 根据self.name从配置文件中获取manager,若有则实例化对应的manager
        self.manager = self._get_manager()
        self.loader = loader or wsgi.Loader()
        # 通过paste.deploy加载app,对应配置文件为api-paste.ini
        self.app = self.loader.load_app(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' % name, 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 = 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

即从api-paste.ini加载app后,创建wsgi.Server

Paste Deployment是用于发现和配置WSGI appliaction和server的系统。对于WSGI application,用户提供一个单独的函数(loadapp),用于从配置文件或者python egg中加载WSGI application。因为WSGI application提供了唯一的单独的简单的访问入口,所以application不需要暴露application的内部的实现细节。首先了解几个基本的概念:

  • application: 应用,符合WSGI规范的可调用对象,接受参数(environ,start_response), 调用start_response返回状态和消息头,返回结果作为消息体。

  • filter:过滤器,可调用对象,类型python中的装饰器,接受一个application对象作为参数,返回一个封装后的application。

  • app_factory:可调用对象,接受参数(global,**local_conf),返回application对象

  • composite_factory: 可调用对象,接受参数(loader,global_config,**local_conf), loader有几个方法, get_app用于获取wsgi_app, get_filter用于加载filter, 返回application对象。

  • filter_factory: 可调用对象,接受参数(global_config, **local_conf),返回filter对象

以nova-api-os-compute为例看看这个配置文件是咋回事

# 这儿是整个api的入口,将不同版本转发到compiste处理
[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v1.1: openstack_compute_api_v2
/v2: openstack_compute_api_v2
/v3: openstack_compute_api_v3

# 比如v3版本api在这儿处理,在使用keystone配置时,要经过request_id faultwrap sizelimit authtoken keystonecontext这些filter的处理最后再交给osapi_compute_app_v3这个app来处理
# 注意这些filter的顺序:先把前n-1个filter逆序,然后逐个应用到app上
[composite:openstack_compute_api_v3]
use = call:nova.api.auth:pipeline_factory_v3
noauth = request_id faultwrap sizelimit noauth_v3 osapi_compute_app_v3
keystone = request_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v3

# filter部分定义了这个filter由哪个类来处理
[filter:request_id]
paste.filter_factory = nova.openstack.common.middleware.request_id:RequestIdMiddleware.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:noauth]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory

[filter:noauth_v3]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddlewareV3.factory

[filter:ratelimit]
paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory

[filter:sizelimit]
paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory

# 这儿是v3的处理app,即APIRouterV3.factory
[app:osapi_compute_app_v3]
paste.app_factory = nova.api.openstack.compute:APIRouterV3.factory

 APIRouterV3会加载setup.cfg中配置的extensions:

        # 初始化api_extension_manager并加载所有的API_EXTENSION_NAMESPACE对应的extensions。
        # 这儿加载的就是setup.cfg中配置的nova.api.v3.extensions部分,比如:
        #   nova.api.v3.extensions =
        #       access_ips = nova.api.openstack.compute.plugins.v3.access_ips:AccessIPs
        #       admin_actions = nova.api.openstack.compute.plugins.v3.admin_actions:AdminActions
        #       admin_password = nova.api.openstack.compute.plugins.v3.admin_password:AdminPassword
        #       agents = nova.api.openstack.compute.plugins.v3.agents:Agents
        #       ...
        # 当然如果有第三方的应用也声明了此namespace的extension,这里也会一并load进来。
        # 其中的check_func为check_load_extension,其所作的工作主要包括:
        #   a) 检测是否为相V3APIExtensionBase类型
        #   b) extension的黑白名单过滤和验证
        self.api_extension_manager = stevedore.enabled.EnabledExtensionManager(
            namespace=self.API_EXTENSION_NAMESPACE, # 'nova.api.v3.extensions'
            check_func=_check_load_extension,
            invoke_on_load=True,
            invoke_kwds={"extension_info": self.loaded_extension_info})

        mapper = PlainMapper()
        self.resources = {}

        # NOTE(cyeoh) Core API support is rewritten as extensions
        # but conceptually still have core
        if list(self.api_extension_manager):
            # NOTE(cyeoh): Stevedore raises an exception if there are
            # no plugins detected. I wonder if this is a bug.
            # Extensions define what resources they want to add through a get_resources function
            self.api_extension_manager.map(self._register_resources,
                                           mapper=mapper)
            # Extensions define what resources they want to add through
            # a get_controller_extensions function
            self.api_extension_manager.map(self._register_controllers)

        # 检测core extensions是否全部加载成功
        missing_core_extensions = self.get_missing_core_extensions(
            self.loaded_extension_info.get_extensions().keys())
        if not self.init_only and missing_core_extensions:
            LOG.critical(_("Missing core API extensions: %s"),
                         missing_core_extensions)
            raise exception.CoreAPIMissing(
                missing_apis=missing_core_extensions)

以 servers = nova.api.openstack.compute.plugins.v3.servers:Servers 这个extension为例来看看加载过程是怎么样的。从前面的代码可以看到对每个extension,会调用get_resources和get_controller_extensions两个函数,其所作的工作分别为新增资源和扩展现有资源以添加action。

class Servers(extensions.V3APIExtensionBase):
    """Servers."""

    name = "Servers"
    alias = "servers"
    version = 1

    def get_resources(self):
        member_actions = {'action': 'POST'}
        collection_actions = {'detail': 'GET'}
        resources = [
            extensions.ResourceExtension(
                'servers',
                ServersController(extension_info=self.extension_info),
                member_name='server', collection_actions=collection_actions,
                member_actions=member_actions)]

        return resources

    def get_controller_extensions(self):
        return []

这里需要注意的是,V2 API中会区分Core API和Extension API。在V3 API中,所有的API均作为extension的形式提供。如V2 API中的servers是Core API,在V3 API中也是一个普通的extension,此extension会在get_resources方法中返回servers资源对象,而get_controller_extensions方法会返回空list。

而当disk_config等需要对servers资源扩展action时,其所需做的就是在get_controller_extensions中返回对servers资源的扩展:

class Disk_config(extensions.ExtensionDescriptor):
    """Disk Management Extension."""

    name = "DiskConfig"
    alias = ALIAS
    namespace = XMLNS_DCF
    updated = "2011-09-27T00:00:00Z"

    def get_controller_extensions(self):
        servers_extension = extensions.ControllerExtension(
                self, 'servers', ServerDiskConfigController())

        images_extension = extensions.ControllerExtension(
                self, 'images', ImageDiskConfigController())

        return [servers_extension, images_extension]

最后再来看看V3版本API是如何避免各Extension之间代码耦合的

V3中,主要是利用了Stevedore避免了各Extension之间的代码耦合,从而避免了上述问题。

还是以当前对核心资源“虚拟机”的create方法扩展来看,在servers资源对应的Controller nova.api.openstack.compute.pulgins.v3.servers:ServersController中,将create作为了一个extension的namespace,并会在初始化时就load所有的extensions:

# Look for implementation of extension point of server creation
        self.create_extension_manager = \
          stevedore.enabled.EnabledExtensionManager(
              namespace=self.EXTENSION_CREATE_NAMESPACE, #nova.api.v3.extensions.server.create
              check_func=_check_load_extension('server_create'),
              invoke_on_load=True,
              invoke_kwds={"extension_info": self.extension_info},
              propagate_map_exceptions=True)
        if not list(self.create_extension_manager):
            LOG.debug("Did not find any server create extensions")

然后在create方法中,会通过map方法依次调用各个Extensions的server_create方法:

        # Query extensions which want to manipulate the keyword
        # arguments.
        # NOTE(cyeoh): This is the hook that extensions use
        # to replace the extension specific code below.
        # When the extensions are ported this will also result
        # in some convenience function from this class being
        # moved to the extension
        if list(self.create_extension_manager):
            self.create_extension_manager.map(self._create_extension_point,
                                              server_dict, create_kwargs)


    def _create_extension_point(self, ext, server_dict, create_kwargs):
        handler = ext.obj
        LOG.debug("Running _create_extension_point for %s", ext.obj)

        handler.server_create(server_dict, create_kwargs)

在nova的setup.cfg中也配置了此namespace对应的所有的extensions,比如 nova.api.openstack.compute.plugins.v3.config_drive:ConfigDrive,其server_create方法如下所示,会更新ServersController:create方法中传入的create_kwargs字典:

    def server_create(self, server_dict, create_kwargs):
        create_kwargs['config_drive'] = server_dict.get(ATTRIBUTE_NAME)

最终,ServersController:create方法会以如下方式调用内部API来创建VM,即直接使用被各个extensions更新过的create_kwargs,而不用感知其实际的内容。

            (instances, resv_id) = self.compute_api.create(context,
                            inst_type,
                            image_uuid,
                            display_name=name,
                            display_description=name,
                            metadata=server_dict.get('metadata', {}),
                            admin_password=password,
                            requested_networks=requested_networks,
                            **create_kwargs)

由上面的分析可知,如上所有的所谓的Extension,最终均依赖的还是一个内部的API,如果此API本身不具有扩展性,那么如上所有的Extension,均只能在此内部API支持的功能的基础上进行发挥。 比如文中提到的创建VM的内部create API,此API的参数是固定的:

    def create(self, context, instance_type,
               image_href, kernel_id=None, ramdisk_id=None,
               min_count=None, max_count=None,
               display_name=None, display_description=None,
               key_name=None, key_data=None, security_group=None,
               availability_zone=None, user_data=None, metadata=None,
               injected_files=None, admin_password=None,
               block_device_mapping=None, access_ip_v4=None,
               access_ip_v6=None, requested_networks=None, config_drive=None,
               auto_disk_config=None, scheduler_hints=None, legacy_bdm=True):
        """Provision instances, sending instance information to the
        scheduler.  The scheduler will determine where the instance(s)
        go and will handle creating the DB entries.

        Returns a tuple of (instances, reservation_id)
        """

参考文档:

http://blog.csdn.net/cloudresearch/article/details/19050595

http://www.choudan.net/2013/07/30/OpenStack-API分析%28一%29.html

http://www.choudan.net/2013/07/31/OpenStack-API分析%28二%29.html

你可能感兴趣的:(nova)