本文主要分析Nova OS API的服务流程。
模块nova/cmd/api_os_compute.py是该API服务的启动脚本,在main方法中,通过调用WSGIService(‘osapi_compute’, use_ssl=should_use_ssl)创建了一个WSGIService对象,该对象负责osapi_compute服务的初始化、启动、终止等。并通过调用service.serve(server, workers=server.workers)启动了这项服务。
def main():
config.parse_args(sys.argv)
logging.setup("nova")
utils.monkey_patch()
objects.register_all()
gmr.TextGuruMeditation.setup_autorun(version)
should_use_ssl = 'osapi_compute' in CONF.enabled_ssl_apis
server = service.WSGIService('osapi_compute', use_ssl=should_use_ssl)
service.serve(server, workers=server.workers)
service.wait()
在WSGIService的__init__方法中,self.name = name指定了WSGI服务的名字,self.loader = loader or wsgi.Loader()则创建了用于加载WSGI应用的装载器,随后通过调用self.loader.loadapp(name)对WSGI应用进行了加载,而self.host、self.port则指定了该服务监听的主机号和端口——具体的配置信息写在了service_opts列表中,最后self.server = wsgi.Server(name, self.app, …)创建了一个wsgi.Server对象,可见WSGIService类是对wsgi.Service类的又一次封装。
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.manager = self._get_manager()
self.loader = loader or wsgi.Loader()
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)
接下来,我们来分析WSGI应用的加载过程。在nova/wsgi.py模块中,可以看到Loader类的定义,它的__init__方法用于确定配置文件的路径,该配置文件是一个paste.deploy库规定的配置文件,它与WSGI应用的加载密切相关。在wsgi_opts列表中,可以看到该配置文件的文件名为api-paste.ini,可以确定它的的路径为/etc/nova/api-paste.ini。
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.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
Loader类的load_app方法进一步通过调用deploy.loadapp方法来加载应用,这个方法有两个参数,一个是配置文件的路径,另一个是需要加载的应用的名字。
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 as err:
LOG.error(err)
raise exception.PasteAppNotFound(name=name, path=self.config_path)
paste/deploy/loadwsgi.py模块中的loadapp方法进一步调用了loadobj方法。
def loadapp(uri, name=None, **kw):
return loadobj(APP, uri, name=name, **kw)
同样的,loadobj方法调用了loadcontext方法。
def loadobj(object_type, uri, name=None, relative_to=None,
global_conf=None):
context = loadcontext(
object_type, uri, name=name, relative_to=relative_to,
global_conf=global_conf)
return context.create()
前面提过,deploy.loadapp的第一个参数是配置文件api-paste.ini的路径,即/etc/nova/api-paste.ini,与loadcontext方法的uri参数对应,这里uri = config: /etc/nova/api-paste.ini。第二个参数是将要加载的应用的名字,即osapi_compute,与loadcontext方法的name参数对应。通过调用url.split(‘:’, 1)方法,获得scheme和path的值,分别为config、/etc/nova/api-paste.ini。在最后的return语句中,_loader为一个字典对象,_loader[‘config’]的值为_loadconfig方法,所以最后返回的对象由_loadconfig方法确定。
def loadcontext(object_type, uri, name=None, relative_to=None,
global_conf=None):
if '#' in uri:
if name is None:
uri, name = uri.split('#', 1)
else:
# @@: Ignore fragment or error?
uri = uri.split('#', 1)[0]
if name is None:
name = 'main'
if ':' not in uri:
raise LookupError("URI has no scheme: %r" % uri)
scheme, path = uri.split(':', 1)
scheme = scheme.lower()
if scheme not in _loaders:
raise LookupError(
"URI scheme not known: %r (from %s)"
% (scheme, ', '.join(_loaders.keys())))
return _loaders[scheme](
object_type,
uri, path, name=name, relative_to=relative_to,
global_conf=global_conf)
在_loadconfig方法中,首先判断path是否是绝对路径,显然是它一个绝对路径。而loader变量是一个ConfigLoader类对象,最后通过调用loader.get_context方法来获得方法的返回值。
def _loadconfig(object_type, uri, path, name, relative_to,
global_conf):
isabs = os.path.isabs(path)
# De-Windowsify the paths:
path = path.replace('\\', '/')
if not isabs:
if not relative_to:
raise ValueError(
"Cannot resolve relative uri %r; no relative_to keyword "
"argument given" % uri)
relative_to = relative_to.replace('\\', '/')
if relative_to.endswith('/'):
path = relative_to + path
else:
path = relative_to + '/' + path
if path.startswith('///'):
path = path[2:]
path = unquote(path)
loader = ConfigLoader(path)
if global_conf:
loader.update_defaults(global_conf, overwrite=False)
return loader.get_context(object_type, name, global_conf)
在ConfigLoader类的__init__方法中,创建了一个NicerConfigParser对象,并赋值给self.parser。之后,根据路径打开配置文件,通过这个self.parser对象对配置文件进行了读取。根据名字,我们可推测出,这个parser对象用于配置文件的解析。
def __init__(self, filename):
self.filename = filename = filename.strip()
defaults = {
'here': os.path.dirname(os.path.abspath(filename)),
'__file__': os.path.abspath(filename)
}
self.parser = NicerConfigParser(filename, defaults=defaults)
self.parser.optionxform = str # Don't lower-case keys
with open(filename) as f:
self.parser.read_file(f)
在ConfigLoader的get_context方法中,首先调用absolute_name方法对参数name进行判断,name即为我们需要加载的应用的名字——osapi_compute。具体来说,absolute_name方法将判断传入的name参数是否符合”^[a-zA-Z]+:”正则式,若符合则返回True,否则返回False。所以代码中的条件判断为False,随后调用self.find_config_section方法获得可配置文件的section。
def get_context(self, object_type, name=None, global_conf=None):
if self.absolute_name(name):
return loadcontext(object_type, name,
relative_to=os.path.dirname(self.filename),
global_conf=global_conf)
section = self.find_config_section(
object_type, name=name)
...
至此,本文还未涉及过api-paste.ini文件的具体内容,在下面粘贴了该文件的部分片段。配置文件由一个个的section组成,section定义了应用的创建方式,例如use=call:nova.api.openstack.urlmap:urlmap_factory规定,需要调用nova/api/openstack/urlmap.py模块中的urlmap_factory方法来创建osapi_compute应用。同时section还可以定义路径与应用的映射关系,如/v2: openstack_compute_api_v2,它表示http请求的url若以/v2开,则都交由openstack_compute_api_v2应用处理。
关于paste.deploy配置文件的定义,读者可以参考官方文档,本文不做详细描述。
[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v1.1: openstack_compute_api_v2
/v2: openstack_compute_api_v2
/v2.1: openstack_compute_api_v21
/v3: openstack_compute_api_v3
[composite:openstack_compute_api_v2]
use = call:nova.api.auth:pipeline_factory
noauth = compute_req_id faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
keystone = compute_req_id faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
keystone_nolimit = compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2