您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
通过demo学习OpenStack开发--API服务(上)
 
作者:魏星 来源:刘陈泓 发布于 2016-2-16
   次浏览      
 

使用OpenStack服务的方式

OpenStack项目作为一个IaaS平台,提供了三种使用方式:

通过Web界面,也就是通过Dashboard(面板)来使用平台上的功能。

通过命令行,也就是通过keystone, nova, neutron等命令,或者通过最新的OpenStack命令来使用各个服务的功能(社区目前的发展目标是使用一个单一的OpenStack命令替代过去的每个项目一个命令的方式,以后会只存在一个OpenStack命令)。

通过API,也就是通过各个OpenStack项目提供的API来使用各个服务的功能。

上面提到的三种方式中,通过API这种方式是基础,是其他两种方式可行的基础。

通过Web界面使用OpenStack服务这种方式是通过OpenStack的Horizon项目提供的。Horizon项目是一个Django应用,实现了一个面板功能,包含了前后端的代码(除了Python,还包括了CSS和JS)。Horizon项目主要是提供一种交互界面,它会通过API来和各个OpenStack服务进行交互,然后在Web界面上展示各个服务的状态;它也会接收用户的操作,然后调用各个服务的API来完成用户对各个服务的使用。

通过命令行是用OpenStack服务的方式是由一系列项目来提供的,这些项目一般都命名为python-projectclient,比如python-keystoneclient,python-novaclietn等。这些命令行项目分别对应到各个主要的服务,为用户提供命令行操作界面和Python的SDK。比如python-keystoneclient对应到keystone,为用户提供了keystone这个命令,同时也提供了keyston项目的SDK(其实是在SDK的基础上实现了命令行)。这些client项目提供的SDK其实也是封装了对各自服务的API的调用。由于每个主要项目都有一个自己的命令行工具,社区觉得不好,于是又有了一个新的项目python-OpenStackclient,用来提供一个统一的命令行工具OpenStack(命令的名字就叫做OpenStack),这个工具实现了命令行,然后使用各个服务的client项目提供的SDK来完成对应的操作。

通过API使用OpenStack的方式是由各个服务自己实现的,比如负责计算的nova项目实现了计算相关的API,负责认证的keystone项目实现了认证和授权相关的API。这些API都是有统一的形式的,都是采用了HTTP协议实现的符合REST规范的API。OpenStack中如何实现这些API就是本文重点要将的内容。

基于HTTP协议的RESTful API

REST的全称是Representational State Transfer,中文翻译过来是表征状态转移,是Roy Fielding在他的博士论文Architectural Styles and the Design of Network-based Software Architectures提出的一种软件架构风格。可以先到wikipedia页面了解一下这个风格的特点。一般会把满足这种设计风格的API成为RESTful API。由于这种软件设计风格非常适合采用HTTP协议来实现,因此HTTP协议是目前实现RESTful API的主要方案。

OpenStack就是基于HTTP协议和JSON来实现自己的RESTful API(之前OpenStack还有采用XML来表示数据的,现在都已经转到JSON了)。当一个服务要提供API时,它就会启动一个HTTP服务端,用来对外提供RESTful API。

OpenStack的API都是有详细的文档记录的,可以查看所有的API文档。每个API的文档形式如下:

当然,你可以点开detail看到详细的说明。从上面这个API的文档来看,你会觉得这个和开发网站时使用的GET方法和POST方法差不多,实际上也是差不多的,只不过对HTTP协议的使用方法做了满足REST风格的规定而已。

Python如何实现RESTful API

因为Python能够进行Web开发,所以用来开发RESTful API也就不成问题,这两者的技术基础是一样。在Python下开发RESTful API应用,无非是解决两个问题:

1.服务如何部署?

2.用什么框架开发?

服务如何部署?

说到Python的Web服务部署这个问题,就不得不提到WSGI。目前Python有两种方式来开发和部署一个Web应用:用WSGI和不用WSGI。如果你不了解WSGI,那么你需要先看下另外这篇关于WSGI的文章:WSGI简介。

OpenStack的API服务都是使用WSGI的方式来部署的。在生产环境中部署WSGI,一般会考虑使用Web服务器 + 应用服务器 + 应用(框架)的方案。OpenStack官方推荐的是使用Apache + mod_wsgi的方案,不过这个要换成其他方案也很容易,你也可以选nginx + uWSGI。对于开发调试的目的,有些项目也会提供使用eventlet的单进程部署方案,比如Keystone项目的keystone-all命令。采用eventlet这种异步架构来进行应用开发也是一个比较大的话题,本文不覆盖这方面的内容。

当然,也可以不用WSGI。在Python中,如果不使用WSGI的化,一般开发者会选择一些专门的服务器和框架,比如Tornado,或者最新最潮的aiohttp。不过在OpenStack的项目中我还没见过不使用WSGI的。

用什么框架开发

Python的Web开发框架很多,最出名自然是Django了。基本上,还活跃的框架都支持RESTful API的开发,有些框架还专门为RESTful API的开发提供了便利的功能(比如Pecan),有些框架则通过第三方模块来提供这种便利,比如Django和Flask都有不少和REST相关的第三方库。

对于框架选择,也没有什么特别好的标准,一般都是比较性能、文档、社区是否活跃等。在我看来,选择流行的一般就不会错。

OpenStack中的RESTful API开发

上面已经谈到了OpenStack都是使用WSGI,也提到了部署方式。这一章来说一下OpenStack中使用的框架。

OpenStack项目倾向于不重新发明轮子,一般都会选择现有的库和框架来使用,除非现有的框架不满足需求。因为Web框架的选择很多,而且都满足需求,所以OpenStack项目到目前为止都是使用现成的Web框架。

OpenStack早期的项目并没有使用一个框架,而是使用了几个不同的模块来组合出一个框架:Paste + PasteDeploy + Routes + WebOb,这几个不同的模块分别负责应用的WSGI化、URL路由和请求处理等功能。Nova, Glance, Neutron, Keystone等早期的项目都是使用这样的架构来实现RESTful API的。

早期的这种技术选型带来的好处是”框架”具备足够的灵活性,缺点则是要把这几个模块组合起来实现一个REST服务,需要写很多代码,连WSGI的入口函数都要自己实现(比如Keystone项目的keystone/common/wsgi.py文件中的class Application)。因为灵活性的好处不是很明显,而代码量大的坏处很明显,比如上面那个class Application需要在每个项目中复制一遍,所以社区的新项目就开始使用新的Web框架Pecan。

Pecan是一个基于对象路由的框架,即灵活又简单。Pecan主要实现了URL路由功能,支持RESTful API。Pecan没有实现模板、session管理和ORM等功能,但是这些功能可以通过其他的模块来实现。对于OpenStack来说,Pecan是一个很好的选择,因为OpenStack项目中统一使用sqlalchemy来实现ORM,API的实现也不需要模板功能,安全控制则基于Keystone体系。使用Pecan来开发REST服务,代码量很少,代码结构也清晰。Ceilometer项目就是使用了Pecan。

本文会重点讲解OpenStack中使用的API开发框架的使用。但是本文的目的并不是覆盖这些框架的使用细节,而是通过说明重要的部分,降低初学者的入门的门槛。框架的使用细节都可以从文档中找到。说明一下,除非特殊说明,本文中的相对路径都是相对于项目源码目录的相对路径。

Paste + PasteDeploy + Routes + WebOb

我们在API服务(1)中已经提到了,这个框架只在早期开始的项目中使用,新的项目都已经转到Pecan框架了。但是,早期的项目都是比较核心的项目,因此我们还是要学会如何使用这个框架。我们会以Keystone项目为例,来说明如何阅读使用这个框架的开发的API代码。

重点在于确定URL路由

RESTful API程序的主要特点就是URL path会和功能对应起来。这点从API文档就可以看得出来,比如用户管理的功能一般都放在/user这个路径下。因此,看一个RESTful API程序,一般都是看它实现了哪些URL path,以及每个path对应了什么功能,这个一般都是由框架的URL路由功能负责的。所以,熟悉一个RESTful API程序的重点在于确定URL路由。本章所说的这个框架对于初学者的难点也是如何确定URL路由。

WSGI入口和中间件作为基础知识,你需要先了解一下WSGI的相关概念,可以参考这篇文章WSGI简介。

WSGI入口

在API服务(1)中提到了WSGI可以使用Apache进行部署,也可以使用eventlet进行部署。Keystone项目同时提供了这两种方案的代码,也就是我们要找的WSGI的入口。

Keystone项目在httpd/目录下,存放了可以用于Apache服务器部署WSGI服务的文件。其中,wsgi-keystone.conf是一个mod_wsgi的示例配置文件,keystone.py则是WSGI应用程序的入口文件。httpd/keystone.py也就是我们要找的入口文件之一。这个文件的内容很简单:

import os
from keystone.server import wsgi as wsgi_server
name = os.path.basename(__file__)
application = wsgi_server.initialize_application(name)

文件中创建了WSGI入口需要使用的application对象。

keystone-all命令则是采用eventlet来进行部署时的入口,可以从setup.cfg文件按中确定keystone-all命令的入口:

[entry_points]
console_scripts =
keystone-all = keystone.cmd.all:main
keystone-manage = keystone.cmd.manage:main

从setup.cfg文件的entry_points部分可以看出,keystone-all的入口是keystone/cmd/all.py文件中的main()函数,这个函数的内容也很简单:

def main():
eventlet_server.run(possible_topdir)

main()函数的主要作用就是启动一个eventlet_server,配置文件从possible_topdir中查找。因为eventlet的部署方式涉及到eventlet库的使用方法,本文不再展开说明。读者可以在学会确定URL路由后再回来看这个代码。下面,继续以httpd/keystone.py文件作为入口来说明如何阅读代码。

Paste和PasteDeploy

httpd/keystone.py中调用的initialize_application(name)函数载入了整个WSGI应用,这里主要用到了Paste和PasteDeploy库。

def initialize_application(name):
...
def loadapp():
return keystone_service.loadapp(
'config:%s' % config.find_paste_config(), name)

_unused, application = common.setup_backends(
startup_application_fn=loadapp)
return application

上面是删掉无关代码后的initialize_application()函数。config.find_paste_config()用来查找PasteDeploy需要用到的WSGI配置文件,这个文件在源码中是etc/keystone-paste.ini文件,如果在线上环境中,一般是/etc/keystone-paste.init。keystone_service.loadapp()函数内部则调用了paste.deploy.loadapp()函数来加载WSGI应用,如何加载则使用了刚才提到的keystone-paste.ini文件,这个文件也是看懂整个程序的关键。

name很关键

在上面的代码中我们可以看到,name这个变量从httpd/keystone.py文件传递到initialize_application()函数,又被传递到keystone_service.loadapp()函数,最终被传递到paste.deploy.loadapp()函数。那么,这个name变量到底起什么作用呢?先把这个问题放在一边,我们后面再来解决它。

paste.ini

使用Paste和PasteDeploy模块来实现WSGI服务时,都需要一个paste.ini文件。这个文件也是Paste框架的精髓,这里需要重点说明一下这个文件如何阅读。

paste.ini文件的格式类似于INI格式,每个section的格式为[type:name]。这里重要的是理解几种不同type的section的作用。

composite: 这种section用于将HTTP请求分发到指定的app。

app: 这种section表示具体的app。

filter: 实现一个过滤器中间件。

pipeline: 用来把把一系列的filter串起来。

上面这些section是在keystone的paste.ini中用到的,下面详细介绍一下如何使用。这里需要用到WSGIMiddleware(WSGI中间件)的知识,可以在WSGI简介这篇文章中找到。

section composite

这种section用来决定如何分发HTTP请求。Keystone的paste.ini文件中有两个composite的section:

[composite:main]
use = egg:Paste#urlmap
/v2.0 = public_api
/v3 = api_v3
/ = public_version_api

[composite:admin]
use = egg:Paste#urlmap
/v2.0 = admin_api
/v3 = api_v3
/ = admin_version_api

