《OpenStack王者归来》的最后一章说明了如何为openstack创建一个自定义的组件,并且提供了源码https://github.com/JiYou/openstack-ubuntu-14-04/tree/master/chap19/monitor,组件名为monitor。书中对monitor service那端的代码说的很清楚,但是client的貌似只是带过了…(有可能是我没看之前的章节)╮(╯▽╰)╭…所以慢慢看monitorclient的源码吧…
命令monitor的创建
使用monitor.sh来安装monitor组件,在monitor.sh中进行了monitor-api等服务与monitorclient代码的安装:
install_package monitor ./tools/ pip-requires
install_package python-monitorclient-1.1 ./tools/ pip-requires
source_install monitor
source_install python-monitorclient-1.1
install_package与source_install均在tools/function中,source_install代码如下
function source_install() {
olddir=`pwd`
cd $DEST/$1
[[ -d .git ]] && git checkout master
python setup.py build
python setup.py develop
cd $olddir
}
DEST变量的值为/opt/stack,对于python-monitorclient来说,相当于先cd到/opt/stack/python-monitorclient-1.1.下,并执行python setup.py build与python setup.py develop。在setup.py中有:
project = 'python-monitorclient'
...
setuptools.setup(
name=project,
...
entry_points={
"console_scripts": ["monitor = monitorclient.shell:main"]
}
)
setup.py将会创建/usr/local/bin/monitor,内容:
#!/usr/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'python-monitorclient','console_scripts','monitor'
__requires__ = 'python-monitorclient'
import re
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(
load_entry_point('python-monitorclient', 'console_scripts', 'monitor')()
)
在命令行中输入monitor后,将会执行入口点:monitorclient.shell:main()函数
解析命令行参数
monitorclient.shell:main()函数为命令monitor的入口函数,调用了
OpenStackMonitorShell().main(map(strutils.safe_decode, sys.argv[1:]))
map(function, iterable, ...)
是对可迭代函数’iterable’中的每一个元素应用‘function’方法,将结果作为list返回,如:
>>> def test(x):
... return x * 10
...
>>> data = [1, 2, 3]
>>> map(test, data)
[10, 20, 30]
>>>
因此这里对输入的每个参数利用strutils.safe_decode解码,之后调用OpenStackMonitorShell().main,main函数首先执行:
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.setup_debugging(options.debug)
get_base_parser调用parser.add_argument确定要解析的参数,parse_known_args将会对输入参数进行解析,将成功解析的参数放在第一个返回值里,没有解析出来的参数放在第二个返回值里,具体描述见
https://docs.python.org/2/library/argparse.html?highlight=argparse#module-argparse
接下来:
# build available subcommands based on version
self.extensions = self._discover_extensions(
options.os_monitor_api_version)
self._run_extension_hooks('__pre_parse_args__')
这一段是用来扫描扩展的客户端API,_discover_extensions中查找了两个方面的python模块,并返回模块的name与module,再分别构建类monitorceint.extension.Extension
- 调用_discover_via_python_path,查找了以python_monitorclient_ext来结尾的python模块
- 调用_discover_via_contrib_path,查找了位于monitorclient/v1/contrib下的除__init__.py之外的所有py文件
def _discover_extensions(self, version):
extensions = []
for name, module in itertools.chain(
self._discover_via_python_path(version),
self._discover_via_contrib_path(version)):
extension = monitorclient.extension.Extension(name, module)
extensions.append(extension)
return extensions
_run_extension_hooks函数则对扫描到的extensions做遍历,并运行带有pre_parse_args的钩子
处理完extension API后,再来解析子命令(parse_known_args没有解析出来的参数)
subcommand_parser = self.get_subcommand_parser(
options.os_monitor_api_version)
...
args = subcommand_parser.parse_args(argv)
self._run_extension_hooks('__post_parse_args__', args)
get_subcommand_parser中,首先增加一个子parser,对应的metavar为subcommand,之后调用三个_find_actions为子parser查找可用的子命令并注册对应的模块
actions_module = {
'1.1': shell_v1,
}[version]
self._find_actions(subparsers, actions_module)
self._find_actions(subparsers, self)
for extension in self.extensions:
self._find_actions(subparsers, extension.module)
- 第一个_find_actions针对的模块为shell_v1,从
from monitorclient.v1 import shell as shell_v1
获知扫描的文件为monitorclient/v1/shell.py - 第二个则针对于当前类
- 第三个针对于之前所获取的extensions API对应的py文件
在_find_actions中,扫描模块中的以do_开头的函数,将其作为子命令,并作为子命令的默认回调函数
def _find_actions(self, subparsers, actions_module):
# 遍历py文件中以do_开头的函数,如monitorclient/v1/shell.py中的do_list函数
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
# 以找到的函数名作为子命令的名字(替换掉'_')
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
# 获取描述、帮助、参数等信息
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
# print 'JIYOU _find_actions in shell.py'
# 注册subparser,利用add_argument说明该command对应的参数,使用set_defaults说明默认回调的函数(即为do_CMD)
subparser = subparsers.add_parser(
command,
help=help,
description=desc,
add_help=False,
formatter_class=OpenStackHelpFormatter)
subparser.add_argument('-h', '--help',
action='help',
help=argparse.SUPPRESS,)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
再接着看main函数:
(os_username, os_password, os_tenant_name, os_auth_url,
os_region_name, os_tenant_id, endpoint_type, insecure,
service_type, service_name, monitor_service_name,
username, apikey, projectid, url, region_name, cacert) = (
args.os_username, args.os_password,
args.os_tenant_name, args.os_auth_url,
args.os_region_name, args.os_tenant_id,
args.endpoint_type, args.insecure,
args.service_type, args.service_name,
args.monitor_service_name, args.username,
args.apikey, args.projectid,
args.url, args.region_name, args.os_cacert)
if not endpoint_type:
endpoint_type = DEFAULT_ENERGY_ENDPOINT_TYPE
...
这里都是在进行参数的判断,如要求要有env[OS_TENANT_ID],env[OS_AUTH_URL]等等,至此参数解析判断完成
响应子命令
在解析完命令行参数之后,main函数创建了client类,并调用子命令对应的默认回调函数
self.cs = client.Client(options.os_monitor_api_version, os_username,
os_password, os_tenant_name, os_auth_url,
insecure, region_name=os_region_name,
tenant_id=os_tenant_id,
endpoint_type=endpoint_type,
extensions=self.extensions,
service_type=service_type,
service_name=service_name,
monitor_service_name=monitor_service_name,
retries=options.retries,
http_log_debug=args.debug,
cacert=cacert)
args.func(self.cs, args)
Client函数位于monitorclient/client.py,其中调用了get_client_class函数,并返回找到的类的一个实例
def Client(version, *args, **kwargs):
client_class = get_client_class(version)
return client_class(*args, **kwargs)
get_client_class函数根据版本来import模块,目前的版本为v1,import的模块位于monitorclient.v1.client.Client,该类的__init__函数中,首先
self.limits = limits.LimitsManager(self)
# extensions
self.monitors = monitors.ServiceManageManager(self)
self.monitor_snapshots = monitor_snapshots.SnapshotManager(self)
self.monitor_types = monitor_types.ServiceManageTypeManager(self)
self.quota_classes = quota_classes.QuotaClassSetManager(self)
self.quotas = quotas.QuotaSetManager(self)
self.backups = monitor_backups.ServiceManageBackupManager(self)
self.restores = monitor_backups_restore.ServiceManageBackupRestoreManager(self)
这些创建的类属性都利用一个Manager类来初始化,接下来则扫描输入参数的extension
# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))
注意在extensions.py的__init__方法里还查找了这个模块里是否有base.Manager的子类,若有的话,将子类赋值给属性manager_class,所以这一段为每一个extensions创建了一个Client类的属性,属性名为extension.name,并用对应的manager类初始化
def __init__(self, name, module):
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
elif utils.safe_issubclass(attr_value, base.Manager):
self.manager_class = attr_value
下面来看这个Manager类到底做了些什么。base.Manager位于monitorclient/base.py
class Manager(utils.HookableMixin):
"""
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
__init__的输入参数为api,Client在调用Manager类时输入的实参为self,从而Client类作为属性(名为api)绑定到了各个Manager类中,而各个manager类也通过成员属性的方式绑定在Client类上
Client类在最后初始化HTTPClient,用来进行post、get、delete、put等操作
self.client = client.HTTPClient(
username,
password
...
命令流程
随便找一个Manager类,如monitorclient/v1/contrib/list_extensions.py,先前在解析参数中说到,monitor会解析v1/contrib下的所有py,并且加入到extensions中,而Client也建立与extensions的Manager类的绑定关系
class ListExtManager(base.Manager):
resource_class = ListExtResource
def show_all(self):
return self._list("/extensions", 'extensions')
另一方面,解析参数时,针对的子命令的默认回调函数为{do_子命令}(并将-替换成了_),以上py带有do_开头的函数有:
@utils.service_type('monitor')
def do_list_extensions(client, _args):
"""
List all the os-api extensions that are available.
"""
extensions = client.list_extensions.show_all()
fields = ["Name", "Summary", "Alias", "Updated"]
utils.print_list(extensions, fields)
这样,在解析到list-extensions子命令的时候,创建Client类来构建与Manager的绑定关系,然后调用do_list_extensions。该函数调用了client.list_extensions.show_all()
,client.list_extensions即为实例化的ListExtManager类,所以调用了ListExtManager.show_all,接着调用了ListExtManager._list。__list在base.py中
def _list(self, url, response_key, obj_class=None, body=None):
print '->JIYOU _list() in base.py'
resp = None
if body:
resp, body = self.api.client.post(url, body=body)
else:
resp, body = self.api.client.get(url)
print
if obj_class is None:
obj_class = self.resource_class
在这里发起了post或者get请求,注意self.api即是Client类,而Client类的client成员用HTTPClient类进行实例化。。
至此,命令请求就通过RESTful发出去了~