CeilometerClient源码学习记录

在强者的眼中,没有最好,只有更好。

此篇文章介绍关于在命令行输入相关命令之后,这个命令是如何调用并且执行的

代码结构

首先介绍一下代码目录的结构

1
2
3
4
5
6
7
8
9
--ceilometerclient
--common/
--openstack/
--tests/
--v1/
--v2/
--client.py
--shell.py
--exc.py

其中commonopenstack提供utils帮助类,tests是测试用例目录,v1v2分别对应ceilometerv1v2版本。client.py提供获取client的方法,包括keystoneclient,提供获取endpointtoken的方法;还包括获取ceilometerclient的函数。shell.py是模块的入口,提供对命令行的解析和命令的调用。exc.py提供用到的异常类。

在每个版本目录下,代码的组织如下(v2为例):

1
2
3
4
5
6
7
8
--v2
--client.py
--shell.py
--alarms.py
--event_types.py
--events.py
--meters.py
-- … …

每个版本目录下也有一个client.py和一个shell.py,这里的shell.py里面提供了do_**的函数完成从上层shell.py传进来的命令并返回结果。其它的py文件按照名称以Manager类的形式分别对应ceilometer各个资源的操作实现。client.py引用所有的Manager,这样shell.py只需要依赖client一个就可以完成对各个资源的操作。

代码分析

代码入口在 /ceilometer/shell.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def main(args=None):
try:
if args is None:
args = sys.argv[1:]

CeilometerShell().main(args)

except Exception as e:
if '--debug' in args or '-d' in args:
raise
else:
print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("Stopping Ceilometer Client", file=sys.stderr)
sys.exit(130)

if __name__ == "__main__":
main()

当用户输入ceilometerXXX-后,程序通过main函数启动。首先是解析输入的命令参数。具体实现语句self.parse_args(argv)后面会详细介绍,这里实际也对输入的合法性做了验证,如果输入关键字不对或者格式不符合,会有相应的错误信息提示并退出。如果只输入ceilometer,则会直接调用do_help函数,效果等效于输入ceilometerhelp.如果解析正常,则parse_args返回版本信息api_version和解析结果args.

如果用户输入的是ceilometer help/bash_completion,则解析后的args中的func属性就对应为do_help/do_bash_completion函数(原理后面会解释)。

如果输入的是实际的操作命令(比如ceilometermeter-list),则先会做keystone鉴权认证,故执行命令前要设置好环境变量或者在命令中加上鉴权的参数;然后再根据版本号得到ceilometer的client,最后执行args.func(client,args)完成对应命令的实现(这里会调用do_meter_list函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class CeilometerShell(object):

def __init__(self):
self.auth_plugin = ceiloclient.AuthPlugin()

def main(self, argv):
parsed = self.parse_args(argv)
if parsed == 0:
return 0
api_version, args = parsed

# Short-circuit and deal with help command right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0

if not ((self.auth_plugin.opts.get('token')
or self.auth_plugin.opts.get('auth_token'))
and self.auth_plugin.opts['endpoint']):
if not self.auth_plugin.opts['username']:
raise exc.CommandError("You must provide a username via "
"either --os-username or via "
"env[OS_USERNAME]")

if not self.auth_plugin.opts['password']:
raise exc.CommandError("You must provide a password via "
"either --os-password or via "
"env[OS_PASSWORD]")

if self.no_project_and_domain_set(args):
# steer users towards Keystone V3 API
raise exc.CommandError("You must provide a project_id via "
"either --os-project-id or via "
"env[OS_PROJECT_ID] and "
"a domain_name via either "
"--os-user-domain-name or via "
"env[OS_USER_DOMAIN_NAME] or "
"a domain_id via either "
"--os-user-domain-id or via "
"env[OS_USER_DOMAIN_ID]")

if not self.auth_plugin.opts['auth_url']:
raise exc.CommandError("You must provide an auth url via "
"either --os-auth-url or via "
"env[OS_AUTH_URL]")

client_kwargs = vars(args)
client_kwargs.update(self.auth_plugin.opts)
client_kwargs['auth_plugin'] = self.auth_plugin
client = ceiloclient.get_client(api_version, **client_kwargs)
# call whatever callback was selected
try:
args.func(client, args)
except exc.HTTPUnauthorized:
raise exc.CommandError("Invalid OpenStack Identity credentials.")

def parse_args(self, argv):
# Parse args once to find version
parser = self.get_base_parser() # 构造参数解析类ArgumentParser的实例parser,然后通过实例调用方法parser.add_argument增加一些固有的参数。其中_get_optional_kwargs会将传入的参数选项最左边的‘-’去掉并将中间的‘-’转换为‘_’。
(options, args) = parser.parse_known_args(argv) # parse_known_args函数是将解析的参数按属性的方式存储到Namespace对象。到用setattr函数,参见http://my.oschina.net/DreamG/blog/138551
self.auth_plugin.parse_opts(options)
self._setup_logging(options.debug)

# build available subcommands based on version
# 解析子命令,其中会根据版本号动态加载对应模块。函数import_versioned_module中的 module = 'glanceclient.v%s' % version。
# 默认加载glanceclient.v1.shell.py,然后通过_find_actions调用,就将所有do_XXX函数加载到self.subcommands中了
api_version = options.ceilometer_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
self.parser = subcommand_parser

# Handle top-level --help/-h before attempting to parse
# a command off the command line
if options.help or not argv:
self.do_help(options)
return 0

# Return parsed args
# 再次解析,将传入的命令和函数关联起来。如传入image-list,返回的args.func为do_image_list
return api_version, subcommand_parser.parse_args(argv)

# parse_args函数解析命令行参数之前,会先获取由argparse提供的解析器parser,这里有两类parser并分主次。主parser提供了通用的可选命令行选项,比如-h/-v/--os-username等,而subparser提供了具体的命令和选项,具体实现在get_subcommand_parser函数中。
# 说明:子解析器有多个(每个子命令对应一个),要获取每个子命令的解析器,首先根据版本号去版本目录下面获取shell模块,比如ceilometerclient.v2.shell,然后在通过_find_actions函数来创建子命令解析器。
def get_subcommand_parser(self, version):
parser = self.get_base_parser()

self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = utils.import_versioned_module(version, 'shell')
self._find_actions(subparsers, submodule)
self._find_actions(subparsers, self)
self._add_bash_completion_subparser(subparsers)

return parser

# _find_actions在相应的(shell)模块中查找满足条件的函数,这里是以do_开头作为过滤条件。然后将函数名转换成具体的子命令command(比如do_meter_list转换成meter-list),函数本身作为执行该命令的callback。然后根据command创建子命令解析器subparser。这里还要获取函数的arguments属性,作为子命令的选项,这些选项是通过@utils.arg装饰器加入到arguments中去的(详见ceilometerclient.common.utils)。arguments中的数据最终添加到子命令解析器中去,最后将callback函数作为func也加进去从而完成子命令解析器的创建和配置。
def _find_actions(self, subparsers, actions_module):
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', [])

subparser = subparsers.add_parser(command, help=help,
description=desc,
add_help=False,
formatter_class=HelpFormatter)
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)

例如

当执行ceilometermeter-list -q xxx的时候,根据上面的分析,首先会创建好commandmeter-listsubparser用来解析命令行,通过匹配最终将命令行字串解析成python对象,(比如meter-list解析成do_meter_list函数,-qxxx 解析为args.query).最终通过args.func(client,args)语句来执行do_meter_list

1
2
3
4
5
6
7
8
9
10
11
12
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean')
def do_meter_list(cc, args={}):
'''List the user's meters.'''
meters = cc.meters.list(q=options.cli_to_array(args.query))
field_labels = ['Name', 'Type', 'Unit', 'Resource ID', 'User ID',
'Project ID']
fields = ['name', 'type', 'unit', 'resource_id', 'user_id',
'project_id']
utils.print_list(meters, fields, field_labels,
sortby=0)

ccceilometerclient.通过cc.meters.list列出满足args.querymeters值,最后通过utils.print_list打印出相应的字段。

参考:

https://blog.csdn.net/joey_5566/article/details/19413015?utm_source=blogxgwz4

https://blog.csdn.net/llg8212/article/details/17954927

https://blog.csdn.net/lwyeluo/article/details/53260914?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.channel_param

-------------本文结束 感谢您的阅读-------------
作者Magiceses
有问题请 留言 或者私信我的 微博
满分是10分的话,这篇文章你给几分