OpenStack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解
OpenStack 实现技术分解 (2) 虚拟机初始化工具 — Cloud-Init & metadata & userdata
OpenStack 实现技术分解 (3) 开发工具 — VIM & dotfiles
OpenStack 实现技术分解 (4) 通用技术 — TaskFlow
OpenStack 实现技术分解 (5) 应用开发 — 使用 OpenStackClients 进行二次开发
OpenStack 实现技术分解 (6) 通用库 — oslo_log
oslo.config — oslo.config 3.23.1.dev1 documentation
argparse - 命令行选项与参数解析(译)
Openstack Oslo.config 学习
(摘自官方文档) An OpenStack library for parsing configuration options from the command line and configuration files.
oslo.config 是一个 OpenStack 通用库, 用于解析并加载来自于 CLI 或配置文件中所提供的参数项或配置项.
oslo.config 属于 OpenStack 中复用率最高的通用库之一, 在网上能够轻易的找到其使用方法与示例, 并且在 oslo.config 的源码中(oslo_config/cfg.py
)也给出了丧心病狂的 443 行使用文档以及大量的注释, 其详尽程度简直前所未见. 所以本篇不再赘述 oslo.config 的使用方法, 主要介绍并记录 oslo.config 的源码实现, 使用技巧及其 单例特性.
本质上, oslo.config 是对 Python’s standard argparse library 标准库的封装, 主要解决了下列两个问题:
- 如何将配置文件中的配置项与 CLI 中的参数项进行了统一管理?
- 如何处理配置文件中与 CLI 中同名选项的优先级?
在阅读 oslo.config 源码之前, 首先要对 argparse 标准库有一定的了解. 准确来说我们需要对 argparse 定义的概念对象以及 argparse 的使用流程有所了解, 因为 oslo.config 仍然沿用了这些概念对象.
- parser(解析器): 使用 argparse 的第一步就是创建一个解析器对象, 并由解析器对象来完成指令行选项清单的预设与解析.
import argparse
# 创建解析器对象 parser
parser = argparse.ArgumentParser(add_help=True, description='This is a demo of argparse library.')
# 调用 argparse.ArgumentParser.add_argument method 预设指令行选项清单
# 只有在为解析器预设了指令行选项清单后, 解析器才能判别并接收解析指定的指令行选项
parser.add_argument("--show", "-s", desc="show", action="store", help="show message", default="You Know")
NOTE 1: 当参数 add_help=True 时, 会自动生成帮助手册. 即隐式的添加了 -h 选项
NOTE 2: argparse.ArgumentParser.add_argument
method 的原型为 add_argument(self, *args, **kwargs)
, 其中 args 元组冗余形参用于接收指令行选项的使用样式等信息, kwargs 字典冗余形参用于接收指令行选项对应的 dest/action/help/default 等功能定义.
action(动作): 如上例所示, 在向解析器添加指令行选项清单时, 需要指定在解析该指令行选项时触发的动作(action), 常用的 action 有
store/store_const/append/version
等, 具体含义请浏览扩展阅读.namespace(命名空间): 预设完指令行选项清单之后, 就可以从 CLI 接收选项输入并对其进行解析, 这需要调用
argparse.ArgumentParser.parse_args
method 来实现. 该 method 的实参缺省从sys.argv[1:]
(CLI 输入) 中获取, 但你也可以传递自己定义的参数列表, E.G.argparse.ArgumentParser.parse_args(['-s', 'Helloword'])
. 该 method 返回的对象就是 namespace 对象, namespace 对象中就以属性的形式保存了指令行选项解析后的值. 所以可以通过 namespace 对象来检索相应选项在 CLI 中指定的 values.
nsp = parser.parse_args(sys.argv[1:])
EXAMPLE:
import sys
import argparse
parser = argparse.ArgumentParser(add_help=True, description='This is a demo of argparse library.')
parser.add_argument('--show', '-s', action="store", help="show message", default="You Know")
nsp = parser.parse_args(sys.argv[1:])
print(nsp.show)
print('=' * 20)
print nsp
Output:
$ python test.py
You Know
====================
Namespace(show='You Know')
$ python test.py -h
usage: test.py [-h] [--show SHOW]
This is a demo of argparse library.
optional arguments:
-h, --help show this help message and exit
--show SHOW, -s SHOW show message
$ python test.py -s
usage: test.py [-h] [--show SHOW]
test.py: error: argument --show/-s: expected one argument
$ python test.py -s 'Hello world'
Hello world
====================
Namespace(show='Hello world')
$ python test.py --show 'Hello world'
Hello world
====================
Namespace(show='Hello world')
上述是 argparse 标准库的一个简单应用, 主要为了介绍其定义的概念对象, 更复杂的应用方式请浏览扩展阅读.
注意: 上文中反复使用了 指令行选项 这个词语, 是为了与后文中的 options 作出区分. 后文中 options 均为配置文件中的配置项与 CLI 中的参数项的统称, 因为 cfg 模块中大量应用了 OOP 的编程思想.
cfg.pyoslo_config.cfg
是 oslo.config 库的核心模块, 3274 行的代码实现了 oslo.config 提供的绝大部分功能. 其中最重要的类实现就是 Opt
和 ConfigOpts
.
class Opt
回到第一个问题: 如何将配置文件中的配置项与 CLI 中的参数项进行了统一管理?
答案就是 Opt
抽象类以及其衍生出的子类.
class Opt(object):
"""Base class for all configuration options.
The only required parameter is the option's name. However, it is
common to also supply a default and help string for all options.
从 Opt document 可以看出, Opt
是所有 configuration options 的基类, 衍生出了 IntOpt(Opt)/FloatOpt(Opt)/StrOpt(Opt)/...
等不同类型的 options 类, 对应了如下 option types:
==================================== ======
Type Option
==================================== ======
:class:`oslo_config.types.String` - :class:`oslo_config.cfg.StrOpt`
- :class:`oslo_config.cfg.SubCommandOpt`
:class:`oslo_config.types.Boolean` :class:`oslo_config.cfg.BoolOpt`
:class:`oslo_config.types.Integer` :class:`oslo_config.cfg.IntOpt`
:class:`oslo_config.types.Float` :class:`oslo_config.cfg.FloatOpt`
:class:`oslo_config.types.Port` :class:`oslo_config.cfg.PortOpt`
:class:`oslo_config.types.List` :class:`oslo_config.cfg.ListOpt`
:class:`oslo_config.types.Dict` :class:`oslo_config.cfg.DictOpt`
:class:`oslo_config.types.IPAddress` :class:`oslo_config.cfg.IPOpt`
:class:`oslo_config.types.Hostname` :class:`oslo_config.cfg.HostnameOpt`
:class:`oslo_config.types.URI` :class:`oslo_config.cfg.URIOpt`
==================================== ======
而且需要注意的是, Opt
还衍生出了 _ConfigFileOpt(Opt)
与 _ConfigDirOpt(Opt)
. 两者都实现了一个内部类 ConfigDirAction(argparse.Action)
与 ConfigFileAction(argparse.Action)
.
class ConfigFileAction(argparse.Action):
"""An argparse action for --config-file.
As each --config-file option is encountered, this action adds the
value to the config_file attribute on the _Namespace object but also
parses the configuration file and stores the values found also in
the _Namespace object.
"""
正如 ConfigFileAction document 所示, 这是一个 CLI 参数项 --config-file
的 argparse action, 这个 action 同时会解析 CLI 参数项 --config-file
并以 config_file 属性的形式添加到 namespace 对象中. 并且还会将 --config-file
选项指定的配置文件中的配置项解析后也以属性的形式添加到 namespace 中. 同理, --config-dir
也是一样的.
所以看到这里, 大家应该可以体会到, oslo.config 正是通过对 Opt
的封装与衍生出不同类型的 sub-Opt
子类来实现将 CLI 中的参数项 和 配置文件中的配置项 以 options 对象统一起来.
而且 Opt
中有几个 method 是比较重要的:
def _add_to_cli(self, parser, group=None)
def _add_to_cli(self, parser, group=None):
"""Makes the option available in the command line interface.
This is the method ConfigOpts uses to add the opt to the CLI interface
as appropriate for the opt type. Some opt types may extend this method,
others may just extend the helper methods it uses.
:param parser: the CLI option parser
:param group: an optional OptGroup object
"""
container = self._get_argparse_container(parser, group)
kwargs = self._get_argparse_kwargs(group)
prefix = self._get_argparse_prefix('', group.name if group else None)
deprecated_names = []
for opt in self.deprecated_opts:
deprecated_name = self._get_deprecated_cli_name(opt.name,
opt.group)
if deprecated_name is not None:
deprecated_names.append(deprecated_name)
self._add_to_argparse(parser, container, self.name, self.short,
kwargs, prefix,
self.positional, deprecated_names)
该 method 接收一个 parser(the CLI option parser), 主要用于 Makes the option available in the command line interface. 生成可用于 CLI 的 options. 类比上文中 argparse 的应用过程, 该 method 就是做的事情就是为 argparse.ArgumentParser.add_argument
准备好用于预设 options 的 args 和 kwargs 实参.
NOTE 1: Opt._add_to_cli
method 语句块中的 container(container: an argparse._ArgumentGroup object
) 对象才是 argparse 的 parser.
NOTE 2: 在该 method 语句块中调用的 Opt._add_to_argparse
才是真正将 args 和 kwargs 传入到 argparse 的解析器对象中的实现.
NOTE 3: 最后可以通过 Opt._get_from_namespace
来从命名空间中获取 options 的值.
class ConfigOpts
第二个问题: 如何处理配置文件中与 CLI 中同名选项的优先级?
答案就是: ConfigOpts
class ConfigOpts(collections.Mapping):
"""Config options which may be set on the command line or in config files.
ConfigOpts is a configuration option manager with APIs for registering
option schemas, grouping options, parsing option values and retrieving
the values of options.
It has built-in support for :oslo.config:option:`config_file` and
:oslo.config:option:`config_dir` options.
"""
从 ConfigOpts document 中的「ConfigOpts is a configuration option manager with APIs for registering option schemas, grouping options, parsing option values and retrieving the values of options.」可以看出, class ConfigOpts 是一个提供了 注册 option 与 option group, 解析 options, 检索 options 值 的管理接口. 所以同样由 ConfigOpts
来负责处理配置文件中与 CLI 中同名选项的优先级.
先来看 oslo.config 在程序中的使用例子, EXAMPLE:
from config import cfg
opts = [
cfg.StrOpt('bind_host', default='0.0.0.0'),
]
cli_opts = [
cfg.IntOpt('bind_port', default=9292),
]
CONF = cfg.CONF
CONF.register_opts(opts) # 此时没有注册任何 CLI 参数项或配置文件配置项, 仅仅注册了一些动态 options
CONF.register_cli_opts(cli_opts) # 注册 CLI 参数项
print CONF.bind_host
print CONF.bind_port
CONF(args=sys.argv[1:], default_config_files=['./test.conf']) # 接收 CLI 参数项与配置文件配置项
print CONF.bind_host
print CONF.bind_port
Output 1: 没有传递 CLI 参数项, 并且 test.conf 没有写入配置项
$ python test.py
0.0.0.0
9292
0.0.0.0
9292
可以看见当没有指定任何配置文件或 CLI 选项时, 只解析了动态定义的 options
Output 2: 没有传递 CLI 参数项, 但有写入 test.conf 配置项 bind_host/bind_port
$ python test.py
0.0.0.0
9292
192.168.0.1
55553
配置文件中的配置项会覆盖动态定义的 options.
Output 3: 有传递 CLI 参数项, 也有写入 test.conf 配置项 bind_host/bind_port
$ python test.py --config-file ./test.conf --bind_port 9090
0.0.0.0
9292
192.168.0.1
9090
CLI 参数项会覆盖配置文件中的配置项.
所以有两点是我们在使用 oslo.config 时需要注意的:
- 优先级: CLI 参数项 > 配置文件配置项 > 动态定义的 options
- 如果需要接收并解析最新的 CLI 参数项时, 需要实现 CLI 注册语句
CONF.register_cli_opts(cli_opts)
并且 传入 CLI 参数项值args=sys.argv[1:]
CONF 对象的单例模式
大家如果有使用 osls.config 的经历, 一定会有一个疑问: 为什么在不同模块中添加的 options 值能够通过 CONF 对象实现共用?
答案是: 其实没有什么共用一说, 因为在整个应用程序中的 CONF 对象都可能是同一个对象. 因为实例化 CONF 对象的 ConfigOpt 类应用了由 import
语句支持的单例模式.
先看一个例子, EXAMPLE:
- vim test.py
class ConfigOpts(object):
def foo(self):
pass
CONF = ConfigOpts()
- vim test_1.py
from test import CONF
print __name__, CONF, id(CONF)
- vim test_2.py
from test import CONF
import test_1
print __name__, CONF, id(CONF)
Output:
$ python test_2.py
test_1 <test.ConfigOpts object at 0x7fd347eec710> 140545421657872
__main__ <test.ConfigOpts object at 0x7fd347eec710> 140545421657872
可以看见在模块 test_1 和 test_2 中生成的 CONF 对象在内存中是同一个对象, 这是 Python 的特性决定的 作为 Python 的模块是天然的单例模式. 只要多个模块之间有互相 import
的实现, 那么这个模块之间所生成的类实例化对象就是同一个内存对象.
再回头看看 cfg 模块中的 CONF 对象是如何实例化的:
CONF = ConfigOpts()
这条语句是顶格代码, 所以当在程序中执行 from oslo_config import cfg
的时候, 就会实例化 class ConfigOpts 的实例对象 CONF, 满足单例模式的实现. 所以程序员在使用 oslo.config 的过程中, 会感到非常的便利, 因为整个程序中的 CONF 都可能是同一个 CONF, 那么程序之中的 options 传递就会显得那么的轻松与优雅.