openstack client源码分析(基于python-monitorclient)

《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发出去了~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值