OpenStack配置解析库oslo.config

oslo.config模块在OpenStack中最大的作用就是配置文件以及命令行参数的解析

要了解oslo.config的工作流程,需要了解一下关于Python基础模块中的argparse模块,该模块的信息可以参考:https://docs.python.org/2/library/argparse.html

命令行参数的解析使用的是argparse基础模块中的ArgumentParser类,该类可以将命令行中的参数进行自己的处理,然后放到一个NameSpace类实例中(一个简单的字典)。

该模块中比较重要的一个类便是Action类,该类的作用就是可以将命令行参数使用指定的方法转换为所需要的数据结构。argparse中的所使用的action类型都是该类的派生类,派生类都重新实现了该类的__call__方法,在__call__方法中实现自己的命令行参数解析方法。该方法在parse_args()会被自动调用,从而将命令行中参数进行自己的处理。当然argparse模块中已经定义了一些action类,用来解析常见的命令行参数,例如_StoreAction,_StoreTrueAction等类,详细信息参考上述链接。

在olso.config模块中,有几个比较重要的类,下面介绍下这几个类。

1、types.py中的ConfigType类及其派生类。ConfigType的作用是为了区分参数类型,如下:

class ConfigType(object):
    def __init__(self, type_name='unknown type'):
        self.type_name = type_name
class Hostname(ConfigType):
    """Host domain name type.

    A hostname refers to a valid DNS or hostname. It must not be longer than
    253 characters, have a segment greater than 63 characters, nor start or
    end with a hyphen.

    :param type_name: Type name to be used in the sample config file.

    """

    def __init__(self, type_name='hostname value'):
        super(Hostname, self).__init__(type_name=type_name)
class IPAddress(ConfigType):

    """IP address type

    Represents either ipv4 or ipv6. Without specifying version parameter both
    versions are checked

    :param version: defines which version should be explicitly checked (4 or 6)
    :param type_name: Type name to be used in the sample config file.

    .. versionchanged:: 2.7

       Added *type_name* parameter.
    """

    def __init__(self, version=None, type_name='IP address value'):
        super(IPAddress, self).__init__(type_name=type_name)
        version_checkers = {
            None: self._check_both_versions,
            4: self._check_ipv4,
            6: self._check_ipv6
        }

        self.version_checker = version_checkers.get(version)
        if self.version_checker is None:
            raise TypeError("%s is not a valid IP version." % version)

从上三段代码可以得到,ConfigType派生类的差别就是type_name属性,而type_name的值就是一个字符串,如上文的"hostname value"、"IP adddress value",仅仅只是为了体现这个配置项是一个hostname还是一个IPAddress。有的甚至还有多次继承,如Port继承自Integer,Integer继承自Number,Number继承自ConfigType。这是因为Number可以是Integer类型,也可以是Float类型,这而Port又是特殊的Integer(0-65535)类型,这就是面对对象的体现。ConfigType的其他派生类可以去看源码,就不一一介绍了。

2、cfg.py中的Opt类,该类的意义就是为了表示各个配置项(Key-Value),该类也有很多子类,例如IPOpt、IntOpt等,如下所示:

class IntOpt(Opt):

    r"""Option with Integer type

    Option with ``type`` :class:`oslo_config.types.Integer`

    :param name: the option's name
    :param min: minimum value the integer can take
    :param max: maximum value the integer can take
    :param \*\*kwargs: arbitrary keyword arguments passed to :class:`Opt`

    .. versionchanged:: 1.15

       Added *min* and *max* parameters.
    """

    def __init__(self, name, min=None, max=None, **kwargs):
        super(IntOpt, self).__init__(name, type=types.Integer(min, max),
                                     **kwargs)
class IPOpt(Opt):

    r"""Opt with IPAddress type

    Option with ``type`` :class:`oslo_config.types.IPAddress`

    :param name: the option's name
    :param version: one of either ``4``, ``6``, or ``None`` to specify
       either version.
    :param \*\*kwargs: arbitrary keyword arguments passed to :class:`Opt`

    .. versionadded:: 1.4
    """

    def __init__(self, name, version=None, **kwargs):
        super(IPOpt, self).__init__(name, type=types.IPAddress(version),
                                    **kwargs

(Opt类篇幅较长,就不粘贴了)

从上面两个类的声明以及__init__方法来看,Opt的派生类(_ConfigDirOpt类、_ConfigFileOpt类除外,后面详细介绍这两个类)以及Opt类之间没有新增修改任何字段,唯一有差别的便是type字段的值。IPOpt类的type属性值是types.IPAdress实例,IntOpt类的type属性值是types.Integer实例。从这里我们可以得知,Opt各个派生类的作用和ConfigType类的派生类作用是一样的,都仅仅只是通过不同的类去区分配置项值类型。

3、_CachedArgumentParser类,该类是cfg.py中定义的一个类,是argparse.ArgumentParser类的派生类,该类的源码如下:

class _CachedArgumentParser(argparse.ArgumentParser):

    """class for caching/collecting command line arguments.

    It also sorts the arguments before initializing the ArgumentParser.
    We need to do this since ArgumentParser by default does not sort
    the argument options and the only way to influence the order of
    arguments in '--help' is to ensure they are added in the sorted
    order.
    """

    def __init__(self, prog=None, usage=None, **kwargs):
        super(_CachedArgumentParser, self).__init__(prog, usage, **kwargs)
        self._args_cache = {}

    def add_parser_argument(self, container, *args, **kwargs):
        values = []
        if container in self._args_cache:
            values = self._args_cache[container]
        values.append({'args': args, 'kwargs': kwargs})
        self._args_cache[container] = values

    def initialize_parser_arguments(self):
        # NOTE(mfedosin): The code below looks a little bit weird, but
        # it's done because we need to sort only optional opts and do
        # not touch positional. For the reason optional opts go first in
        # the values we only need to find an index of the first positional
        # option and then sort the values slice.
        for container, values in self._args_cache.items():
            index = 0
            has_positional = False
            for index, argument in enumerate(values):
                if not argument['args'][0].startswith('-'):
                    has_positional = True
                    break
            size = index if has_positional else len(values)
            values[:size] = sorted(values[:size], key=lambda x: x['args'])
            for argument in values:
                try:
                    container.add_argument(*argument['args'],
                                           **argument['kwargs'])
                except argparse.ArgumentError:
                    options = ','.join(argument['args'])
                    raise DuplicateOptError(options)
        self._args_cache = {}

    def parse_args(self, args=None, namespace=None):
        self.initialize_parser_arguments()
        return super(_CachedArgumentParser, self).parse_args(args, namespace)

    def print_help(self, file=None):
        self.initialize_parser_arguments()
        super(_CachedArgumentParser, self).print_help(file)

    def print_usage(self, file=None):
        self.initialize_parser_arguments()
        super(_CachedArgumentParser, self).print_usage(file)

从以上代码可以看出该类的作用就是对命令行参数做一个简单的处理:用一个字典类型来保存命令行参数,并对其中的可选参数进行筛选排序,剩下的都是对ArgumentParser类方法的封装而已。

4、_ConfigFileOpt以及_ConfigDirOpt。这两个类的作用以及实现方式是一样的,只不过一个针对文件目录,一个针对文件而已。下面就只说明一下_ConfigFileOpt类,该类的源码如下:

class _ConfigFileOpt(Opt):

    """The --config-file option.

    This is an private option type which handles the special processing
    required for --config-file options.

    As each --config-file option is encountered on the command line, we
    parse the file and store the parsed values in the _Namespace object.
    This allows us to properly handle the precedence of --config-file
    options over previous command line arguments, but not over subsequent
    arguments.

    .. versionadded:: 1.2
    """

    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)

    def _get_argparse_kwargs(self, group, **kwargs):
        """Extends the base argparse keyword dict for the config file opt."""
        kwargs = super(_ConfigFileOpt, self)._get_argparse_kwargs(group)
        kwargs['action'] = self.ConfigFileAction
        return kwargs

从代码中我们可以看出来上,该类是为了处理命令行参数中的"--configure-file"配置参数。其中还专门定义了一个内部类ConfigFileAction,该类就是argparse.Action的派生类,在该类中重写了__call__方法,在该方法调用iniparse进行配置文件的解析,将配置文件中配置信息读取出来。

上面就是oslo.config模块中需要了解的几个类。


下面详细介绍下oslo.config在某个服务启动时,是如何工作的,以cinder-volume为例。

通过cinder-volume.service文件,可以看出,cinder-volume的启动是通过以下命令行启动的:

/usr/bin/cinder-volume --config-file /usr/share/cinder/cinder-dist.conf --config-file /etc/cinder/cinder.conf --logfile /var/log/cinder/volume.log

cinder-volume中的文件内容如下:

#!/usr/bin/env python
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Starter script for Cinder Volume."""

import os

import eventlet

if os.name == 'nt':
    # eventlet monkey patching the os module causes subprocess.Popen to fail
    # on Windows when using pipes due to missing non-blocking IO support.
    eventlet.monkey_patch(os=False)
else:
    eventlet.monkey_patch()

import sys
import warnings

warnings.simplefilter('once', DeprecationWarning)

from oslo.config import cfg

# If ../cinder/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
                                   os.pardir,
                                   os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'cinder', '__init__.py')):
    sys.path.insert(0, possible_topdir)

from cinder import i18n
i18n.enable_lazy()

# Need to register global_opts
from cinder.common import config  # noqa
from cinder.openstack.common import log as logging
from cinder import service
from cinder import utils
from cinder import version


host_opt = cfg.StrOpt('host',
                      help='Backend override of host value.')
CONF = cfg.CONF


if __name__ == '__main__':
    CONF(sys.argv[1:], project='cinder',
         version=version.version_string())
    logging.setup("cinder")
    utils.monkey_patch()
    launcher = service.get_launcher()
    if CONF.enabled_backends:
        for backend in CONF.enabled_backends:
            CONF.register_opts([host_opt], group=backend)
            backend_host = getattr(CONF, backend).host
            host = "%s@%s" % (backend_host or CONF.host, backend)
            server = service.Service.create(host=host,
                                            service_name=backend)
            launcher.launch_service(server)
    else:
        server = service.Service.create(binary='cinder-volume')
        launcher.launch_service(server)
    launcher.wait()

在cinder-volume中引用了cfg中的CONF实例,并在main方法中通过该实例调用其__call__方法,并通过sys.argv传入命令行参数以及project、version。

在执行到_pre_setup方法时,会执行以下代码获取配置文件默认位置,以及prog(program_name,不指定的话,就是脚本名称),并设置第一个参数version,并将其prog, default_config_files进行返回。

def _pre_setup(self, project, prog, version, usage, default_config_
    """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

在add_parser_argument方法中,我们可以看到该方法只是在self._args_cache指定一个键值对而已,并没有使用argparse进行命令参数的生成。

在后续_setup方法中,又增加了了两个配置项config-file以及config-dir,并且该配置项的值是上一个方法中在默认位置中找到的文件,并通过register_cli_opts方法放入到CONF的_cli_opts属性值中,要注意,这个地方的配置文件是默认的配置文件,如果服务启动的命令行中不指定配置文件,则使用该配置文件进行服务的初始化。该步骤完成后,进行的就是通过argparse进行命令行参数的渲染生成了。

self._namespace = self._parse_cli_opts(args if args is not None
                                               else sys.argv[1:])

通过_parse_cli_opts方法,并将服务启动的参数(配置文件)传入其中,通过_add_to_cli方法将封装成argparse所支持的数据结构,其中执行_get_argparse_kwargs方法时,不通的opt执行的具体方法可能会不一样(多态)不同类型的配置项对应不用的Action。将每个参数都包装成argparse所支持的数据结构之后,会将每个参数都放到_CachedArgumentParser类的_args_cache属性值中。

    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)
该步骤完成后,开始执行_parse_config_files,即开始真正的通过argparse的parse_args方法开始进行参数的读取,并放置到NameSpace中,从而提供给各个模块,进行配置项的读取。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值