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

1元 10元 50元





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



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

上一篇文章我们了解了一个巨啰嗦的框架:Paste + PasteDeploy + Routes + WebOb。后来,OpenStack社区的人受不了这么啰嗦的代码了,决定换一个框架,他们最终选中了Pecan。Pecan框架相比上一篇文章的啰嗦框架有如下好处:

1.不用自己写WSGI application了

2.请求路由很容易就可以实现了

总的来说,用上Pecan框架以后,很多重复的代码不用写了,开发人员可以专注于业务,也就是实现每个API的功能。

Pecan

Pecan框架的目标是实现一个采用对象分发方式进行URL路由的轻量级Web框架。它非常专注于自己的目标,它的大部分功能都和URL路由以及请求和响应的处理相关,而不去实现模板、安全以及数据库层,这些东西都可以通过其他的库来实现。关于Pecan的更多信息,可以查看文档。本文以,OpenStack的magnum项目为例来说明Pecan项目在实际中的应用,但是本文不会详细讲解Pecan的各个方面,一些细节请读者阅读Pecan的文档。

项目中的代码结构

使用Pecan框架时,,OpenStack项目一般会把API服务的实现都放在一个api目录下,比如magnum项目是这样的:

? ~/openstack/env/p/magnum git:(master) $ tree magnum/api
magnum/api
├── app.py
├── auth.py
├── config.py
├── controllers
│ ├── base.py
│ ├── __init__.py
│ ├── link.py
│ ├── root.py
│ └── v1
│ ├── base.py
│ ├── baymodel.py
│ ├── bay.py
│ ├── certificate.py
│ ├── collection.py
│ ├── container.py
│ ├── __init__.py
│ ├── magnum_services.py
│ ├── node.py
│ ├── pod.py
│ ├── replicationcontroller.py
│ ├── service.py
│ ├── types.py
│ ├── utils.py
│ └── x509keypair.py
├── expose.py
├── hooks.py
├── __init__.py
├── middleware
│ ├── auth_token.py
│ ├── __init__.py
│ └── parsable_error.py
├── servicegroup.py
└── validation.py

你也可以在Ceilometer项目中看到类似的结构。介绍一下几个主要的文件,这样你以后看到一个使用Pecan的,OpenStack项目时就会比较容易找到入口。

app.py 一般包含了Pecan应用的入口,包含应用初始化代码

config.py 包含Pecan的应用配置,会被app.py使用

controllers/ 这个目录会包含所有的控制器,也就是API具体逻辑的地方

controllers/root.py 这个包含根路径对应的控制器

controllers/v1/ 这个目录对应v1版本的API的控制器。如果有多个版本的API,你一般能看到v2等目录。

代码变少了:application的配置

Pecan的配置很容易,通过一个Python源码式的配置文件就可以完成基本的配置。这个配置的主要目的是指定应用程序的root,然后用于生成WSGI application。我们来看Magnum项目的例子。Magnum项目有个API服务是用Pecan实现的,在magnum/api/config.py文件中可以找到这个文件,主要内容如下:

app = {
'root': 'magnum.api.controllers.root.RootController',
'modules': ['magnum.api'],
'debug': False,
'hooks': [
hooks.ContextHook(),
hooks.RPCHook(),
hooks.NoExceptionTracebackHook(),
],
'acl_public_routes': [
'/'
],
}

上面这个app对象就是Pecan的配置,每个Pecan应用都需要有这么一个名为app的配置。app配置中最主要的就是root的值,这个值表示了应用程序的入口,也就是从哪个地方开始解析HTTP的根path:/。hooks对应的配置是一些Pecan的hook,作用类似于WSGI Middleware。

有了app配置后,就可以让Pecan生成一个WSGI application。在Magnum项目中,magnum/api/app.py文件就是生成WSGI application的地方,我们来看一下这个的主要内容:

def get_pecan_config():
# Set up the pecan configuration
filename = api_config.__file__.replace('.pyc', '.py')
return pecan.configuration.conf_from_file(filename)


def setup_app(config=None):
if not config:
config = get_pecan_config()

app_conf = dict(config.app)

app = pecan.make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
wrap_app=middleware.ParsableErrorMiddleware,
**app_conf
)

return auth.install(app, CONF, config.app.acl_public_routes)

get_pecan_config()方法读取我们上面提到的config.py文件,然后返回一个pecan.configuration.Config对象。setup_app()函数首先调用get_pecan_config()函数获取application的配置,然后调用pecan.make_app()函数创建了一个WSGI application,最后调用了auth.install()函数(也就是magnum.api.auth.install()函数)为刚刚生成的WSGI application加上Keystone的认证中间件(确保所有的请求都会通过Keystone认证)。

到这边为止,一个Pecan的WSGI application就已经准备好了,只要调用这个setup_app()函数就能获得。至于如何部署这个WSGI application,请参考WSGI简介这篇文章。

从Magnum这个实际的例子可以看出,使用了Pecan之后,我们不再需要自己写那些冗余的WSGI application代码了,直接调用Pecan的make_app()函数就能完成这些工作。另外,对于之前使用PasteDeploy时用到的很多WSGI中间件,可以选择使用Pecan的hooks机制来实现,也选择使用WSGI中间件的方式来实现。在Magnum的API服务就同时使用了这两种方式。其实,Pecan还可以和PasteDeploy一起使用,Ceilometer项目就是这么做的,大家可以看看。

确定路由变得容易了:对象分发式的路由

Pecan不仅缩减了生成WSGI application的代码,而且也让开发人员更容易的指定一个application的路由。Pecan采用了一种对象分发风格(object-dispatch style)的路由模式。我们直接通过例子来解释这种路由模式,还是以Magnum项目为例。

上面提到了,Magnum的API服务的root是magnum.api.controllers.root.RootController。这里的RootController的是一个类,我们来看它的代码:

class RootController(rest.RestController):

_versions = ['v1']
"""All supported API versions"""

_default_version = 'v1'
"""The default API version"""

v1 = v1.Controller()

@expose.expose(Root)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return Root.convert()

@pecan.expose()
def _route(self, args):
string">"""Overrides the default routing behavior.

It redirects the request to the default version of the magnum API
if the version number is not specified in the url.
"""

if args[0] and args[0] not in self._versions:
args = [self._default_version] + args
return super(RootController, self)._route(args)

别看这个类这么长,我来解释一下你就懂了。首先,你可以先忽略掉_route()函数,这个函数是用来覆盖Pecan的默认路由实现的,在这里去掉它不妨碍我们理解Pecan(这里的_route()函数的作用把所有请求重定向到默认的API版本去)。去掉_route()和其他的东西后,整个类就变成这么短:

class RootController(rest.RestController):
v1 = v1.Controller()

@expose.expose(Root)
def get(self):
return Root.convert()

1.首先,你要记住,这个RootController对应的是URL中根路径,也就是path中最左边的/。

2.RootController继承自rest.RestController,是Pecan实现的RESTful控制器。这里的get()函数表示,当访问的是GET /时,由该函数处理。get()函数会返回一个WSME对象,表示一个形式化的HTTP Response,这个下面再讲。get()函数上面的expose装饰器是Pecan实现路由控制的一个方式,被expose的函数才会被路由处理。

3.这里的v1 = v1.Controller()表示,当访问的是GET /v1或者GET /v1/…时,请求由一个v1.Controller实例来处理。

为了加深大家的理解,我们再来看下v1.Controller的实现:

class Controller(rest.RestController):
"""Version 1 API controller root."""

bays = bay.BaysController()
baymodels = baymodel.BayModelsController()
containers = container.ContainersController()
nodes = node.NodesController()
pods = pod.PodsController()
rcs = rc.ReplicationControllersController()
services = service.ServicesController()
x509keypairs = x509keypair.X509KeyPairController()
certificates = certificate.CertificateController()

@expose.expose(V1)
def get(self):
return V1.convert()

...

上面这个Controller也是继承自rest.RestController。所以它的get函数表示,当访问的是GET /v1的时候,要做的处理。然后,它还有很多类属性,这些属性分别表示不同URL路径的控制器:

1./v1/bays 由bays处理

2./v1/baymodels 由baymodels处理

3./v1/containers 由containers处理

其他的都是类似的。我们再继续看bay.BaysController的代码:

class BaysController(rest.RestController):
"""REST controller for Bays."""
def __init__(self):
super(BaysController, self).__init__()

_custom_actions = {
'detail': ['GET'],
}

def get_all(...):

def detail(...):

def get_one(...):

def post(...):

def patch(...):

def delete(...):

这个controller中只有函数,没有任何类属性,而且没有实现任何特殊方法,所以/v1/bays开头的URL处理都在这个controller中终结。这个类会处理如下请求:

GET /v1/bays 
GET /v1/bays/{UUID}
POST /v1/bays
PATCH /v1/bays/{UUID}
DELETE /v1/bays/{UUID}
GET /v1/bays/detail/{UUID}

看了上面的3个controller之后,你应该能大概明白Pecan是如何对URL进行路由的。这种路由方式就是对象分发:根据类属性,包括数据属性和方法属性来决定如何路由一个HTTP请求。Pecan的文档中对请求的路由有专门的描述,要想掌握Pecan的路由还是要完整的看一下官方文档。

内置RESTful

支持我们上面举例的controller都是继承自pecan.rest.RestController,这种controller称为RESTful controller,专门用于实现RESTful API的,因此在,OpenStack中使用特别多。Pecan还支持普通的controller,称为Generic controller。Generic controller继承自object对象,默认没有实现对RESTful请求的方法。简单的说,RESTful controller帮我们规定好了get_one(),get_all(), get(),post()等方法对应的HTTP请求,而Generic controller则没有。关于这两种controller的区别,可以看官方文档《Writing RESTful Web Services with Generic Controllers》,有很清楚的示例。

对于RestController中没有预先定义好的方法,我们可以通过控制器的_custom_actions属性来指定其能处理的方法。

class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}

@expose()
def test(self):
return 'hello'

上面这个控制器是一个根控制器,指定了/test路径支持GET方法,效果如下:

$ curl http://localhost:8080/test
hello%

那么HTTP请求和HTTP响应呢?

上面讲了这么多,我们都没有说明在Pecan中如何处理请求和如何返回响应。这个将在下一章中说明,同时我们会引入一个新的库WSME。

WSME

Pecan对请求和响应的处理

在开始提到WSME之前,我们先来看下Pecan自己对HTTP请求和响应的处理。这样你能更好的理解为什么会再引入一个WSME库。

Pecan框架为每个线程维护了单独的请求和响应对象,你可以直接在请求处理函数中访问。pecan.request和pecan.response分别代表当前需要处理的请求和响应对象。你可以直接操作这两个对象,比如指定响应的状态码,就像下面这个例子一样(例子来自官方文档):

@pecan.expose()
def login(self):
assert pecan.request.path == '/login'
username = pecan.request.POST.get('username')
password = pecan.request.POST.get('password')

pecan.response.status = 403
pecan.response.text = 'Bad Login!'

这个例子演示了访问POST请求的参数以及返回403。你也可以重新构造一个pecan.Response对象作为返回值(例子来自官方文档):

from pecan import expose, Response

class RootController(object):

@expose()
def hello(self):
return Response('Hello, World!', 202)

另外,HTTP请求的参数也会可以作为控制器方法的参数,还是来看几个官方文档的例子:

class RootController(object):
@expose()
def index(self, arg):
return arg

@expose()
def kwargs(self, **kwargs):
return str(kwargs)

这个控制器中的方法直接返回了参数,演示了对GET请求参数的处理,效果是这样的:

$ curl http://localhost:8080/?arg=foo
foo
$ curl http://localhost:8080/kwargs?a=1&b=2&c=3
{u'a': u'1', u'c': u'3', u'b': u'2'}

有时候,参数也可能是URL的一部分,比如最后的一段path作为参数,就像下面这样:

class RootController(object):
@expose()
def args(self, *args):
return ','.join(args)

效果是这样的:

$ curl http://localhost:8080/args/one/two/three
one,two,three

另外,我们还要看一下POST方法的参数如何处理(例子来自官方文档):

class RootController(object):
@expose()
def index(self, arg):
return arg

效果如下,就是把HTTP body解析成了控制器方法的参数:

$ curl -X POST "http://localhost:8080/" -H "Content-Type: 
application/x-www-form-urlencoded" -d "arg=foo" foo

返回JSON还是HTML?

如果你不是明确的返回一个Response对象,那么Pecan中方法的返回内容类型就是由expose()装饰器决定的。默认情况下,控制器的方法返回的content-type是HTML。

class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}

@expose()
def test(self):
return 'hello'

效果如下:

 $ curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Tue, 15 Sep 2015 14:31:28 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 5
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0
hello%

也可以让它返回JSON:

class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}

@expose('json')
def test(self):
return 'hello'

效果如下:

curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Tue, 15 Sep 2015 14:33:27 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 18
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
{"hello": "world"}%

甚至,你还可以让一个控制器方法根据URL path的来决定是返回HTML还是JSON:

class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}

@expose()
@expose('json')
def test(self):
return json.dumps({'hello': 'world'})

返回JSON:

$ curl -v http://localhost:8080/test.json
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.json HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 14:26:27 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 24
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
"{\"hello\": \"world\"}"%

返回HTML:

$ curl -v http://localhost:8080/test.html
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.html HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 14:26:24 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 18
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0
{"hello": "world"}%

这里要注意一下:

同一个字符串作为JSON返回和作为HTML返回是不一样的,仔细看一下HTTP响应的内容。

我们的例子中在URL的最后加上了.html后缀或者.json后缀,请尝试一下不加后缀的化是返回什么?然后,调换一下两个expose()的顺序再试一下。

从上面的例子可以看出,决定响应类型的主要是传递给expose()函数的参数,我们看下expose()函数的完整声明:

pecan.decorators.expose(template=None,
content_type='text/html',
generic=False)

template参数用来指定返回值的模板,如果是’json’就会返回JSON内容,这里可以指定一个HTML文件,或者指定一个mako模板。

content_type指定响应的content-type,默认值是’text/html’。

generic参数表明该方法是一个“泛型”方法,可以指定多个不同的函数对应同一个路径的不同的HTTP方法。

看过参数的解释后,你应该能大概了解expose()函数是如何控制HTTP响应的内容和类型的。

用WSME来做什么?

上面两节已经说明了Pecan可以比较好的处理HTTP请求中的参数以及控制HTTP返回值。那么为什么我们还需要WSME呢?因为Pecan在做下面这个事情的时候比较麻烦:请求参数和响应内容的类型检查(英文简称就是typing)。当然,做是可以做的,不过你需要自己访问pecan.request和pecan.response,然后检查指定的值的类型。WSME就是为解决这个问题而生的,而且适用场景就是RESTful API。

WSME简介

WSME的全称是Web Service Made Easy,是专门用于实现REST服务的typing库,让你不需要直接操作请求和响应,而且刚好和Pecan结合得非常好,所以,OpenStack的很多项目都使用了Pecan + WSME的组合来实现API(好吧,我看过的项目,用了Pecan的都用了WSME)。WSME的理念是:在大部分情况下,Web服务的输入和输出对数据类型的要求都是严格的。所以它就专门解决了这个事情,然后把其他事情都交给其他框架去实现。因此,一般WSME都是和其他框架配合使用的,支持Pecan、Flask等。WSME的文档地址。

WSME的使用

用了WSME后的好处是什么呢?WSME会自动帮你检查HTTP请求和响应中的数据是否符合预先设定好的要求。WSME的主要方式是通过装饰器来控制controller方法的输入和输出。WSME中主要使用两个控制器:

@signature: 这个装饰器用来描述一个函数的输入和输出。

@wsexpose: 这个装饰器包含@signature的功能,同时会把函数的路由信息暴露给Web框架,效果就像Pecan的expose装饰器。

这里我们结合Pecan来讲解WSME的使用。先来看一个原始类型的例子:

from wsmeext.pecan import wsexpose

class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}

@wsexpose(int, int)
def test(self, number):
return number

如果不提供参数,访问会失败:

$ curl http://localhost:8080/test
{"debuginfo": null, "faultcode": "Client", "faultstring": "Missing argument: \"number\""}%

如果提供的参数不是整型,访问也会失败:

$ curl http://localhost:8080/test\?number\=a
{"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid
input for field/attribute number. Value: 'a'. unable to convert to int"}%

上面这些错误信息都是由WSME框架直接返回的,还没有执行到你写的方法。如果请求正确,那么会是这样的:

$ curl -v http://localhost:8080/test\?number\=1
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test?number=1 HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 15:06:35 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 1
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
1%

请注意返回的content-type,这里返回JSON是因为我们使用的wsexpose设置的返回类型是XML和JSON,并且JSON是默认值。上面这个例子就是WSME最简单的应用了。

那么现在有下面这些问题需要思考一下:

如果想用POST的方式来传递参数,要怎么做呢?提示:要阅读WSME中@signature装饰器的文档。

如果我希望使用/test/1这种方式来传递参数要怎么做呢?提示:要阅读Pecan文档中关于路由的部分。

WSME中支持对哪些类型的检查呢?WSME支持整型、浮点型、字符串、布尔型、日期时间等,甚至还支持用户自定义类型。提示:要阅读WSME文档中关于类型的部分。

WSME支持数组类型么?支持。

上面的问题其实也是很多人使用WSME的时候经常问的问题。我们将在下一篇文章中使用Pecan + WSME来继续开发我们的demo,并且用代码来回答上面所有的问题。

上一篇文章说到,我们将以实例的形式来继续讲述这个API服务的开发知识,这里会使用Pecan和WSME两个库。

设计REST API

要开发REST API服务,我们首先需要设计一下这个服务。设计包括要实现的功能,以及接口的具体规范。我们这里要实现的是一个简单的用户管理接口,包括增删改查等功能。如果读者对REST API不熟悉,可以先从Wiki页面了解一下。

另外,为了方便大家阅读和理解,本系列的代码会放在github上,diabloneo/webdemo。

Version of REST API

在OpenStack的项目中,都是在URL中表明这个API的版本号的,比如Keystone的API会有/v2.0和/v3的前缀,表明两个不同版本的API;Magnum项目目前的API则为v1版本。因为我们的webdemo项目才刚刚开始,所以我们也把我们的API版本设置为v1,下文会说明怎么实现这个version号的设置。

REST API of Users

我们将要设计一个管理用户的API,这个和Keystone的用户管理的API差不多,这里先列出每个API的形式,以及简要的内容说明。这里我们会把上面提到的version号也加入到URL path中,让读者能更容易联系起来。

GET /v1/users 获取所有用户的列表。

POST /v1/users 创建一个用户。

GET /v1/users/ 获取一个特定用户的详细信息。

PUT /v1/users/ 修改一个用户的详细信息。

DELETE /v1/users/ 删除一个用户。

这些就是我们要实现的用户管理的API了。其中,表示使用一个UUID字符串,这个是OpenStack中最经常被用来作为各种资源ID的形式,如下所示:

In [5]: import uuid
In [6]: print uuid.uuid4()
adb92482-baab-4832-84bc-f842f3eabd66
In [7]: print uuid.uuid4().hex
29520c88de6b4c76ae8deb48db0a71e7

因为是个demo,所以我们设置一个用户包含的信息会比较简单,只包含name和age。

使用Pecan搭建API服务的框架接下来就要开始编码工作了。首先要把整个服务的框架搭建起来。我们会在软件包管理这篇文件中的代码基础上继续我们的demo(所有这些代码在github的仓库里都能看到)。

代码目录结构

一般来说,OpenStack项目中,使用Pecan来开发API服务时,都会在代码目录下有一个专门的API目录,用来保存API相关的代码。比如Magnum项目的magnum/api,或者Ceilometer项目的ceilometer/api等。我们的代码也遵守这个规范,让我们直接来看下我们的代码目录结构(#后面的表示注释):

-> ~/programming/python/webdemo/webdemo/api git:(master) ? $ tree .
.
├── app.py # 这个文件存放WSGI application的入口
├── config.py # 这个文件存放Pecan的配置
├── controllers/ # 这个目录用来存放Pecan控制器的代码
├── hooks.py # 这个文件存放Pecan的hooks代码(本文中用不到)
└── __init__.py

这个在API服务(3)这篇文章中已经说明过了。

先让我们的服务跑起来为了后面更好的开发,我们需要先让我们的服务在本地跑起来,这样可以方便自己做测试,看到代码的效果。不过要做到这点,还是有些复杂的。

必要的代码

首先,先创建config.py文件的内容:

app = {
'root': 'webdemo.api.controllers.root.RootController',
'modules': ['webdemo.api'],
'debug': False,
}

就是包含了Pecan的最基本配置,其中指定了root controller的位置。然后看下app.py文件的内容,主要就是读取config.py中的配置,然后创建一个WSGI application:

import pecan

from webdemo.api import config as api_config


def get_pecan_config():
filename = api_config.__file__.replace('.pyc', '.py')
return pecan.configuration.conf_from_file(filename)


def setup_app():
config = get_pecan_config()

app_conf = dict(config.app)
app = pecan.make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
**app_conf
)

return app

然后,我们至少还需要实现一下root controller,也就是webdemo/api/controllers/root.py这个文件中的RootController类:

from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan


class RootController(rest.RestController):

@wsme_pecan.wsexpose(wtypes.text)
def get(self):
return "webdemo"

本地测试服务器

为了继续开放的方便,我们要先创建一个Python脚本,可以启动一个单进程的API服务。这个脚本会放在webdemo/cmd/目录下,名称是api.py(这目录和脚本名称也是惯例),来看看我们的api.py吧:

from wsgiref import simple_server

from webdemo.api import app


def main():
host = '0.0.0.0'
port = 8080

application = app.setup_app()
srv = simple_server.make_server(host, port, application)

srv.serve_forever()


if __name__ == '__main__':
main()

运行测试服务器的环境

要运行这个测试服务器,首先需要安装必要的包,并且设置正确的路径。在后面的文章中,我们将会知道,这个可以通过tox这个工具来实现。现在,我们先做个简单版本的,就是手动创建这个运行环境。

首先,完善一下requirements.txt这个文件,包含我们需要的包:

pbr<2.0,>=0.11
pecan
WSME

然后,我们手动创建一个virtualenv环境,并且安装requirements.txt中要求的包:

-> ~/programming/python/webdemo git:(master) ? $ virtualenv .venv
New python executable in .venv/bin/python
Installing setuptools, pip, wheel...done.
-> ~/programming/python/webdemo git:(master) ? $ source .venv/bin/activate
(.venv)
-> ~/programming/python/webdemo git:(master) ? $ pip install -r requirement.txt
...
Successfully installed Mako-1.0.3 MarkupSafe-0.23 WSME-0.8.0 WebOb-
1.5.1 WebTest-2.0.20 beautifulsoup4-4.4.1 logutils-0.3.3 netaddr-
0.7.18 pbr-1.8.1 pecan-1.0.3 pytz-2015.7 simplegeneric-0.8.1
singledispatch-3.4.0.3 six-1.10.0 waitress-0.8.10

启动我们的服务

启动服务需要技巧,因为我们的webdemo还没有安装到系统的Python路径中,也不在上面创建virtualenv环境中,所以我们需要通过指定PYTHONPATH这个环境变量来为Python程序增加库的查找路径:

(.venv)-> ~/programming/python/webdemo git:(master) ? $ PYTHONPATH=. python webdemo/cmd/api.py

现在测试服务器已经起来了,可以通过浏览器访问http://localhost:8080/ 这个地址来查看结果。(你可能会发现,返回的是XML格式的结果,而我们想要的是JSON格式的。这个是WSME的问题,我们后面再来处理)。

到这里,我们的REST API服务的框架已经搭建完成,并且测试服务器也跑起来了。

用户管理API的实现

现在我们来实现我们在第一章设计的API。这里先说明一下:我们会直接使用Pecan的RestController来实现REST API,这样可以不用为每个接口指定接受的method。

让API返回JSON格式的数据

现在,所有的OpenStack项目的REST API的返回格式都是使用JSON标准,所以我们也要这么做。那么有什么办法能够让WSME框架返回JSON数据呢?可以通过设置wsmeext.pecan.wsexpose()的rest_content_types参数来是先。这里,我们借鉴一段Magnum项目中的代码,把这段代码存放在文件webdemo/api/expose.py中:

import wsmeext.pecan as wsme_pecan


def expose(*args, **kwargs):
"""Ensure that only JSON, and not XML, is supported."""
if 'rest_content_types' not in kwargs:
kwargs['rest_content_types'] = ('json',)

return wsme_pecan.wsexpose(*args, **kwargs)

这样我们就封装了自己的expose装饰器,每次都会设置响应的content-type为JSON。上面的root controller代码也就可以修改为:

from pecan import rest
from wsme import types as wtypes

from webdemo.api import expose


class RootController(rest.RestController):

@expose.expose(wtypes.text)
def get(self):
return "webdemo"

再次运行我们的测试服务器,就可以返现返回值为JSON格式了。

实现 GET /v1这个其实就是实现v1这个版本的API的路径前缀。在Pecan的帮助下,我们很容易实现这个,只要按照如下两步做即可:

先实现v1这个controller

把v1 controller加入到root controller中

按照OpenStack项目的规范,我们会先建立一个webdemo/api/controllers/v1/目录,然后将v1 controller放在这个目录下的一个文件中,假设我们就放在v1/controller.py文件中,效果如下:

from pecan import rest
from wsme import types as wtypes

from webdemo.api import expose


class V1Controller(rest.RestController):

@expose.expose(wtypes.text)
def get(self):
return 'webdemo v1controller'

然后把这个controller加入到root controller中:

...
from webdemo.api.controllers.v1 import controller as v1_controller
from webdemo.api import expose


class RootController(rest.RestController):
v1 = v1_controller.V1Controller()

@expose.expose(wtypes.text)
def get(self):
return "webdemo"

此时,你访问http://localhost:8080/v1就可以看到结果了。

实现 GET/v1/users

添加users controller

这个API就是返回所有的用户信息,功能很简单。首先要添加users controller到上面的v1 controller中。为了不影响阅读体验,这里就不贴代码了,请看github上的示例代码。

使用WSME来规范API的响应值

上篇文章中,我们已经提到了WSME可以用来规范API的请求和响应的值,这里我们就要用上它。首先,我们要参考OpenStack的惯例来设计这个API的返回值:

{
"users": [
{
"name": "Alice",
"age": 30
},
{
"name": "Bob",
"age": 40
}
]
}

其中users是一个列表,列表中的每个元素都是一个user。那么,我们要如何使用WSME来规范我们的响应值呢?答案就是使用WSME的自定义类型。我们可以利用WSME的类型功能定义出一个user类型,然后再定义一个user的列表类型。最后,我们就可以使用上面的expose方法来规定这个API返回的是一个user的列表类型。

定义user类型和user列表类型

这里我们需要用到WSME的Complex types的功能,请先看一下文档Types。简单说,就是我们可以把WSME的基本类型组合成一个复杂的类型。我们的类型需要继承自wsme.types.Base这个类。因为我们在本文只会实现一个user相关的API,所以这里我们把所有的代码都放在webdemo/api/controllers/v1/users.py文件中。来看下和user类型定义相关的部分:

from wsme import types as wtypes


class User(wtypes.Base):
name = wtypes.text
age = int


class Users(wtypes.Base):
users = [User]

这里我们定义了class User,表示一个用户信息,包含两个字段,name是一个文本,age是一个整型。class Users表示一组用户信息,包含一个字段users,是一个列表,列表的元素是上面定义的class User。完成这些定义后,我们就使用WSME来检查我们的API是否返回了合格的值;另一方面,只要我们的API返回了这些类型,那么就能通过WSME的检查。我们先来完成利用WSME来检查API返回值的代码:

class UsersController(rest.RestController):

# expose方法的第一个参数表示返回值的类型
@expose.expose(Users)
def get(self):
pass

这样就完成了API的返回值检查了。

实现API逻辑

我们现在来完成API的逻辑部分。不过为了方便大家理解,我们直接返回一个写好的数据,就是上面贴出来的那个。

class UsersController(rest.RestController):

@expose.expose(Users)
def get(self):
user_info_list = [
{
'name': 'Alice',
'age': 30,
},
{
'name': 'Bob',
'age': 40,
}
]
users_list = [User(**user_info) for user_info in user_info_list]
return Users(users=users_list)

代码中,会先根据user信息生成User实例的列表users_list,然后再生成Users实例。此时,重启测试服务器后,你就可以从浏览器访问http://localhost:8080/v1/users,就能看到结果了。

实现 POST /v1/users

这个API会接收用户上传的一个JSON格式的数据,然后打印出来(实际中一般是存到数据库之类的),要求用户上传的数据符合User类型的规范,并且返回的状态码为201。代码如下:

class UsersController(rest.RestController):

@expose.expose(None, body=User, status_code=201)
def post(self, user):
print user

可以使用curl程序来测试:

~/programming/python/webdemo git:(master) -> $ curl -X POST 
http://localhost:8080/v1/users -H "Content-Type: application/json" -d
'{"name": "Cook", "age": 50}' -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /v1/users HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
>
* upload completely sent off: 27 out of 27 bytes
* HTTP 1.0, assume close after body
< HTTP/1.0 201 Created
< Date: Mon, 16 Nov 2015 15:18:24 GMT
< Server: WSGIServer/0.1 Python/2.7.10
< Content-Length: 0
<
* Closing connection 0

同时,服务器上也会打印出:

127.0.0.1 - - [16/Nov/2015 23:16:28] "POST /v1/users HTTP/1.1" 201 0
<webdemo.api.controllers.v1.users.User object at 0x7f65e058d550>

我们用3行代码就实现了这个POST的逻辑。现在来说明一下这里的秘密。expose装饰器的第一个参数表示这个方法没有返回值;第三个参数表示这个API的响应状态码是201,如果不加这个参数,在没有返回值的情况下,默认会返回204。第二个参数要说明一下,这里用的是body=User,你也可以直接写User。使用body=User这种形式,你可以直接发送符合User规范的JSON字符串;如果是用expose(None, User, status_code=201)那么你需要发送下面这样的数据:

{ "user": {"name": "Cook", "age": 50} }

你可以自己测试一下区别。要更多的了解本节提到的expose参数,请参考WSM文档Functions。

最后,你接收到一个创建用户请求时,一般会为这个用户分配一个id。本文前面已经提到了OpenStack项目中一般使用UUID。你可以修改一下上面的逻辑,为每个用户分配一个UUID。

实现 GET /v1/users/

要实现这个API,需要两个步骤:

1。在UsersController中解析出的部分,然后把请求传递给这个一个新的UserController。从命名可以看出,UsersController是针对多个用户的,UserController是针对一个用户的。

2.在UserController中实现get()方法。

使用_lookup()方法

Pecan的_lookup()方法是controller中的一个特殊方法,Pecan会在特定的时候调用这个方法来实现更灵活的URL路由。Pecan还支持用户实现_default()和_route()方法。这些方法的具体说明,请阅读Pecan的文档:routing。

我们这里只用到_lookup()方法,这个方法会在controller中没有其他方法可以执行且没有_default()方法的时候执行。比如上面的UsersController中,没有定义/v1/users/如何处理,它只能返回404;如果你定义了_lookup()方法,那么它就会调用该方法。

_lookup()方法需要返回一个元组,元组的第一个元素是下一个controller的实例,第二个元素是URL path中剩余的部分。

在这里,我们就需要在_lookup()方法中解析出UUID的部分并传递给新的controller作为新的参数,并且返回剩余的URL path。来看下代码:

class UserController(rest.RestController):

def __init__(self, user_id):
self.user_id = user_id


class UsersController(rest.RestController):

@pecan.expose()
def _lookup(self, user_id, *remainder):
return UserController(user_id), remainder

_lookup()方法的形式为_lookup(self, user_id, *remainder),意思就是会把/v1/users/中的部分作为user_id这个参数,剩余的按照”/”分割为一个数组参数(这里remainder为空)。然后,_lookup()方法里会初始化一个UserController实例,使用user_id作为初始化参数。这么做之后,这个初始化的控制器就能知道是要查找哪个用户了。然后这个控制器会被返回,作为下一个控制被调用。请求的处理流程就这么转移到UserController中了。

实现API逻辑

实现前,我们要先修改一下我们返回的数据,里面需要增加一个id字段。对应的User定义如下:

class User(wtypes.Base):
id = wtypes.text
name = wtypes.text
age = int

现在,完整的UserController代码如下:

class UserController(rest.RestController):

def __init__(self, user_id):
self.user_id = user_id

@expose.expose(User)
def get(self):
user_info = {
'id': self.user_id,
'name': 'Alice',
'age': 30,
}
return User(**user_info)

使用curl来检查一下效果:

~/programming/python/webdemo git:(master) ? $ curl 
http://localhost:8080/v1/users/29520c88de6b4c76ae8deb48db0a71e7
{"age": 30, "id": "29520c88de6b4c76ae8deb48db0a71e7", "name": "Alice"}

定义WSME类型的技巧

你可能会有疑问:这里我们修改了User类型,增加了一个id字段,那么前面实现的POST /v1/users会不会失效呢?你可以自己测试一下。(答案是不会,因为这个类型里的字段都是可选的)。这里顺便讲两个技巧。

如何设置一个字段为强制字段

像下面这样做就可以了(你可以测试一下,改成这样后,不传递id的POST /v1/users会失败):

class User(wtypes.Base):
id = wtypes.wsattr(wtypes.text, mandatory=True)
name = wtypes.text
age = int

如何检查一个可选字段的值是否存在

检查这个值是否为None是肯定不行的,需要检查这个值是否为wsme.Unset。

实现 PUT /v1/users/这个和上一个API一样,不过_lookup()方法已经实现过了,直接添加方法到UserController中即可:

class UserController(rest.RestController):

@expose.expose(User, body=User)
def put(self, user):
user_info = {
'id': self.user_id,
'name': user.name,
'age': user.age + 1,
}
return User(**user_info)

通过curl来测试:

-> ~/programming/python/webdemo git:(master) ? $ curl -X PUT
http://localhost:8080/v1/users/29520c88de6b4c76ae8deb48db0a71e7 -H
"Content-Type: application/json" -d '{"name": "Cook", "age": 50}'
{"age": 51, "id": "29520c88de6b4c76ae8deb48db0a71e7", "name": "Cook"}%

实现 DELETE /v1/users/

同上,没有什么新的内容:

class UserController(rest.RestController):

@expose.expose()
def delete(self):
print 'Delete user_id: %s' % self.user_id

总结

到此为止,我们已经完成了我们的API服务了,虽然没有实际的逻辑,但是本文搭建起来的框架也是OpenStack中API服务的一个常用框架,很多大项目的API服务代码都和我们的webdemo长得差不多。最后再说一下,本文的代码在github上托管着:diabloneo/webdemo。

现在我们已经了解了包管理和API服务了,那么接下来就要开始数据库相关的操作了。大部分OpenStack的项目都是使用非常著名的sqlalchemy库来实现数据库操作的,本系列接下来的文章就是要来说明数据库的相关知识和应用。

   
次浏览       
 
相关文章

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

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

云计算原理与应用
云计算应用与开发
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培训
北京 云计算原理与应用