天天看點

openstack oslo.config元件服務淺析

首先,仍然以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 

        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 

            # 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的類的執行個體,即第節解析的定義好的option執行個體;

group:第節中定義的oslo_config.cfg.OptGroup類執行個體,或名稱。即需要注冊option進入的section;

cli:标明是否為可從指令行接受的option。
           

代碼邏輯:

如果提供了group,則取出儲存的對應group執行個體。如果選擇自動建立且沒有建立并注冊指定的group時,進行建立并注冊後儲存,傳回儲存的group執行個體;

  如果cli=True,則向self._cli_opts這個deque中添加opt與group的相關綁定資訊;

  如果取得了group執行個體,則向group的_opts字典中,以opt.dest為key, dict(opt=opt, cli=cli)為value添加元素,傳回True。如果之前已添加相同元素,則傳回False。

  如果未取得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[:],
         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[:])
        :param project: the toplevel project name, used to locate config files
        :param prog: the name of the program (defaults to sys.argv[]
            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()
           

解析邏輯:

. 通過_pre_setup方法,傳回服務啟動腳本名稱('nova-compute')及可能的配置檔案的絕對路徑default_config_files。檔案名可能為nova.conf或nova-compute.conf,路徑可能為~/.nova/、~/、/etc/nova/、/etc/。

 . 定義了self._namespace,其值為通過_parse_cli_opts(sys.arg[:])方法,首先将之前存儲在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。

繼續閱讀