当前位置: 首页 > 工具软件 > oslo.cache > 使用案例 >

openstack oslo.config组件服务浅析

云曦之
2023-12-01

首先,仍然以compute节点为例,看一下cfg服务的启动过程:
在compute节点的nova-compute服务启动时,调用nova.cmd.compute中的main方法。在此之前会执行模块内的CONF = nova.conf.CONF,其返回的是oslo_config.cfg.ConfigOpts类的实例。

之后我们返回到nova.conf.__init__.py中,模块初始化时服务把默认的配置项及配置组注册至cfg中,首先举例看conf中group即对应conf文件的section注册流程:

api.register_opts(CONF)
↓  `nova.conf.api`

def register_opts(conf):
    conf.register_group(api_group)
    conf.register_opts(API_OPTS, group=api_group)
    conf.register_opts(deprecated_opts)

↓  `nova.conf.api`

api_group = cfg.OptGroup('api',
    title='API options',
    help=''

↓  `oslo_config.cfg`

class OptGroup(object):
    def __init__(self, name, title=None, help=None):
        self.name = name
        self.title = "%s options" % name if title is None else title
        self.help = help

        self._opts = {}  # dict of dicts of (opt:, override:, default:)
        self._argparse_group = None

↓  `oslo_conf.cfg.ConfigOpts`

def register_group(self, group):
    if group.name in self._groups:
        return

    self._groups[group.name] = copy.copy(group)
  • api_group变量值为OptGroup类实例,定义了某option的集合,等同于conf中的 [] section。传入的name=’api’即为section的名称,即conf文件中的[api] section。
  • register_group(api_group)将api_group的name属性作为key,并copy(api_group)这个实例作为value,存入CONF的_groups字典中,完成此section的注册。

接下来,我们分析section中options的注册流程:


1.常用类型的option,包括string、int、bool、float等,均继承自oslo_config.cfg.Opt类。类初始化方法接受的参数为:

name:option的名称,对应conf文件section下各个option的名称;

type:option值的数据类型,必须是oslo_config.types模块下不同数据类型对应的类的实例。能够接收string,返回转换后且经过验证的值;

dest:CONF中相对应属性名;

short:命令行中option名称的单字母缩写;

default:option的默认值;

positional:是否为命令行中固定位置的argument;

metavar:--help中显示的argument信息;

help:此option的使用帮助信息;

secret:是否不在log中显示相关信息;

required:是否值不能为空;

deprecated_name:option注册时,将覆写的相关option的名称。类似别名;

deprecated_group:别名所在的section,如果不填则代表同option所在一样的section;

deprecated_opts: option注册时,将要覆写的别名信息列表,元素为oslo_config.cfg.DeprecatedOpt类实例;

deprecated_for_removal:标明是否在未来版本中,此项option将会被废弃;

deprecated_reason:对于此项option在未来版本中被废弃的解释;

deprecated_since:标明在哪个未来版本中此项option将会被废弃;

mutable:是否此option可以被重载;

advanced:是否此option是一个高级选项,默认不被大部分用户使用。

2.区分option不同的类型,由初始化中type参数,即oslo_config.types模块中对应的不同类的实例来实现。在此举string类型的option为例说明:

cfg.StrOpt()
↓
class StrOpt(Opt):
    def __init__(self, name, choices=None, quotes=None,
                 regex=None, ignore_case=None, max_length=None, **kwargs):
        super(StrOpt, self).__init__(name,
                                     type=types.String(
                                         choices=choices,
                                         quotes=quotes,
                                         regex=regex,
                                         ignore_case=ignore_case,
                                         max_length=max_length),
                                     **kwargs)
↓
class String(ConfigType):
    def __init__(self, choices=None, quotes=False, regex=None,
                 ignore_case=False, max_length=None,
                 type_name='string value'):
        super(String, self).__init__(type_name=type_name)
        if choices and regex:
            raise ValueError("'choices' and 'regex' cannot both be specified")

        self.ignore_case = ignore_case
        self.quotes = quotes
        self.max_length = max_length or 0

        self.choices = choices
        self.lower_case_choices = None
        if self.choices is not None and self.ignore_case:
            self.lower_case_choices = [c.lower() for c in choices]

        self.regex = regex
        if self.regex is not None:
            re_flags = re.IGNORECASE if self.ignore_case else 0

            # Check if regex is a string or an already compiled regex
            if isinstance(regex, six.string_types):
                self.regex = re.compile(regex, re_flags)
            else:
                self.regex = re.compile(regex.pattern, re_flags | regex.flags)

types.String类初始化接受的参数为:

choices:option的可选值,与regex不可同时存在;

quotes:如果为True,则传入的string如果被单引号或双引号包围,则会去掉引号。如果引号不成对,则会raise错误。在string为容器结构如列表等时比较有用。默认为False;

regex:option值由正则表达式匹配出,与choices不可同时存在;

ignore_case:如果为True,则对大小写不敏感;

max_length:option值的长度最大值;

type_name:标明的option值类型的字符串,默认为'string value'

可知每个定义好的option即为继承了oslo_config.cfg.Opt的子类的实例。其余类型的option类似,区别为types模块中对应类初始化所接受的参数不同。


3.定义好了option的数据类型,如何将其注册至config中,或config对应section即group中?
注册opt的方法为oslo_config.cfg.ConfigOpts().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)

        # NOTE(gcb) We can't use some names which are same with attributes of
        # Opts in default group. They includes project, prog, version, usage
        # and default_config_files.
        if group is None:
            if opt.name in self.disallow_names:
                raise ValueError('Name %s was reserved for oslo.config.'
                                 % opt.name)

        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