在composite seciont中,use是一个关键字,指定处理请求的代码。egg:Paste#urlmap表示到Paste模块的egg-info中去查找urlmap关键字所对应的函数。在virtualenv环境下,是文件/lib/python2.7/site-packages/Paste-2.0.2.dist-info/metadata.json:

{
...
"extensions": {
...
"python.exports": {
"paste.composite_factory": {
"cascade": "paste.cascade:make_cascade",
"urlmap": "paste.urlmap:urlmap_factory"
},
...
}

在这个文件中,你可以找到urlmap对应的是paste.urlmap:urlmap_factory,也就是paste/urlmap.py文件中的urlmap_factory()函数。
composite section中其他的关键字则是urlmap_factory()函数的参数,用于表示不同的URL path前缀。urlmap_factory()函数会返回一个WSGI app,其功能是根据不同的URL path前缀,把请求路由给不同的app。以[composite:main]为例:

[composite:main]
use = egg:Paste#urlmap
/v2.0 = public_api # /v2.0 开头的请求会路由给public_api处理
/v3 = api_v3 # /v3 开头的请求会路由个api_v3处理
/ = public_version_api # / 开头的请求会路由给public_version_api处理

路由的对象其实就是paste.ini中其他secion的名字,类型必须是app或者pipeline。

section pipeline

pipeline是把filter和app串起来的一种section。它只有一个关键字就是pipeline。我们以api_v3这个pipeline为例:

[pipeline:api_v3]
# The last item in this pipeline must be service_v3 or an equivalent
# application. It cannot be a filter.
pipeline = sizelimit url_normalize request_id build_auth_context
token_auth admin_token_auth json_body ec2_extension_v3 s3_extension
simple_cert_extension revoke_extension federation_extension
oauth1_extension endpoint_filter_extension endpoint_policy_extension service_v3

pipeline关键字指定了很多个名字,这些名字也是paste.ini文件中其他section的名字。请求会从最前面的section开始处理,一直向后传递。pipeline指定的section有如下要求:

最后一个名字对应的section一定要是一个app。

非最后一个名字对应的section一定要是一个filter。

section filter

filter是用来过滤请求和响应的,以WSGI中间件的方式实现。

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

这个是api_v3这个pipeline指定的第一个filter,作用是限制请求的大小。其中的paste.filter_factory表示调用哪个函数来获得这个filter中间件。

section app

app表示实现主要功能的应用,是一个标准的WSGI application。

[app:service_v3]
paste.app_factory = keystone.service:v3_app_factory

paste.app_factory表示调用哪个函数来获得这个app。

总结一下

paste.ini中这一大堆配置的作用就是把我们用Python写的WSGI application和middleware串起来,规定好HTTP请求处理的路径。
name是用来确定入口的

上面我们提到了一个问题,就是name变量的作用到底是什么?name变量表示paste.ini中一个section的名字,指定这个section作为HTTP请求处理的第一站。在Keystone的paste.ini中,请求必须先由[composite:main]或者[composite:admin]处理,所以在keystone项目中,name的值必须是main或者admin。

上面提到的httpd/keystone.py文件中,name等于文件名的basename,所以实际部署中,必须把keystone.py重命名为main.py或者admin.py。

举个例子

一般情况下,从Keystone服务获取一个token时,会使用下面这个API:

POST http://hostname:35357/v3/auth/tokens

我们根据Keystone的paste.ini来说明这个API是如何被处理的:

hostname:35357 这一部分是由Web服务器处理的,比如Apache。然后,请求会被转到WSGI的入口,也就是httpd/keystone.py中的application对象取处理。

application对象是根据paste.ini中的配置来处理的。这里会先由[composite:admin]来处理(一般是admin监听35357端口,main监听5000端口)。

[composite:admin]发现请求的path是/v3开头的,于是就把请求转发给[pipeline:api_v3]去处理,转发之前,会把/v3这个部分去掉。

[pipeline:api_v3]收到请求,path是/auth/tokens,然后开始调用各个filter来处理请求。最后会把请求交给[app:service_v3]进行处理。

[app:service_v3]收到请求,path是/auth/tokens,然后交给最终的WSGI app去处理。

下一步

到此为止,paste.ini中的配置的所有工作都已经做完了。下面请求就要转移到最终的app内部去处理了。前面已经说过了,我们的重点是确定URL路由,那么现在还有一部分的path的路由还没确定,/auth/tokens,这个还需要下一步的工作。

中间件的实现

上面我们提到paste.ini中用到了许多的WSGI中间件,那么这些中间件是如何实现的呢?我们来看一个例子就知道了。

[filter:build_auth_context]
paste.filter_factory = keystone.middleware:AuthContextMiddleware.factory

build_auth_context这个中间件的作用是在WSGI的environ中添加KEYSTONE_AUTH_CONTEXT这个键,包含的内容是认证信息的上下文。实现这个中间件的类继承关系如下:

keystone.middleware.core.AuthContextMiddleware
-> keystone.common.wsgi.Middleware
-> keystone.common.wsgi.Application
-> keystone.common.wsgi.BaseApplication

这里实现的关键主要在前面两个类中。

keystone.common.wsgi.Middleware类实现了__call__()方法,这个就是WSGI中application端被调用时运行的方法。

class Middleware(Application):
...
@webob.dec.wsgify()
def __call__(self, request):
try:
response = self.process_request(request)
if response:
return response
response = request.get_response(self.application)
return self.process_response(request, response)
except exceptin.Error as e:
...
...

__call__()方法实现为接收一个request对象,返回一个response对象的形式,然后使用WebOB模块的装饰器webob.dec.wsgify()将它变成标准的WSGI application接口。这里的request和response对象分别是webob.Request和webob.Response。这里,__call__()方法内部调用了self.process_request()方法,这个方法在keystone.middleware.core.AuthContextMiddleware中实现:

class AuthContextMiddleware(wsgi.Middleware):
...
def process_request(self, request):
...
request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context

这个函数会根据功能设计创建auth_context,然后赋值给`request.environ[‘KEYSTONE_AUTH_CONTEXT]“,这样就能通过WSGI application方法的environ传递到下一个WSGI application中去了。

最后的Application

上面我们已经看到了,对于/v3开头的请求,在paste.ini中会被路由到[app:service_v3]这个section,会交给keystone.service:v3_app_factory这个函数生成的application处理。最后这个application需要根据URL path中剩下的部分,/auth/tokens,来实现URL路由。从这里开始,就需要用到Routes模块了。

同样由于篇幅限制,我们只能展示Routes模块的大概用法。Routes模块是用Python实现的类似Rails的URL路由系统,它的主要功能就是把path映射到对应的动作。

Routes模块的一般用法是创建一个Mapper对象,然后调用该对象的connect()方法把path和method映射到一个controller的某个action上,这里controller是一个自定义的类实例,action是表示controller对象的方法的字符串。一般调用的时候还会指定映射哪些方法,比如GET或者POST之类的。

举个例子,来看下keystone/auth/routers.py的代码:

class Routers(wsgi.RoutersBase):
 def append_v3_routers(self, mapper, routers):
auth_controller = controllers.Auth() self._add_resource(
mapper, auth_controller,
path='/auth/tokens',
get_action='validate_token',
head_action='check_token',
post_action='authenticate_for_token',
delete_action='revoke_token',
rel=json_home.build_v3_resource_relation('auth_tokens')) ...

这里调用了自己Keystone自己封装的_add_resource()方法批量为一个/auth/tokens这个path添加多个方法的处理函数。其中,controller是一个controllers.Auth实例,也就是 keystone.auth.controllers.Auth。其他的参数,我们从名称可以猜出其作用是指定对应方法的处理函数,比如get_action用于指定GET方法的处理函数为validate_token。我们再深入一下,看下_add_resource()这个方法的实现:

 def _add_resource(self, mapper, controller, path, rel,
get_action=None, head_action=None, get_head_action=None,
put_action=None, post_action=None, patch_action=None,
delete_action=None, get_post_action=None,
path_vars=None, status=json_home.Status.STABLE):
...
if get_action:
getattr(controller, get_action) # ensure the attribute exists
mapper.connect(path, controller=controller, action=get_action,
conditions=dict(method=['GET']))

这个函数其实很简单,就是调用mapper对象的connect方法指定一个path的某些method的处理函数。

Keystone项目的代码结构

Keystone项目把每个功能都分到单独的目录下,比如token相关的功能是放在keystone/token/目录下,assignment相关的功能是放在keystone/assignment/目录下。目录下都一般会有三个文件:routers.py, controllers.py, core.py。routers.py中实现了URL路由,把URL和controllers.py中的action对应起来;controllers.py中的action调用core.py中的底层接口实现RESTful API承诺的功能。所以,我们要进一步确定URL路由是如何做的,就要看routers.py文件。

注意,这个只是Keystone项目的结构,其他项目即使用了同样的框架,也不一定是这么做的。

Keystone中的路由汇总

每个模块都定义了自己的路由,但是这些路由最终要还是要通过一个WSGI application来调用的。上面已经提到了,在Keystone中,/v3开头的请求最终都会交给keystone.service.v3_app_factory这个函数生成的application来处理。这个函数里也包含了路由最后分发的秘密,我们来看代码:

def v3_app_factory(global_conf, **local_conf):
...
mapper = routes.Mapper()
...

router_modules = [auth,
assignment,
catalog,
credential,
identity,
policy,
resource]
...

for module in router_modules:
routers_instance = module.routers.Routers()
_routers.append(routers_instance)
routers_instance.append_v3_routers(mapper, sub_routers)

# Add in the v3 version api
sub_routers.append(routers.VersionV3('public', _routers))
return wsgi.ComposingRouter(mapper, sub_routers)

v3_app_factory()函数中先遍历了所有的模块,将每个模块的路由都添加到同一个mapper对象中,然后把mapper对象作为参数用于初始化wsgi.ComposingRouter对象,所以我们可以判断,这个wsgi.ComposingRouter对象一定是一个WSGI application,我们看看代码就知道了:

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

def __init__(self, mapper):
self.map = mapper
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
self.map)

@webob.dec.wsgify()
def __call__(self, req):
return self._router

...

class ComposingRouter(Router):
def __init__(self, mapper=None, routers=None):
...

上述代码证实了我们的猜测。这个ComposingRouter对象被调用时(在其父类Router中实现),会返回一个WSGI application。这个application中则使用了routes模块的中间件来实现了请求路由,在routes.middleware.RoutesMiddleware中实现。这里对path进行路由的结果就是返回各个模块的controllers.py中定义的controller。各个模块的controller都是一个WSGI application,这个你可以通过这些controller的类继承关系看出来。

但是这里只讲到了,routes模块把path映射到了一个controller,但是如何把对path的处理映射到controller的方法呢?这个可以从controller的父类keystone.common.wsgi.Application的实现看出来。这个Application类中使用了environ['wsgiorg.routing_args']中的数据来确定调用controller的哪个方法,这些数据是由上面提到的routes.middleware.RoutesMiddleware设置的。

总结到这里我们大概把Paste + PasteDeploy + Routes + WebOb这个框架的流程讲了一遍,从本文的长度你就可以看出这个框架有多啰嗦,用起来有多麻烦。下一篇文章我们会讲Pecan框架,我们的demo也将会使用Pecan框架来开发。

   
次浏览       
 
相关文章

云计算的架构
对云计算服务模型
云计算核心技术剖析
了解云计算的漏洞
 
相关文档

云计算简介
云计算简介与云安全
下一代网络计算--云计算
软浅析云计算
 
相关课程

云计算原理与应用
云计算应用与开发
CMMI体系与实践
基于CMMI标准的软件质量保证
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件的思考
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS
更多...   
相关培训课程

云计算原理与应用
Windows Azure 云计算应用

摩托罗拉 云平台的构建与应用
通用公司GE Docker原理与实践
某研发中心 Openstack实践
知名电子公司 云平台架构与应用
某电力行业 基于云平台构建云服务
云计算与Windows Azure培训
北京 云计算原理与应用