- 前段时间一直在做openstacksdk的二次开发工作,对openstacksdk源码了解得比较深入,现趁着国庆假期,好好总结一下。毕竟记忆不好,过段时间可能就忘得差不多了,哈哈。
- openstacksdk是基于当前最新版openstacksdk-0.17.2版本,可从 GitHub:OpenstackSDK 获取到最新的源码。openstacksdk的官方使用手册为:SDK 文档
- 实验使用的openstack是Queen版本。
SDK 的历史背景
据官方文档介绍,openstacksdk项目是由shade、os-client-config、python-openstacksdk三个子项目合并而成的。由于我对这三个项目不太熟悉,所以也没什么好说的,感兴趣的读者可以自行查看每个项目的主要用途。为什么合并三个项目,我理解的原因有几个吧:
- 方便开发者使用,虽然openstack的子项目都会有restful api的调用接口,但对于开发者来说,调用sdk比使用rest方便,也不需要处理形形色色的各种突发情况;
- 方便维护,访问openstack服务的client端底层都需要配置session(即config配置)以及处理python-request,而这三个项目提供的服务接口都有着几乎相同的底层处理,部分服务接口也雷同,维护起来麻烦;
- 项目功能重复,比如shade和python-openstacksdk虽然实现方式有所不同,但是提供的功能类似,没必要维护多个相同的服务组件。
不管如何,上面说的都不是重点。由于openstacksdk融合了三个子项目,为了向下兼容,也导致了openstacksdk项目的复杂度。第一次看源码的人可能会很纳闷,明明是有个具体的框架,但有些模块偏偏却脱离在框架之外,让人摸不着头脑。说白了就是代码的组织架构不够清晰,但也没办法,毕竟sdk还在开发过程中,后续应该会变得更好吧。
SDK 的使用
SDK使用不是这篇文章的重点,因此只会简单的描述一下。具体的可参考官文的 User Guides。
总的来说,使用sdk很简单,其支持三种形式的配置方式,分别是配置文件、函数传参、系统环境变量三种。这三种可以混合使用,如果有重复配置,则优先顺序应该是:函数传参 > 系统环境变量 > 配置文件。同时,使用sdk不仅支持user/password方式,也支持传入token形式使用,但是两种认证方式的配置有些许不同,token和password有某些关于auth的配置是冲突的。
-
配置文件方式
sdk会默认寻找名称为clouds.yaml
的配置文件,其搜寻顺序为:当前目录
、~/.config/openstack
、/etc/openstack
。只要找到符合的配置文件,就会读取并解析内容。
clouds.yaml的配置文件写法为:clouds: mordred: region_name: RegionOne auth_type: password auth: username: 'mordred' password: XXXXXXX project_name: 'shade' auth_url: 'https://identity.example.com'
import openstack # Initialize cloud conn = openstack.connect(cloud='mordred') for server in conn.compute.servers(): print(server.to_dict())
-
函数传参方式
import openstack # Initialize cloud auth_type = "token" auth = { "project_id": "xxxxx", "user_domain_id": "default", "token": "xxxxxx" "auth_url": "https://identity.example.com" } conn = openstack.connect(auth=auth, auth_type=auth_type) for server in conn.compute.servers(): print(server.to_dict())
-
系统环境变量方式
admin-openrc.sh
文件设置如下:export OS_PROJECT_DOMAIN_ID=default export OS_USER_DOMAIN_ID=default export OS_PROJECT_NAME=admin export OS_TENANT_NAME=admin export OS_USERNAME=admin export OS_PASSWORD=ADMIN_PASS export OS_AUTH_URL=http://controller:35357/v3 export OS_IDENTITY_API_VERSION=3
在shell中执行
source admin-openrc.sh
,然后执行如下代码即可:import openstack # Initialize cloud conn = openstack.connect() for server in conn.compute.servers(): print(server.to_dict())
SDK 的源码解析
从上面的使用例子我们可以看到,openstacksdk的入口函数是openstack.connect(*args, **kwargs)
。这个函数基于传入的参数,生成了连接openstack服务的实例,然后我们就可以使用该实例访问openstack各组件服务了,具体的调用方式就是:conn.{service}.{operator}
。其中service
就是openstack的组件,比如常用的compute(nova)、image(glance)、identity(keystone) 和 block_storage(cinder)等等了。
循例的,要了解SDK的源码实现,首先还是先介绍下源码目录结构吧。
源码目录结构
先不多说,呈上源码目录结构的缩减版:
openstack/
|--_meta/ # 存放框架中的metaclass类的实现
|--config/ # os-client-config项目代码主要存放在此
|--|--cloud/ # shade项目代码主要存放在此
|--|--{services}/ # 各个服务组件的代码,应该是python-openstacksdk项目代码,以identity为例
|--|--identity/
|--|--|--v2/
|--|--|--|--_proxy.py # v2版本的proxy接口
|--|--|--|--{resources} # v2版本resource类,比如role资源
|--|--|--v3/
|--|--|--|--_proxy.py # v3版本的proxy接口
|--|--|--|--{resources} # v3版本的role资源类
|--|--|--identity_service.py # identity的服务入口
|--resource.py # 定义资源类的基类
|--proxy.py # 定义proxy接口的基类
|--connection.py # 管理各openstack服务的类,暴露给用户使用
|--service_description.py # 服务的描述类,conn通过该类找到对应的组件服务接口
|--service_filter.py # 划分并路由到组件服务的不同版本的类
|--task_manager.py # 管理组件服务的rest请求处理
前面说了,openstacksdk是由三个子项目合并而来的。由目录结构就可以看出,三个项目的代码都扁平地放在openstack
目录下。而其余的*.py
文件就是负责整合这三个项目的框架代码。其中,框架中比较重要的类有三个,其分别为:
- Connection 类是负责建立用户层到服务层的服务连接,即如何组织这三个子项目,将其功能提供给上层用户调用,就是Connection类要做的事情。
- Proxy 类是代理各组件的服务,为用户提供该组件服务的可使用接口的类,其子类由各个组件进行定义。Proxy类继承了
keystoneauth.adapter
,其是openstack用于认证和访问组件rest服务的通用库。同样以identity为例,其identity/v2/_proxy.py
里定义了所有keystone v2版本的可使用服务接口。 - Resource 类代表的是组件的远程资源,比如identity有role、project、user等资源类型,其定义在
identity/v2/{name}.py
中。一般地,用户调用proxy接口的方法,proxy调用resource构建 rest 请求中需要的所有东西,如header、url、data等等,然后resource调用proxy中的keystoneauth里的request方法与远程服务组件进行交互,然后resource处理其返回response,获取与该资源类相关的属性值,然后proxy返回特定resource类的实例给用户。
源码的架构组织
通过目录结构的介绍,我们对sdk源码已经有了大致的认识了。下面通过架构图,能够更好地了解其框架组织结构:
其中,CloudRegion为Connection相关的配置(由原os-client-config模块进行适配),用于处理使用openstacksdk的各种配置;OpenstackCloud为一种形式的接口调用(原shade项目模块),其提供所有的服务接口都放在一个类里实现了,即实例化该类,就可以调用各组件的服务处理接口了;各种Proxy我猜是原python-openstacksdk的接口,它是上面所说的以组件区分服务的一种组织形式,是区别OpenstackCloud的另一种服务使用方式。但是,不管是OpenStackCloud方式,还是Proxy的调用方式,最终都是通过keystoneauth库去调用openstack组件服务的Restful API接口进行处理的。
源码的详细解析
在了解SDK项目的组织架构后,下面我们详细地对SDK源码进行解读分析。
首先,OpenstackCloud定义在 openstack/cloud
目录下,其就是一个类里面定义了一堆关于network、images、compute等的函数方法,就是在一个大类里填充了所有关于openstack操作的函数,非常暴力简单,没什么好说的,直接用就好,本文章也不会对其进行过多的解析,有兴趣的自己看看代码吧。
CloudRegion类定义并解析了openstack的客户端配置,比如前面所说的用户密码,连接方式,服务组件的接口的API版本等等。虽然很复杂,但是也没什么好讲的。其实是太复杂了,不好把握,哈哈哈。无论如何,这里配置其实只是和keystoneauth库session需要用到的配置相关,和框架服务倒没什么关系。
个人觉得sdk的精华部分大概也就剩Connection–>Proxy–>Resource这条线了哈。 Connection到Proxy这条路径经过了ServiceDescription这个类,而不是直接Connection里初始化Proxy的原因,是为了在使用时才初始化对应组件的proxy实例。大家都知道,openstack组件有很多,如果client只是使用了image服务,但是却要实例化上百个proxy,其性能是很低下的,也没有必要。而ServiceDescription就是用来解决这种情况的,其就是相当于Connection只是维护了所有服务组件的指针数组,在真正使用某个服务组件的接口时,才实例化该proxy服务,并将指针数组中的特定一个指针指向该实例。那么,后续又用到该服务时就不用重新初始化一遍了,直接返回该实例引用就行了。
Connection类
Connection 是用户使用时接触到的最上层的类了。正如上面用户使用的例子所介绍,一般会使用openstack.connnect()
方法生成Conenction实例。connect()
方法做了两件事情,一是解析配置,并生成一个CloudRegion的实例,一是生成Connection实例。代码如下所示:
def connect(*agrs, **kwargs):
cloud_region = openstack.config.get_cloud_region(
cloud=cloud,
app_name=app_name, app_version=app_version,
load_yaml_config=load_yaml_config,
load_envvars=load_envvars,
options=options, **kwargs)
return openstack.connection.Connection(config=cloud_region)
下面,我们看看Connection类内部都做了什么事情。首先,看看它类实现的代码,这里做了些调整,为了减少代码篇幅,把profile及一些不太重要的内容给去掉了,profile后续应该会deprecate了:
from openstack._meta import connection as _meta
from openstack import cloud as _cloud
# 类定义其实相当于是这样的,即继承了OpenstackCloud类,拥有OpentackCloud的所有方法:
# class Connection(_cloud.OpenStackCloud):
# __metaclass__ = _meta.ConnectionMeta
class Connection(six.with_metaclass(_meta.ConnectionMeta,
_cloud.OpenStackCloud)):
def __init__(self, cloud=None, config=None, session=None,
app_name=None, app_version=None,
extra_services=None,
strict=False,
use_direct_get=False,
task_manager=None,
**kwargs):
"""Create a connection to a cloud."""
# config即上面我们传进来的CloudRegion实例,负责管理连接配置的内容的
# 如果没有,后续会创建,但是默认是不读取配置文件和系统环境变量
self.config = config
self._extra_services = {
}
# 即允许注册额外的services
if extra_services: