Openstack版本为Mitaka,IDE用的是Pycharm远程调试。这篇文章主要研究了Openstack command-line clients。
介绍
主要是研究manilaclient的指令调用过程。也就是当我们在shell里输入了一个manila指令之后,后台到底发生了什么,整个调用的过程是怎么样的,真正干活的人是谁。
Openstack命令行指令详见OpenStack command-line clients. http://docs.openstack.org/user-guide/cli.html
我们通过manila创建共享网络的指令来进行测试, 具体的指令及运行效果如下:
[stack@localhost devstack]$ manila share-network-create --neutron-net-id d736b408-125b-41c1-8499-8fefd8c4c9cf --neutron-subnet-id b91adb44-4acb-4611-8caf-8a6903a44f24 --name TestShareNetwork
+-------------------+--------------------------------------+
| Property | Value |
+-------------------+--------------------------------------+
| name | TestShareNetwork |
| segmentation_id | None |
| created_at | 2016-08-15T02:49:15.222359 |
| neutron_subnet_id | b91adb44-4acb-4611-8caf-8a6903a44f24 |
| updated_at | None |
| network_type | None |
| neutron_net_id | d736b408-125b-41c1-8499-8fefd8c4c9cf |
| ip_version | None |
| nova_net_id | None |
| cidr | None |
| project_id | adc9e46e02fd4a909dbbea278e202f10 |
| id | cb4704b6-6857-498d-9711-e73c5630e714 |
| description | None |
+-------------------+--------------------------------------+
开始
本例中, manilaclient被默认安装在/opt/stack/目录下.
/opt/stack/python-manilaclient/manilaclient
|---client.py------OpenStack Client interface. Handles the REST calls and responses.
|---base.py------Base utilities to build API operation managers and objects on top of. 提供基本的Manager基类
|---shell.py------命令解析,创建相应版本的Client类对象,调用相应版本的shell.py中的函数
......
|---v2
|---client.py------版本Client类,拥有一系列Manager类对象,这些Manager可以调用相应的组件
|---flavors.py------具体的Manager类,使用HTTPClient对象与对应的组件进行通信
......
|---shell.py------提供每个Command对应的方法
找到API入口
首先查看manila API入口:
[root@localhost keystoneclient]# which manila | xargs -I{} file {}
/usr/bin/manila: Python script, ASCII text executable
[root@localhost ~]# cat /usr/bin/manila
#!/usr/bin/python
# PBR Generated from u'console_scripts'
import sys
from manilaclient.shell import main
if __name__ == "__main__":
sys.exit(main())
可以看出, manila指令实际上就是一个Python文件. 查看该文件发现, 真正干活的人在manilaclient.shell的main()函数里.
pycharm远程调试配置
详见: Remote debug with a Python Debug Server http://www.jetbrains.com/help/pycharm/2016.1/remote-debugging.html#6
$easy_install pycharm-debug.egg
为了通过宿主机的pycharm对manila代码进行远程调试, 在pycharm中配置远程debug如下:
其中, Local host name为宿主机的IP地址, 相当于在宿主机中部署了一个debug server. 当VM执行代码到pydevd.settrace(…)时, 就会去连接这个debug server. 因此, 在所有需要打断点的位置插入pydevd.settrace(…)即可.
相应地, 修改文件 /usr/bin/manila 如下:
#!/usr/bin/python
# PBR Generated from u'console_scripts'
import sys
from manilaclient.shell import main
import pydevd
if __name__ == "__main__":
pydevd.settrace('192.168.137.1', port=51234, stdoutToServer=True, stderrToServer=True)
sys.exit(main())
为了便于修改代码, 可以同时在pycharm中配置一个Deployment. 这样就可以在本地修改代码并同步到VM中. 编辑Tools->Deployment->Configuration…如下:
同时修改Tools->Deployment->Options…如下:
这样每次修改完文件, 就不用手动去选择Upload to Centos7_base_testnetwork. 通过Ctrl+S就会自动同步本地代码到VM.
指令参数解析
回到代码中, 我们已经知道真正干活的人在manilaclient.shell的main()函数里. manilaclient.shell即 /opt/stack/python-manilaclient/manilaclient/shell.py文件. 这是devstack默认的安装manilaclient的位置. 查看其代码如下:
# /opt/stack/python-manilaclient/manilaclient/shell.py
def main():
try:
if sys.version_info >= (3, 0):
OpenStackManilaShell().main(sys.argv[1:]) # 在这里
else:
OpenStackManilaShell().main(
map(encodeutils.safe_decode, sys.argv[1:]))
except KeyboardInterrupt:
print("... terminating manila client", file=sys.stderr)
sys.exit(130)
except Exception as e:
logger.debug(e, exc_info=1)
message = e.message
if not isinstance(message, six.string_types):
message = str(message)
print("ERROR: %s" % encodeutils.safe_encode(message), file=sys.stderr)
sys.exit(1)
显然, 命令的参数sys.argv[1:]又进一步被甩给了OpenStackManilaShell类的main()函数, 这才是真正的入口main()函数.
OpenStackManilaShell类的main()函数的前半部分, 对命令行参数进行解析,此处用到了argparse的相关知识, 具体请参考argparse — Parser for command-line options, arguments and sub-commands. 同时还进行了各种参数和环境变量的读取, 这里不深入分析. 只需要知道, 最终我们得到了一个字典client_args, 其值如下:
至此, 准备工作算是告一段落, 下面开始要真正开始干活了. OpenStackManilaShell类的main()函数后半部分代码如下:
# /opt/stack/python-manilaclient/manilaclient/shell.py
from manilaclient import client
class OpenStackManilaShell(object):
def main(self, argv):
......(指令参数解析得到{dict}client_args)
# This client is needed to discover the server api version.
temp_client = client.Client(manilaclient.API_MAX_VERSION,
**client_args) # 2
# 构造一个Client对象,具体的Client会根据版本创建
self.cs, discovered_version = self._discover_client(temp_client,
os_api_version,
os_endpoint_type,
os_service_type,
client_args) # 3
args = self._build_subcommands_and_extensions(discovered_version,
argv,
options) # ch4
# 输入的命令为manila share-network-create, 经过对参数的解析,args.func实际表示manilaclient/v2/shell.py中的do_share_network_create()函数,调用该函数进行处理
args.func(self.cs, args) # ch5
我们分几个章节具体看看这几个函数都干了些什么.
client.Client()函数分析
这一部分我们分析这段代码:
temp_client = client.Client(manilaclient.API_MAX_VERSION,
**client_args) # 2
client.Client()本身是一个函数, 传入的参数有两个. 其中, manilaclient.API_MAX_VERSION是一个manilaclient.api_versions.APIVersion类的对象, 其值如下:
manilaclient.API_MAX_VERSION
进入到client.Client()函数, 看看这个函数到底做了些什么, 其代码如下:
# /opt/stack/python-manilaclient/manilaclient/client.py
def Client(client_version, *args, **kwargs):
def _convert_to_api_version(version):
"""Convert version to an APIVersion object unless it already is one."""
if hasattr(version, 'get_major_version'):
api_version = version
else:
if version in ('1', '1.0'):
api_version = api_versions.APIVersion(
api_versions.DEPRECATED_VERSION)
elif version == '2':
api_version = api_versions.APIVersion(api_versions.MIN_VERSION)
else:
api_version = api_versions.APIVersion(version)
return api_version
api_version = _convert_to_api_version(client_version) # 2.1
client_class = get_client_class(api_version.get_major_version()) # 2.2
# Make sure the kwarg api_version is set with an APIVersion object.
# 1st choice is to use the incoming kwarg. 2nd choice is the positional.
kwargs['api_version'] = _convert_to_api_version(
kwargs.get('api_version', api_version))
return client_class(*args, **kwargs) # 2.3
_convert_to_api_version()
应该是用来做版本控制的, 具体功能不详. ‘manilaclient.api_versions.APIVersion’这个类提供了很多比较版本大小相关的操作, 比如重写了”==”, “>” 之类的比较符的功能, 可以借此学习一下Python的运算符重载.
get_client_class()函数: 根据版本找到对应的Client类
# /opt/stack/python-manilaclient/manilaclient/client.py
from oslo_utils import importutils
def get_client_class(version):
version_map = {
'1': 'manilaclient.v1.client.Client',
'2': 'manilaclient.v2.client.Client',
}
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = "Invalid client version '%s'. must be one of: %s" % (
(version, ', '.join(version_map)))
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)
这个函数的功能是找到版本对应的代码位置, 这里的版本是大的版本号. 传入的参数version = {str} ‘2’, 进而得到client_path = {str} ‘manilaclient.v2.client.Client’.
再进一步地, 调用了importutils.import_class()函数, 其代码如下:
# /usr/lib/python2.7/site-packages/oslo_utils/importutils.py
def import_class(import_str):
"""Returns a class from a string including module and class.
.. versionadded:: 0.3
"""
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_str)
try:
return getattr(sys.modules[mod_str], class_str)
except AttributeError:
raise ImportError('Class %s cannot be found (%s)' %
(class_str,
traceback.format_exception(*sys.exc_info())))
首先, 输入的参数import_str = {str} ‘manilaclient.v2.client.Client’经过处理, 得到:
sys.modules 是一个字典, 它包含了从Python开始运行起, 被导入的所有模块. 通过getattr()函数, 返回了文件/opt/stack/python-manilaclient/manilaclient/v2/client.py里的Client类.
综上, client_class = get_client_class(api_version.get_major_version()) # 2.2这行代码, 最终得到了client_class = {type} <class ‘manilaclient.v2.client.Client’>.
最后, return client_class(*args, **kwargs) # 2.3. 也就是说, 所有的参数被甩给了manilaclient.v2.client.Client这个类, 它才是真正执行功能的类. 以上的所有操作都是为了正确找到这个类. 那么这个类到底做了些什么呢, 来看看这个类的实现.
manilaclient.v2.client.Client类
联系keystone获取用户身份
# /opt/stack/python-manilaclient/manilaclient/v2/client.py
class Client(object):
def __init__(self, username=None, api_key=None,
project_id=None, auth_url=None, insecure=False, timeout=None,
tenant_id=None, project_name=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type=constants.V2_SERVICE_TYPE, service_name=None,
retries=None, http_log_debug=False, input_auth_token=None,
session=None, auth=None, cacert=None,
service_catalog_url=None, user_agent='python-manilaclient',
use_keyring=False, force_new_token=False,
cached_token_lifetime=300,
api_version=manilaclient.API_MIN_VERSION,
user_id=None,
user_domain_id=None,
user_domain_name=None,
project_domain_id=None,
project_domain_name=None,
cert=None,
password=None,
**kwargs):
......(各种赋值代码)
# 如果有auth_token但是没有service_catalog_url, 异常
if input_auth_token and not service_catalog_url:
msg = ("For token-based authentication you should "
"provide 'input_auth_token' and 'service_catalog_url'.")
raise exceptions.ClientException(msg)
self.project_id = tenant_id if tenant_id is not None else project_id
self.keystone_client = None
self.session = session
# NOTE(u_glide): token authorization has highest priority.
# That's why session and/or password will be ignored
# if token is provided.
if not input_auth_token:
```session = None```
# 如果session不为None:
if session:
self.keystone_client = adapter.LegacyJsonAdapter(
session=session,
auth=auth,
interface=endpoint_type,
service_type=service_type,
service_name=service_name,
region_name=region_name)
input_auth_token = self.keystone_client.session.get_token(auth)
else:
# 通过keystone验证用户身份
self.keystone_client = self._get_keystone_client()
input_auth_token = self.keystone_client.auth_token
if not input_auth_token:
......
manilaclient.v2.client.Client类拿到参数之后, 首先是一大波从形参到类成员变量的赋值操作. 比如self.username = username之类. 然后检查input_auth_token和session, 发现既没有input_auth_token也没有session. 于是联系keystone进行认证, 即self.keystone_client = self._get_keystone_client().
执行这行代码等了一会, 然后拿到keystone返回来的认证信息. 具体是什么信息, 以及认证的过程, 留待其它文章再分析. 总之, 经过keystone认证之后, 我们终于是有身份的人了, 相应地, input_auth_token = u'1071653b663343fcae7841ed2325d742’.
获取service_catalog_url
# /opt/stack/python-manilaclient/manilaclient/v2/client.py
......(联系keystone获取用户身份)
# 检查input_auth_token
if not input_auth_token:
raise RuntimeError("Not Authorized")
```session = None, service_catalog_url = ''```
# 如果有session但是没有service_catalog_url
if session and not service_catalog_url:
service_catalog_url = self.keystone_client.session.get_endpoint(
auth, interface=endpoint_type,
service_type=service_type)
# 没有service_catalog_url
elif not service_catalog_url:
"""service_type = 'share'"""
# 从keystone获取share服务的endpoints
catalog = self.keystone_client.service_catalog.get_endpoints(
service_type)
for catalog_entry in catalog.get(service_type, []):
if (catalog_entry.get("interface") == (
endpoint_type.lower().split("url")[0]) or
catalog_entry.get(endpoint_type)):
# 如果region匹配不上, 即不是这个region, 直接continue
if (region_name and not region_name == (
catalog_entry.get(
"region",
catalog_entry.get("region_id")))):
continue
service_catalog_url = catalog_entry.get(
"url", catalog_entry.get(endpoint_type))
break
if not service_catalog_url:
......
虽然有了身份, 但此时还没有service_catalog_url, 不知道应该去哪里请求服务. 首先, 从keystone获取share服务的endpoints. 得到字典catelog, 它保存了不同的服务的入口, 其值如下:
逐个遍历catelog里的条目, 找到region一致的条目. 根据endpoint_type = ‘publicURL’, 最后得到service_catalog_url = u’http://192.168.137.2:8786/v1/adc9e46e02fd4a909dbbea278e202f10’. 链接最后的这一段数字不知道是什么, 后续再研究.
生成HTTPClient类实例
# /opt/stack/python-manilaclient/manilaclient/v2/client.py
from manilaclient.common import httpclient
......(获取service_catalog_url)
if not service_catalog_url:
raise RuntimeError("Could not find Manila endpoint in catalog")
self.api_version = api_version
self.client = httpclient.HTTPClient(service_catalog_url,
input_auth_token,
user_agent,
insecure=insecure,
cacert=cacert,
timeout=timeout,
retries=retries,
http_log_debug=http_log_debug,
api_version=self.api_version)
有了service_catalog_url之后, 生成了一个HTTPClient类, 其类描述如下:
HTTP Client class used by multiple clients.
The imported Requests module caches and reuses objects with the same
destination. To avoid the problem of sending duplicate requests it is
necessary that the Requests module is only imported once during client
execution. This class is shared by multiple client versions so that the
client can be changed to another version during execution.
它的作用应该就是进行HTTP连接.
在self上绑定各种Manager类
# /opt/stack/python-manilaclient/manilaclient/v2/client.py
......(生成HTTPClient类实例)
self.limits = limits.LimitsManager(self)
self.services = services.ServiceManager(self)
self.security_services = security_services.SecurityServiceManager(self)
self.share_networks = share_networks.ShareNetworkManager(self)
self.quota_classes = quota_classes.QuotaClassSetManager(self)
self.quotas = quotas.QuotaSetManager(self)
self.shares = shares.ShareManager(self)
self.share_export_locations = (
share_export_locations.ShareExportLocationManager(self))
self.share_instances = share_instances.ShareInstanceManager(self)
self.share_instance_export_locations = (
share_instance_export_locations.ShareInstanceExportLocationManager(
self))
self.share_snapshots = share_snapshots.ShareSnapshotManager(self)
self.share_types = share_types.ShareTypeManager(self)
self.share_type_access = share_type_access.ShareTypeAccessManager(self)
self.share_servers = share_servers.ShareServerManager(self)
self.share_replicas = share_replicas.ShareReplicaManager(self)
self.pools = scheduler_stats.PoolManager(self)
self.consistency_groups = (
consistency_groups.ConsistencyGroupManager(self))
self.cg_snapshots = (
cg_snapshots.ConsistencyGroupSnapshotManager(self))
self._load_extensions(extensions)
之后, 又生成了一些Manager类实例. 从这段代码可以看出, manilaclient.v2.client.Client类的角色有点像是具体功能模块的注册类. 以上代码, 如self.share_networks = share_networks.ShareNetworkManager(self)就是注册在ShareNetwork功能. 所有的功能模块类都是由base.Manager类派生出来的, base.Manager类相当于定义了功能模块标准接口, 其构造函数如下:
# /opt/stack/python-manilaclient/manilaclient/base.py
class Manager(utils.HookableMixin):
"""Manager for CRUD operations.
Managers interact with a particular type of API (shares, snapshots,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
self.client = api.client
这个例子中, self.api = manilaclient.v2.client.Client object, 也就是上面的注册类. self.client = manilaclient.common.httpclient.HTTPClient object, 也就是上面的HTTPClient类.
总结
至此, client.Client()函数结束. 它通过get_client_class()函数找到了正确版本的Client类, 在这里是manilaclient.v2.client.Client. 该类很像是功能模块的注册类:
(1) 联系keystone进行了用户身份的认证, 同时得到了服务的endpoint, 生成了HTTPClient类的实例. 这个HTTPClient类将被所有的功能模块类共用;
(2) 完成功能模块类的注册, 注册的同时各个功能模块也获得了上一步生成的HTTPClient类的实例;
(3) 注册类本身也被放到功能模块类中, 可能是为了传递之前读取的各种参数, 环境变量等.
最后, 返回该注册类manilaclient.v2.client.Client.
_discover_client()函数
辛苦得到的注册类被赋给temp_client, 居然只是临时类= =. 代码来到:
self.cs, discovered_version = self._discover_client(temp_client,
os_api_version,
os_endpoint_type,
os_service_type,
client_args) # 3
_discover_client()函数代码如下:
# /opt/stack/python-manilaclient/manilaclient/shell.py
class OpenStackManilaShell(object):
def _discover_client(self,
current_client,
os_api_version,
os_endpoint_type,
os_service_type,
client_args):
if os_api_version == manilaclient.API_DEPRECATED_VERSION:
discovered_version = manilaclient.API_DEPRECATED_VERSION
os_service_type = constants.V1_SERVICE_TYPE
else:
discovered_version = api_versions.discover_version(
current_client,
os_api_version
)
if not os_endpoint_type:
os_endpoint_type = DEFAULT_MANILA_ENDPOINT_TYPE
if not os_service_type:
os_service_type = self._discover_service_type(discovered_version)
```os_service_type = 'sharev2'```
if (discovered_version != manilaclient.API_MAX_VERSION or
os_service_type != constants.V1_SERVICE_TYPE or
os_endpoint_type != DEFAULT_MANILA_ENDPOINT_TYPE):
client_args['version'] = discovered_version
client_args['service_type'] = os_service_type
client_args['endpoint_type'] = os_endpoint_type
return (client.Client(discovered_version, **client_args),
discovered_version)
else:
return current_client, discovered_version
最后又再次调用client.Client()函数, 又返回了一个注册类manilaclient.v2.client.Client对象. 至于为什么要这么做, 新的注册类和原本的temp_client有什么区别, 后续再研究.
解析子命令
得到了”正式”的注册类self.cs之后, 至此, 我们已经有了manila的服务类对象. 但是manila这个指令有哪些子命令呢? 如这个例子中的manila share-network-create指令, share-network-create就是子命令.
args = self._build_subcommands_and_extensions(discovered_version,
argv,
options)
_build_subcommands_and_extensions()函数代码如下:
# /opt/stack/python-manilaclient/manilaclient/shell.py
class OpenStackManilaShell(object):
def _build_subcommands_and_extensions(self,
os_api_version,
argv,
options):
self.extensions = self._discover_extensions(os_api_version)
self._run_extension_hooks('__pre_parse_args__')
self.parser = self.get_subcommand_parser(
os_api_version.get_major_version()) # 4.1
# 如果是help参数, 或者没有参数, 直接打印帮助信息并退出
if options.help or not argv:
self.parser.print_help()
return False
args = self.parser.parse_args(argv) # 4.2
self._run_extension_hooks('__post_parse_args__', args)
return args
get_subcommand_parser()
这个函数的功能是根据保存了功能函数的代码文件生成所有的子命令. 该函数代码如下:
# /opt/stack/python-manilaclient/manilaclient/shell.py
from manilaclient.v2 import shell as shell_v2
class OpenStackManilaShell(object):
def get_subcommand_parser(self, version):
# 获取基本参数解析器
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
try:
actions_module = {
V2_MAJOR_VERSION: shell_v2,
}[version] # 4.1.1
except KeyError:
actions_module = shell_v2
self._find_actions(subparsers, actions_module) # 4.1.2
self._find_actions(subparsers, self)
for extension in self.extensions:
self._find_actions(subparsers, extension.module)
self._add_bash_completion_subparser(subparsers)
return parser
找到全部方法
首先, 根据版本(默认是shell_v2)找到所有可用的方法. 在这个例子中 shell_v2 = <module ‘manilaclient.v2.shell’ from ‘/opt/stack/python-manilaclient/manilaclient/v2/shell.pyc’>. 该文件中保存了所有的功能函数. 比如此例中, 我们实际上调用的就是do_share_network_create()这个功能函数. 这里我们不关心这个功能具体是怎么实现的. 重点是这种映射关系是怎么做到的.
方法映射_find_actions()函数
重点是_find_actions()函数, 以下是该方法代码:
# /opt/stack/python-manilaclient/manilaclient/shell.py
class OpenStackManilaShell(object):
def _find_actions(self, subparsers, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# 对manilaclient/v2/shell.py中的每个do_xxx函数进行处理
# 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()
# 观察novaclient/v1_1/shell.py中的do_xxx函数都使用了装饰器进行处理,而具体的处理就是为函数添加arguments属性,关于装饰器,可以参考文档:
# http://www.cnblogs.com/rhcad/archive/2011/12/21/2295507.html
# http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html
arguments = getattr(callback, 'arguments', [])
# 添加子命令解析器
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)
# 此处设置了子命令的缺省处理函数,与后面对func的调用相呼应
subparser.set_defaults(func=callback)
首先, dir(actions_module)返回了actions_module这个模块的所有attribute. 从4.1.1我们知道, actions_module实际上对应的就是/opt/stack/python-manilaclient/manilaclient/v2/shell.py这个模块. dir(actions_module)的内容如下图(右边)所示, 正好对应到/opt/stack/python-manilaclient/manilaclient/v2/shell.py模块的atrribute:
比如, 当attr的值遍历到attr = ‘do_share_network_create’时, 就添加了一个新的子命令command = ‘share-network-create’. callback保存了回调函数do_share_network_create()的信息, 如下图所示:
通过subparser.set_defaults(func=callback), 把函数作为变量赋值到func, 实现了指令和函数的绑定. 这就是所谓的反射机制, 子命令share-network-create被绑定到do_share_network_create()函数上.
根据参数找到具体的函数
完成了子命令的注册之后, 代码来到:
args = self.parser.parse_args(argv) # 4.2
回忆之前的内容, 指令参数保存在argv中, 在这个例子中, 其值如下:
通过参数解析, 得到args如下, 这就是要交给回调函数的子命令参数相关信息了:
真正干活的函数
此时, argc.func = function do_share_network_create at 0x3591ed8.
终于要调用功能了:
args.func(self.cs, args) # ch5
通过反射机制, 程序进入到do_share_network_create()函数中. 函数代码之前已经给过了, 如下:
# /opt/stack/python-manilaclient/manilaclient/v2/shell.py
def do_share_network_create(cs, args):
"""Create description for network used by the tenant."""
values = dict(
neutron_net_id=args.neutron_net_id,
neutron_subnet_id=args.neutron_subnet_id,
nova_net_id=args.nova_net_id,
name=args.name,
description=args.description)
share_network = cs.share_networks.create(**values)
info = share_network._info.copy()
cliutils.print_dict(info)
这个函数是”manila share-network-create”这个指令功能的入口函数. 其中, cs.share_networks是之前2.3.4中注册功能模块时创建的Manager:
self.share_networks = share_networks.ShareNetworkManager(self)