OpenStack配置模块oslo_config – 源码解析

By | 2022年4月26日
目录
[隐藏]

在上一篇文章中已经学习了如何使用oslo_config模块,再通过这篇文章跟大家讲解一下它的实现原理,丛里到外一层层剥开这个模块的面纱。方便大家更轻松的学习OpenStack源代码。

源码分析

oslo_config模块的核心代码在类ConfigOpts中,代码中提供了相关的注释:

"""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.

"""

从中可以知道这个类是用来管理配置项的,它对外提供了一些API用来:注册选项和选项组、解析选项的值、取得选项的值。
由于在oslo_config的cfg模块中最外层有这么一行代码:

CONF = ConfigOpts()

当我们导入cfg模块的时候就会执行到这部分代码:

from oslo_config import cfg

所以就相当于导入的时候自动创建了一个ConfigOpts对象CONF,所以导入之后的cfg.CONF其实就是ConfigOpts对象的引用。
在讲它的底层实现之前先了解一下它的相关属性:

    def __init__(self):
        """Construct a ConfigOpts object."""
        self._opts = {}  # dict of dicts of (opt:, override:, default:)
        self._groups = {}

        self._args = None

        self._oparser = None
        self._namespace = None
        self._mutable_ns = None
        self._mutate_hooks = set([])
        self.__cache = {}
        self._config_opts = []
        self._cli_opts = collections.deque()
        self._validate_default_values = False
  • _opts: 用来保存所有注册的选项,包括:所有的命令行选项和配置文件中的DEFAULT分组的配置项
  • _groups:用来保存所有注册的配置组(OptGroup对象),配置组中也有_opts属性,配置文件中该组对应的配置项注册时是保存在该组下的_opts属性中。
  • _args: 用来保存传入的命令行参数选项
  • _cli_opts:只用来保存注册的命令行选项
  • _oparser:提供解析方法的对象

下面我们就从注册开始来查看它的底层实现。

1.注册命令行选项

注册命令行选项的方法为cfg.CONF.register_cli_opts(cli_opts),代码如下所示:

    @__clear_cache
    def register_cli_opts(self, opts, group=None):
        """Register multiple CLI option schemas at once."""
        for opt in opts:
            self.register_cli_opt(opt, group, clear_cache=False)

依次遍历待注册的opts列表,并对每个opt对象调用register_cli_opt方法:

    @__clear_cache
    def register_cli_opt(self, opt, group=None):
        """Register a CLI option schema.

        CLI option schemas must be registered before the command line and
        config files are parsed. This is to ensure that all CLI options are
        shown in --help and option validation works as expected.

        :param opt: an instance of an Opt sub-class
        :param group: an optional OptGroup object or group name
        :return: False if the opt was already registered, True otherwise
        :raises: DuplicateOptError, ArgsAlreadyParsedError
        """
        # 命令行选项必须在命令行和文件被解析之前进行,如果已经有选项被解析,则
        # 禁止注册命令行选项
        if self._args is not None:
            raise ArgsAlreadyParsedError("cannot register CLI option")

        return self.register_opt(opt, group, cli=True, clear_cache=False)

首先判断了选项注册的条件:命令行选项必须在命令行和文件被解析之前进行,如果已经有选项被解析,则禁止注册命令行选项。这个限制也是命令行选项注册与文件配置项注册之间的不同,文件配置项的注册可以在解析之后继续操作,而命令行选项则必须在解析之前完成,一旦解析开始,则不再允许注册命令行选项。之后调用了register_opt方法,且参数cli=True,该方法如下所示:

    @__clear_cache
    def register_opt(self, opt, group=None, cli=False):
        """Register an option schema.

        Registering an option schema makes any option value which is previously
        or subsequently parsed from the command line or config files available
        as an attribute of this object.

        :param opt: an instance of an Opt sub-class
        :param group: an optional OptGroup object or group name
        :param cli: whether this is a CLI option
        :return: False if the opt was already registered, True otherwise
        :raises: DuplicateOptError
        """
        if group is not None:
            group = self._get_group(group, autocreate=True)
            if cli:
                self._add_cli_opt(opt, group)
            return group._register_opt(opt, cli)

        if cli:
            self._add_cli_opt(opt, None)

        if _is_opt_registered(self._opts, opt):
            return False

        self._opts[opt.dest] = {'opt': opt, 'cli': cli}

        return True

在register_opt方法中首先判断了注册选项是否指定了group,如果指定了会通过_get_group(group, autocreate=True)这个方法去获取这个group对象,autocreate=True参数会使得:如果获取的group不存在会自动去注册该gruop。
我们来看一下_get_group这个方法:

    def _get_group(self, group_or_name, autocreate=False):
        """Looks up a OptGroup object.

        Helper function to return an OptGroup given a parameter which can
        either be the group's name or an OptGroup object.

        The OptGroup object returned is from the internal dict of OptGroup
        objects, which will be a copy of any OptGroup object that users of
        the API have access to.

        If autocreate is True, the group will be created if it's not found. If
        group is an instance of OptGroup, that same instance will be
        registered, otherwise a new instance of OptGroup will be created.

        :param group_or_name: the group's name or the OptGroup object itself
        :param autocreate: whether to auto-create the group if it's not found
        :raises: NoSuchGroupError
        """
        group = group_or_name if isinstance(group_or_name, OptGroup) else None
        group_name = group.name if group else group_or_name

        if group_name not in self._groups:
            if not autocreate:
                raise NoSuchGroupError(group_name)

            self.register_group(group or OptGroup(name=group_name))

        return self._groups[group_name]

_groups是ConfigOpts对象的一个属性,所有已经注册的group都会保存在_groups中,它以字典的形式存在,在这个字典中group的名称作为这个字典的key,group对象为这个字典的value。在_get_group方法中如果_groups中没有指定的group,且参数autocreate为True,则会去注册该group。注册group实质上就是将对应的OptGroup对象拷贝一份保存到_groups中。
回到register_opt方法中来:

    @__clear_cache
    def register_opt(self, opt, group=None, cli=False):
        """Register an option schema.

        Registering an option schema makes any option value which is previously
        or subsequently parsed from the command line or config files available
        as an attribute of this object.

        :param opt: an instance of an Opt sub-class
        :param group: an optional OptGroup object or group name
        :param cli: whether this is a CLI option
        :return: False if the opt was already registered, True otherwise
        :raises: DuplicateOptError
        """
        if group is not None:
            group = self._get_group(group, autocreate=True)
            if cli:
                self._add_cli_opt(opt, group)
            return group._register_opt(opt, cli)

        if cli:
            self._add_cli_opt(opt, None)

        if _is_opt_registered(self._opts, opt):
            return False

        self._opts[opt.dest] = {'opt': opt, 'cli': cli}

        return True

上面说到如果注册选项指定了group,通过_get_group方法获取到group对象之后就会将opt注册到这个group对象中。上面说到过group对象中存在一个_opts属性,注册的opt将会保存在这个_opts属性中。但是在注册进group对象之前,如果是命令行选项则会调用_add_cli_opt这个方法,将该选项加到ConfigOpts的_cli_opts这个属性中。

如果没有指定group,一样的用_add_cli_opt这个方法把命令行选项加到_cli_opts属性中,与gruop不同的是选项会被加到ConfigOpts自己的_opts属性中(就是DEFAULT组)

2.注册配置文件的选项

注册配置文件中的选项的方法是register_opts,它是直接调用了register_opt方法,与命令行选项的注册相比,少了判断当前是否已经解析(见register_cli_opt方法中的if self._args is not None这个判断),也就是说配置文件选项注册可以在解析开始之后操作。register_opt之后的流程可以参照上面命令行的注册。

    @__clear_cache
    def register_opts(self, opts, group=None):
        """Register multiple option schemas at once."""
        for opt in opts:
            self.register_opt(opt, group, clear_cache=False)

3.解析选项

先看一下模块执行解析的调用:

cfg.CONF(default_config_files=['/etc/example.conf'])

上文中有说过cfg.CONF是ConfigOpts对象的一个引用,那为啥可以把这个对象当方法一样来调用呢,这是因为在ConfigOpts类中实现了__call__方法,这个方法把ConfigOpts对象变成了一个可调用对象,感兴趣的同学可以自己多去了解一下。当有上面形式的调用时就会执行这个__call__方法,我们一起来看一下这个方法:

    def __call__(self,
                 args=None,
                 project=None,
                 prog=None,
                 version=None,
                 usage=None,
                 default_config_files=None,
                 validate_default_values=False):
        """Parse command line arguments and config files.

        Calling a ConfigOpts object causes the supplied command line arguments
        and config files to be parsed, causing opt values to be made available
        as attributes of the object.

        The object may be called multiple times, each time causing the previous
        set of values to be overwritten.

        Automatically registers the --config-file option with either a supplied
        list of default config files, or a list from find_config_files().

        If the --config-dir option is set, any *.conf files from this
        directory are pulled in, after all the file(s) specified by the
        --config-file option.

        :param args: command line arguments (defaults to sys.argv[1:])
        :param project: the toplevel project name, used to locate config files
        :param prog: the name of the program (defaults to sys.argv[0] basename)
        :param version: the program version (for --version)
        :param usage: a usage string (%prog will be expanded)
        :param default_config_files: config files to use by default
        :param validate_default_values: whether to validate the default values
        :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError,
                 ConfigFilesPermissionDeniedError,
                 RequiredOptError, DuplicateOptError
        """
        self.clear()

        self._validate_default_values = validate_default_values

        prog, default_config_files = self._pre_setup(project,
                                                     prog,
                                                     version,
                                                     usage,
                                                     default_config_files)

        self._setup(project, prog, version, usage, default_config_files)

        self._namespace = self._parse_cli_opts(args if args is not None
                                               else sys.argv[1:])
        if self._namespace._files_not_found:
            raise ConfigFilesNotFoundError(self._namespace._files_not_found)
        if self._namespace._files_permission_denied:
            raise ConfigFilesPermissionDeniedError(
                self._namespace._files_permission_denied)

        self._check_required_opts()

self.clear()用来清除自身的状态,其实就是把一些属性恢复到了默认的状态。然后调用了_pre_setup()方法,该方法用来初始化解析用的解析对象_CachedArgumentParser,最后返回prog合default_config_files,代码参考下面:

    def _pre_setup(self, project, prog, version, usage, default_config_files):
        """Initialize a ConfigCliParser object for option parsing."""

        if prog is None:
            prog = os.path.basename(sys.argv[0])

        if default_config_files is None:
            default_config_files = find_config_files(project, prog)

        self._oparser = _CachedArgumentParser(prog=prog, usage=usage)
        self._oparser.add_parser_argument(self._oparser,
                                          '--version',
                                          action='version',
                                          version=version)

        return prog, default_config_files

prog的值通过代码可以知道是启动脚本的名称,就像上文中举的neutron server服务启动的例子,它的启动脚本是neutron-server,所以prog的值就是neutron-server。
紧接着,如果参数中没有指定default_config_files,就会去自动通过方法find_config_files去查找配置文件,查找配置文件动作可以分成两种情况,如下所示,具体的代码感兴趣的同学可以自己去看,这里就不展示了。

  • 指定了project参数:

    • "/usr/share/${project}/"中查找${project}-dist.conf文件
    • "/usr/share/${project}/"中查找${prog}-dist.conf文件
    • 按照顺序依次从"~/.${project}/", "~/", "/etc/${project}/", "/etc/" 四个路径中查找${prog}.conf,取找到的第一个配置文件。
  • 没有指定project参数:

    • 按照顺序依次从"~/", "/etc/"两个路径中查找${prog}.conf,取找到的第一个配置文件。

然后将找到的配置文件组成一个list返回,重复一遍,必须是没有指定默认配置文件的情况下才会自动去查找,如果指定了就直接使用指定的配置文件列表了。

之后就是创建了一个_CachedArgumentParser对象并保存为属性_oparser,_CachedArgumentParser其实就是继承了argparse的ArgumentParser。argparse是python用于解析命令行参数和选项的标准模块。这个模块的使用可以参照官方文档:https://docs.python.org/3/library/argparse.html。 argparse的使用简单的说就是:先创建一个解析对象即ArgumentParser对象;然后向该对象中添加你要关注的命令行参数和选项(使用add_argument方法),每一个add_argument方法对应一个你要关注的参数或选项;最后调用parse_args()方法进行解析。

创建完_CachedArgumentParser对象后紧接着就调用了它的add_parser_argument方法,将–version这个参数临时保存在_CachedArgumentParser对象中等待解析。_pre_setup方法就执行结束,回到__call__方法后就开始调用_setup方法。

    def _setup(self, project, prog, version, usage, default_config_files):
        """Initialize a ConfigOpts object for option parsing."""

        self._config_opts = self._make_config_options(default_config_files)
        self.register_cli_opts(self._config_opts)

        self.project = project
        self.prog = prog
        self.version = version
        self.usage = usage
        self.default_config_files = default_config_files

在这个方法中一上来就注册了上文中讲到的config-file和config-dir,并且将他们注册为了命令行选项,这样就能从外部读取这两个选项的值了。后面就是简单的一些赋值操作。
_setup方法结束后就开始真正的解析操作:_parse_cli_opts,请跟我一起来看一下这个流程。

    def _parse_cli_opts(self, args):
        """Parse command line options.

        Initializes the command line option parser and parses the supplied
        command line arguments.

        :param args: the command line arguments
        :returns: a _Namespace object containing the parsed option values
        :raises: SystemExit, DuplicateOptError
                 ConfigFileParseError, ConfigFileValueError

        """
        self._args = args
        for opt, group in self._all_cli_opts():
            opt._add_to_cli(self._oparser, group)

        return self._parse_config_files()

这里的参数args就是传进来的命令行选项。
于是这里又可以分为命令行选项的解析和配置文件的解析,下面我们为了清楚就分开说明。

3.1 命令行解析选项

        for opt, group in self._all_cli_opts():
            opt._add_to_cli(self._oparser, group)

这两行代码遍历了所有已经注册的命令行选项,并对每个选项都调用了选项自身的_add_to_cli方法:

    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)

方法中首先获取了三个对象,分别是container、kwargs、prefix。其中

  • container: 解析用对象
  • kwargs:argparse对象调用add_argument方法时的参数字典
    这里需要看一下container的获取:

      def _get_argparse_container(self, parser, group):
          """Returns an argparse._ArgumentGroup.
    
          :param parser: an argparse.ArgumentParser
          :param group: an (optional) OptGroup object
          :returns: an argparse._ArgumentGroup if group is given, else parser
          """
          if group is not None:
              return group._get_argparse_group(parser)
          else:
              return parser

    如果指定了group对象就会返回一个argparse._ArgumentGroup对象,并将这个对象通过self._oparser的add_argument_group方法添加到._oparser对象中,否则就返回_oparser对象。这步的理解简单的讲就是利用了argparse的群组功能。因为上文中已经讲到了self._oparser其实就是继承自argparse的ArgumentParser。

获取到三个对象后会调用_add_to_argparse方法,其底层实现就是调用了parser.add_parser_argument方法。到这里,根据上文中讲的argparse的使用方法,就只差最后的一步:parse_args()方法的调用就能完成命令行选项的解析。

那最后一步在哪里调用的呢?
回到_parse_cli_opts方法中,可以看到接下来会调用self._parse_config_files()方法,进入该方法:

    def _parse_config_files(self):
        """Parse configure files options.

        :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError,
                 ConfigFilesPermissionDeniedError,
                 RequiredOptError, DuplicateOptError
        """
        namespace = _Namespace(self)
        for arg in self._args:
            if arg == '--config-file' or arg.startswith('--config-file='):
                break
        else:
            for config_file in self.default_config_files:
                ConfigParser._parse_file(config_file, namespace)

        self._oparser.parse_args(self._args, namespace)

        self._validate_cli_options(namespace)

        return namespace

有没有找到self._oparser.parse_args(self._args, namespace)?就是在这个方法中解析了所有的命令行选项。

3.2 配置文件解析

配置文件解析的话直接就在方法_parse_config_files中,把上面那段代码拷贝下来:

    def _parse_config_files(self):
        """Parse configure files options.

        :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError,
                 ConfigFilesPermissionDeniedError,
                 RequiredOptError, DuplicateOptError
        """
        namespace = _Namespace(self)
        for arg in self._args:
            if arg == '--config-file' or arg.startswith('--config-file='):
                break
        else:
            for config_file in self.default_config_files:
                ConfigParser._parse_file(config_file, namespace)

        self._oparser.parse_args(self._args, namespace)

        self._validate_cli_options(namespace)

        return namespace

开始的时候判断有没有指定–config-file,如果没有就利用ConfigParser解析所有的默认配置文件,ConfigParser是Python读取conf配置文件标准的库。
如果指定了–config-file,配置文件的解析就交给argparse去做了,因为代码中为–config-file指定了action:ConfigFileAction

    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.
        """

        def __call__(self, parser, namespace, values, option_string=None):
            """Handle a --config-file command line argument.

            :raises: ConfigFileParseError, ConfigFileValueError
            """
            if getattr(namespace, self.dest, None) is None:
                setattr(namespace, self.dest, [])
            items = getattr(namespace, self.dest)
            items.append(values)

            ConfigParser._parse_file(values, namespace)

    def __init__(self, name, **kwargs):
        super(_ConfigFileOpt, self).__init__(name, lambda x: x, **kwargs)

可以看到ConfigFileAction中也是用的ConfigParser来解析的。

至此,命令行选项和配置文件解析就完成了,后面剩下一下解析后的验证和判断就不讲了。

4. 读取配置项的值

当解析完成之后,所有解析的值都会保存在ConfigOpts的_namespace属性中,当我们获取配置项的值的时候,例如cfg.CONF.debug(获取debug选项的值)会调用getattr方法

    def __getattr__(self, name):
        """Look up an option value and perform string substitution.

        :param name: the opt name (or 'dest', more precisely)
        :returns: the option value (after string substitution) or a GroupAttr
        :raises: ValueError or NoSuchOptError
        """
        try:
            return self._get(name)
        except ValueError:
            raise
        except Exception:
            raise NoSuchOptError(name)

转而调用_get方法:

    def _get(self, name, group=None, namespace=None):
        if isinstance(group, OptGroup):
            key = (group.name, name)
        else:
            key = (group, name)
        if namespace is None:
            try:
                return self.__cache[key]
            except KeyError:  # nosec: Valid control flow instruction
                pass
        value = self._do_get(name, group, namespace)
        self.__cache[key] = value
        return value

在这个方法中会从self.__cache中获取值,如果没有对应的key则会调用self._do_get方法:

    def _do_get(self, name, group=None, namespace=None):
        """Look up an option value.

        :param name: the opt name (or 'dest', more precisely)
        :param group: an OptGroup
        :param namespace: the namespace object to get the option value from
        :returns: the option value, or a GroupAttr object
        :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError,
                 TemplateSubstitutionError
        """
        if group is None and name in self._groups:
            return self.GroupAttr(self, self._get_group(name))

        info = self._get_opt_info(name, group)
        opt = info['opt']

        if isinstance(opt, SubCommandOpt):
            return self.SubCommandAttr(self, group, opt.dest)

        if 'override' in info:
            return self._substitute(info['override'])

        def convert(value):
            return self._convert_value(
                self._substitute(value, group, namespace), opt)

        if opt.mutable and namespace is None:
            namespace = self._mutable_ns
        if namespace is None:
            namespace = self._namespace
        if namespace is not None:
            group_name = group.name if group else None
            try:
                return convert(opt._get_from_namespace(namespace, group_name))
            except KeyError:  # nosec: Valid control flow instruction
                pass
            except ValueError as ve:
                raise ConfigFileValueError(
                    "Value for option %s is not valid: %s"
                    % (opt.name, str(ve)))

        if 'default' in info:
            return self._substitute(info['default'])

        if self._validate_default_values:
            if opt.default is not None:
                try:
                    convert(opt.default)
                except ValueError as e:
                    raise ConfigFileValueError(
                        "Default value for option %s is not valid: %s"
                        % (opt.name, str(e)))

        if opt.default is not None:
            return convert(opt.default)

        return None

先找到name对应的Opt对象,然后调用该对象的_get_from_namespace方法:

    def _get_from_namespace(self, namespace, group_name):
        """Retrieves the option value from a _Namespace object.

        :param namespace: a _Namespace object
        :param group_name: a group name
        """
        names = [(group_name, self.dest)]
        current_name = (group_name, self.name)

        for opt in self.deprecated_opts:
            dname, dgroup = opt.name, opt.group
            if dname or dgroup:
                names.append((dgroup if dgroup else group_name,
                              dname if dname else self.dest))

        value = namespace._get_value(
            names, multi=self.multi,
            positional=self.positional, current_name=current_name)
        # The previous line will raise a KeyError if no value is set in the
        # config file, so we'll only log deprecations for set options.
        if self.deprecated_for_removal and not self._logged_deprecation:
            self._logged_deprecation = True
            pretty_group = group_name or 'DEFAULT'
            LOG.warning('Option "%s" from group "%s" is deprecated for '
                        'removal.  Its value may be silently ignored in the '
                        'future.', self.dest, pretty_group)
        return value

从对应的namespace中获取对应的值。

Category: 未分类 标签:

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注