所接受的参数:

opt:继承了oslo_config.cfg.Opt的类的实例,即第2节解析的定义好的option实例;

group:第1节中定义的oslo_config.cfg.OptGroup类实例,或名称。即需要注册option进入的section;

cli:标明是否为可从命令行接受的option。

代码逻辑:


 1. 如果提供了group,则取出保存的对应group实例。如果选择自动创建且没有创建并注册指定的group时,进行创建并注册后保存,返回保存的group实例;

 2. 如果cli=True,则向self._cli_opts这个deque中添加opt与group的相关绑定信息;

 3. 如果取得了group实例,则向group的_opts字典中,以opt.dest为key, dict(opt=opt, cli=cli)为value添加元素,返回True。如果之前已添加相同元素,则返回False。

 4. 如果未取得group实例,则向自身的_opts字典中,以opt.dest为key, dict(opt=opt, cli=cli)为value添加元素,返回True。如果之前已添加相同元素,则返回False。

至此,group与option的注册简单流程完成。下面我们看程序如何从nova.conf配置文件中覆写option为自定义的值。


1.在nova.cmd.compute.main中,有如下代码config.parse_args(sys.argv),而在此方法中,有如下代码:

CONF(argv[1:],
         project='nova',
         version=version.version_string(),
         default_config_files=default_config_files) # defalut_config_files=None

可知在此处,调用了CONF的__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, without extension .py)
        :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()

解析逻辑:


 1. 通过_pre_setup方法,返回服务启动脚本名称('nova-compute')及可能的配置文件的绝对路径default_config_files。文件名可能为nova.conf或nova-compute.conf,路径可能为~/.nova/、~/、/etc/nova/、/etc/。

 2. 定义了self._namespace,其值为通过_parse_cli_opts(sys.arg[1:])方法,首先将之前存储在CONF._cli_opts的所有支持CLI设定的option可用,之后通过_parse_config_files方法,返回了_Namespace(),并将之前CONF中定义的opt.dest, value添加进namespace。之后使用config-file中的相同option在namespace中覆写。

最后,分析如何从CONF中取出所需的option的值。


当需要获取option值时,方式为以option name调取CONF的属性,即__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)
↓
    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
↓
    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

逻辑为通过格式转换,从namespace中获取所需value。

 类似资料: