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 中同名选项的优先级?

argparse

在阅读 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.py

​oslo_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 传递就会显得那么的轻松与优雅.