1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00
Files
pgbackrest/build/lib/config/parse.py
David Steele 3f590f7012 Fix lines that exceed 132 characters.
Most of these are prose in a python docstring or comment wrapped a few characters wide, which black does not reflow, and macro continuation backslashes left stranded past the margin. The rest are long calls broken after the first parenthesis, section separator comments with too many dashes, and long expectation strings split along what they hold. A few long shell commands in the workflows are wrapped with continuations.

hrnTestResultDiff() had a continuation backslash left over from when it was a macro, which spliced the closing brace onto the line above it. It is removed.

The linter that enforces the limit follows, so this commit is only the reformatting and can be ignored by blame.
2026-08-07 12:35:14 +07:00

710 lines
29 KiB
Python

"""Parse Configuration Yaml.
Reads the command and option declarations, which are the definition of record for the pgBackRest command line and configuration
file.
Most of the work here is inheritance. An option can take another option's command list, allow list, or dependency, and a command
entry in an option can default to what the option itself declares, so what a declaration means depends on what was declared before
it. That is resolved here rather than at render time, so the render sees complete options.
The declaration order is also what the reader sees, so an option can only inherit from an option above it."""
####################################################################################################################################
import copy
import os
from common.error import ToolError, check
from common.storage import file_read
from common.yaml import YamlMap, yaml_bool, yaml_load, yaml_map_empty
# Command roles. A role is a process that carries out part of a command, and main is the process the user started. The order here is
# the order the roles are rendered in.
CMD_ROLE_MAIN = "main"
CMD_ROLE_ASYNC = "async"
CMD_ROLE_LOCAL = "local"
CMD_ROLE_REMOTE = "remote"
CMD_ROLE_LIST = (CMD_ROLE_MAIN, CMD_ROLE_ASYNC, CMD_ROLE_LOCAL, CMD_ROLE_REMOTE)
# Commands that take no options of their own, so they are left out of an option's command list
CMD_HELP = "help"
CMD_VERSION = "version"
# Option types
OPT_TYPE_BOOLEAN = "boolean"
OPT_TYPE_HASH = "hash"
OPT_TYPE_INTEGER = "integer"
OPT_TYPE_LIST = "list"
OPT_TYPE_PATH = "path"
OPT_TYPE_SIZE = "size"
OPT_TYPE_STRING = "string"
OPT_TYPE_STRING_ID = "string-id"
OPT_TYPE_TIME = "time"
# Options that the rest of the build refers to by name
OPT_STANZA = "stanza"
# Sections an option can be configured in. An option in the command-line section cannot be set in the configuration file, which is
# why it can be neither negated nor reset.
SECTION_COMMAND_LINE = "command-line"
# How a default value is rendered
DEFAULT_TYPE_QUOTE = "quote"
DEFAULT_TYPE_LITERAL = "literal"
DEFAULT_TYPE_DYNAMIC = "dynamic"
_DEFAULT_TYPE_LIST = (DEFAULT_TYPE_QUOTE, DEFAULT_TYPE_LITERAL, DEFAULT_TYPE_DYNAMIC)
####################################################################################################################################
class BldCfgCommand:
"""A command."""
def __init__(self, name):
self.name = name
self.internal = False # Is the command internal?
self.log_file = True # Does the command write automatically to a log file?
self.log_level_default = "info" # Default log level
self.lock_required = False # Is a lock required?
self.lock_remote_required = False # Is a remote lock required?
self.lock_type = "none" # Lock type
self.parameter_allowed = False # Are command line parameters allowed?
self.role_list = None # Roles valid for the command
####################################################################################################################################
class BldCfgOptionGroup:
"""An option group, i.e. a set of options that can be indexed, e.g. repo1-path and repo2-path."""
def __init__(self, name):
self.name = name
####################################################################################################################################
class BldCfgOptionValue:
"""A value in an allow list, which may be compiled in only when a feature is."""
def __init__(self, value, condition):
self.value = value
self.condition = condition # Is the value conditionally compiled?
####################################################################################################################################
class BldCfgOptionAllowRangeMap:
"""An allowed range for one value of the option the range maps on."""
def __init__(self, map, min, max):
self.map = map
self.min = min
self.max = max
####################################################################################################################################
class BldCfgOptionAllowRange:
"""An allowed range, either one range or a range per value of another option."""
def __init__(self, min=None, max=None, map_list=None):
self.min = min
self.max = max
self.map_list = map_list
####################################################################################################################################
class BldCfgOptionDefaultMap:
"""A default for one value of the option the default maps on."""
def __init__(self, map, value):
self.map = map
self.value = value
####################################################################################################################################
class BldCfgOptionDefault:
"""A default value, either one value or a value per value of another option."""
def __init__(self, value=None, map_list=None):
self.value = value
self.map_list = map_list
####################################################################################################################################
class BldCfgOptionDepend:
"""A dependency on another option."""
def __init__(self, option, default_value, value_list):
self.option = option # Option the dependency is on
self.default_value = default_value # Value to use when the dependency is not resolved
self.value_list = value_list # Values of the option that resolve the dependency
####################################################################################################################################
class BldCfgOptionDeprecate:
"""A deprecated name for an option."""
def __init__(self, name):
self.name = name
self.indexed = False # Can the deprecation be indexed?
self.unindexed = False # Can the deprecation be unindexed?
####################################################################################################################################
class BldCfgOptionCommand:
"""What an option means for one command."""
def __init__(self, name):
self.name = name
self.internal = None # Is the option internal, or None to use what the option declares?
self.required = None # Is the option required, or None to use what the option declares?
self.sequence = None # Is a sequence added to the StringId, or None to use what the option declares?
self.default_value = None
self.depend = None
self.allow_list = None
self.role_list = None # Roles of the command the option is valid for
####################################################################################################################################
class BldCfgOption:
"""An option."""
def __init__(self, name):
self.name = name
self.type = None # Option type, e.g. integer
self.section = None # Section the option can be configured in
self.bool_like = False # Does the option accept y/n and can be treated as a boolean?
self.internal = False # Is the option internal?
self.beta = False # Is the option beta?
self.required = True # Is the option required?
self.negate = None # Can the option be negated?
self.reset = False # Can the option be reset?
self.sequence = False # Is a sequence added to the StringId?
self.default_type = DEFAULT_TYPE_QUOTE # How the default is rendered
self.default_value = None
self.group = None # Option group, if any
self.secure = False # Does the option contain a secret?
self.depend = None
self.allow_list = None
self.allow_range = None
self.cmd_list = None # What the option means for each command it is valid for
self.cmd_role_list = None # Roles the option is valid for, before they are filtered per command
self.deprecate_list = None
####################################################################################################################################
class BldCfg:
"""The configuration declaration."""
def __init__(self, cmd_list, opt_grp_list, opt_list, opt_resolve_list):
self.cmd_list = cmd_list # Commands, sorted
self.opt_grp_list = opt_grp_list # Option groups, sorted
self.opt_list = opt_list # Options, sorted
self.opt_resolve_list = opt_resolve_list # Options in the order their dependencies can be resolved in
####################################################################################################################################
def _find(item_list, name):
"""Find an entry in a list by name, or None when it is not there."""
for item in item_list:
if item.name == name:
return item
return None
####################################################################################################################################
def _command_role(role_raw, name):
"""Parse a command role list, which is a map of roles with nothing under them."""
result = []
for role, detail in role_raw:
yaml_map_empty(detail, "%s role '%s'" % (name, role))
result.append(role)
return result
####################################################################################################################################
def _command_list(cmd_raw):
"""Parse the command list."""
result = []
for name, detail in cmd_raw:
cmd = BldCfgCommand(name)
for key, value in detail:
if key == "command-role":
cmd.role_list = _command_role(value, "command '%s'" % name)
elif key == "internal":
cmd.internal = yaml_bool(value, "command '%s' internal" % name)
elif key == "lock-type":
cmd.lock_type = value
elif key == "lock-remote-required":
cmd.lock_remote_required = yaml_bool(value, "command '%s' lock-remote-required" % name)
elif key == "lock-required":
cmd.lock_required = yaml_bool(value, "command '%s' lock-required" % name)
elif key == "log-file":
cmd.log_file = yaml_bool(value, "command '%s' log-file" % name)
elif key == "log-level-default":
cmd.log_level_default = value.lower()
elif key == "parameter-allowed":
cmd.parameter_allowed = yaml_bool(value, "command '%s' parameter-allowed" % name)
else:
raise ToolError("unknown command definition '%s'" % key)
# Every command has a main role whether it declares one or not, since main is the process the user started
cmd.role_list = sorted(set(cmd.role_list or []) | {CMD_ROLE_MAIN})
result.append(cmd)
return sorted(result, key=lambda cmd: cmd.name)
####################################################################################################################################
def _option_group_list(opt_grp_raw):
"""Parse the option group list, which is a map of groups with nothing under them."""
result = []
for name, detail in opt_grp_raw:
yaml_map_empty(detail, "option group '%s'" % name)
result.append(BldCfgOptionGroup(name))
return sorted(result, key=lambda opt_grp: opt_grp.name)
####################################################################################################################################
def _allow_list(allow_raw, opt_list, name):
"""Parse an allow list, which is either the list of allowed values or the name of an option to take it from."""
# A scalar names the option the allow list is inherited from
if isinstance(allow_raw, str):
check(opt_list is not None, "allow list for %s cannot be inherited" % name)
opt_inherit = _find(opt_list, allow_raw)
check(opt_inherit is not None, "allow list inherited from option '%s' before it is defined" % allow_raw)
return copy.copy(opt_inherit.allow_list)
result = []
for value in allow_raw:
# A scalar is the value on its own, else a map of the value to the feature it is compiled in with
if isinstance(value, str):
result.append(BldCfgOptionValue(value, None))
else:
check(len(value) == 1, "allow list value for %s must have a single condition" % name)
for item, condition in value:
result.append(BldCfgOptionValue(item, condition))
return result
####################################################################################################################################
def _allow_range(range_raw, name):
"""Parse an allow range, which is either a min/max pair or a min/max pair per value of the option it maps on."""
# A scalar first means the range itself rather than a range per map
if isinstance(range_raw[0], str):
check(len(range_raw) == 2, "allow range for %s must be a min and a max" % name)
return BldCfgOptionAllowRange(min=range_raw[0], max=range_raw[1])
map_list = []
for entry in range_raw:
for map, range in entry:
check(len(range) == 2, "allow range for %s map '%s' must be a min and a max" % (name, map))
map_list.append(BldCfgOptionAllowRangeMap(map, range[0], range[1]))
return BldCfgOptionAllowRange(map_list=map_list)
####################################################################################################################################
def _default(default_raw):
"""Parse a default, which is either the value or a value per value of the option it maps on.
A tilde overrides an inherited default back to no default at all."""
# A scalar is the default itself
if isinstance(default_raw, str):
return None if default_raw == "~" else BldCfgOptionDefault(value=default_raw)
map_list = []
for entry in default_raw:
for map, value in entry:
map_list.append(BldCfgOptionDefaultMap(map, value))
return BldCfgOptionDefault(map_list=map_list)
####################################################################################################################################
def _depend(depend_raw, opt_list):
"""Parse a dependency, which is either the dependency or the name of an option to take it from."""
# A scalar names the option the dependency is inherited from
if isinstance(depend_raw, str):
opt_inherit = _find(opt_list, depend_raw)
if opt_inherit is None:
raise ToolError("dependency inherited from option '%s' before it is defined" % depend_raw)
return opt_inherit.depend
option = None
default_value = None
value_list = None
for key, value in depend_raw:
if key == "list":
value_list = list(value)
elif key == "default":
default_value = value
elif key == "option":
option = value
else:
raise ToolError("unknown depend definition '%s'" % key)
return BldCfgOptionDepend(option, default_value, value_list)
####################################################################################################################################
def _depend_reconcile(opt, depend, opt_list):
"""Resolve a dependency to the option it is on, now that every option exists."""
if depend is None:
return None
if depend.default_value is not None and opt.type not in (OPT_TYPE_BOOLEAN, OPT_TYPE_INTEGER):
raise ToolError("dependency default invalid for non integer/boolean option '%s'" % opt.name)
opt_depend = _find(opt_list, depend.option)
if opt_depend is None:
raise ToolError("dependency on undefined option '%s'" % depend.option)
return BldCfgOptionDepend(opt_depend, depend.default_value, depend.value_list)
####################################################################################################################################
def _deprecate_list(deprecate_raw):
"""Parse the deprecated names of an option.
A question mark in the name means the deprecation may be indexed, e.g. repo?-path covers repo-path and repo1-path, so the same
name can appear twice and each spelling is recorded separately."""
result = []
for name, detail in deprecate_raw:
yaml_map_empty(detail, "deprecate '%s'" % name)
indexed = "?" in name
name = name.replace("?", "", 1)
deprecate = _find(result, name)
if deprecate is None:
deprecate = BldCfgOptionDeprecate(name)
result.append(deprecate)
if indexed:
deprecate.indexed = True
else:
deprecate.unindexed = True
return sorted(result, key=lambda deprecate: deprecate.name)
####################################################################################################################################
def _option_command_list(opt_cmd_raw, cmd_list, opt_list, name):
"""Parse the commands an option is valid for, which is either the list or the name of an option to take it from."""
# A scalar names the option the command list is inherited from
if isinstance(opt_cmd_raw, str):
opt_inherit = _find(opt_list, opt_cmd_raw)
check(opt_inherit is not None, "command list inherited from option '%s' before it is defined" % opt_cmd_raw)
return opt_inherit.cmd_list
result = []
for opt_cmd, detail in opt_cmd_raw:
# Add every command of another option, stripped of what that option declared about them
if opt_cmd == "+inherit":
opt_inherit = _find(opt_list, detail)
check(opt_inherit is not None, "commands inherited from option '%s' before it is defined" % detail)
for cmd in opt_inherit.cmd_list:
result.append(BldCfgOptionCommand(cmd.name))
# Add every command that has a role, or any role
elif opt_cmd == "+role":
for cmd in cmd_list:
# Help and version take no options of their own
if cmd.name in (CMD_HELP, CMD_VERSION):
continue
for role in cmd.role_list:
if (detail == "any" or role == detail) and _find(result, cmd.name) is None:
result.append(BldCfgOptionCommand(cmd.name))
# Remove a command, which is how a command is trimmed from what +inherit or +role added
elif opt_cmd == "-command":
cmd = _find(result, detail)
if cmd is not None:
result.remove(cmd)
# Else the command is declared with what it overrides
else:
cmd = BldCfgOptionCommand(opt_cmd)
for key, value in detail:
if key == "allow-list":
cmd.allow_list = _allow_list(value, None, "%s command '%s'" % (name, opt_cmd))
elif key == "command-role":
cmd.role_list = _command_role(value, "%s command '%s'" % (name, opt_cmd))
elif key == "depend":
cmd.depend = _depend(value, opt_list)
elif key == "default":
cmd.default_value = _default(value)
elif key == "internal":
cmd.internal = yaml_bool(value, "%s command '%s' internal" % (name, opt_cmd))
elif key == "required":
cmd.required = yaml_bool(value, "%s command '%s' required" % (name, opt_cmd))
elif key == "sequence":
cmd.sequence = yaml_bool(value, "%s command '%s' sequence" % (name, opt_cmd))
else:
raise ToolError("unknown option command definition '%s'" % key)
# A declaration replaces whatever +inherit or +role added for the command
cmd_prior = _find(result, opt_cmd)
if cmd_prior is not None:
result.remove(cmd_prior)
result.append(cmd)
return sorted(result, key=lambda cmd: cmd.name)
####################################################################################################################################
def _option_list_raw(opt_raw, cmd_list, opt_grp_list):
"""Parse the options as declared, before any of them are resolved against each other."""
result = []
for name, detail in opt_raw:
opt = BldCfgOption(name)
inherit_found = False
for key, value in detail:
if key == "allow-list":
opt.allow_list = _allow_list(value, result, "option '%s'" % name)
elif key == "allow-range":
opt.allow_range = _allow_range(value, "option '%s'" % name)
elif key == "command":
opt.cmd_list = _option_command_list(value, cmd_list, result, "option '%s'" % name)
elif key == "command-role":
opt.cmd_role_list = _command_role(value, "option '%s'" % name)
elif key == "default":
opt.default_value = _default(value)
elif key == "depend":
opt.depend = _depend(value, result)
elif key == "deprecate":
opt.deprecate_list = _deprecate_list(value)
elif key == "default-type":
check(value in _DEFAULT_TYPE_LIST, "option '%s' has invalid default type '%s'" % (name, value))
opt.default_type = value
elif key == "group":
check(_find(opt_grp_list, value) is not None, "option '%s' has invalid group '%s'" % (name, value))
opt.group = value
elif key == "inherit":
opt_inherit = _find(result, value)
check(opt_inherit is not None, "option '%s' inherited before it is defined" % value)
# Everything declared before inherit is replaced and everything after it overrides, which is why inherit is written
# first. Deprecations are not inherited since they name the option they are a deprecation of.
opt = copy.copy(opt_inherit)
opt.name = name
opt.deprecate_list = None
inherit_found = True
elif key == "internal":
opt.internal = yaml_bool(value, "option '%s' internal" % name)
elif key == "bool-like":
opt.bool_like = yaml_bool(value, "option '%s' bool-like" % name)
elif key == "beta":
opt.beta = yaml_bool(value, "option '%s' beta" % name)
elif key == "negate":
opt.negate = yaml_bool(value, "option '%s' negate" % name)
elif key == "sequence":
opt.sequence = yaml_bool(value, "option '%s' sequence" % name)
elif key == "required":
opt.required = yaml_bool(value, "option '%s' required" % name)
elif key == "section":
opt.section = value
elif key == "secure":
opt.secure = yaml_bool(value, "option '%s' secure" % name)
elif key == "type":
opt.type = value
else:
raise ToolError("unknown option definition '%s'" % key)
if opt.type is None:
raise ToolError("option '%s' requires 'type'" % name)
# An inherited option already has these set from the option it inherited from
if not inherit_found:
# An option that does not say where it can be configured can only be given on the command line
if opt.section is None:
opt.section = SECTION_COMMAND_LINE
# Only an option that can be written in the configuration file can be negated there
if opt.negate is None:
opt.negate = (opt.type == OPT_TYPE_BOOLEAN or opt.bool_like) and opt.section != SECTION_COMMAND_LINE
# An option that does not name its commands is valid for all of them
if opt.cmd_list is None:
opt.cmd_list = sorted(
(BldCfgOptionCommand(cmd.name) for cmd in cmd_list if cmd.name not in (CMD_HELP, CMD_VERSION)),
key=lambda cmd: cmd.name,
)
# Only an option that can be written in the configuration file can be reset, which is what removes it again
opt.reset = opt.section != SECTION_COMMAND_LINE
result.append(opt)
return sorted(result, key=lambda opt: opt.name)
####################################################################################################################################
def _option_list(opt_raw, cmd_list, opt_grp_list):
"""Parse the option list and resolve every option against the others."""
opt_list_raw = _option_list_raw(opt_raw, cmd_list, opt_grp_list)
# Copy the options so a dependency can point at the option it is on rather than at the name of one
result = []
for opt_raw in opt_list_raw:
opt = copy.copy(opt_raw)
opt.cmd_list = None
opt.depend = None
result.append(opt)
# Resolve what each option means for each of its commands, which is what it declares for the command falling back to what it
# declares for itself
for idx, opt_raw in enumerate(opt_list_raw):
cmd_opt_list = []
for opt_cmd_raw in opt_raw.cmd_list:
cmd = _find(cmd_list, opt_cmd_raw.name)
if cmd is None:
raise ToolError("invalid command '%s' in option '%s' command list" % (opt_cmd_raw.name, opt_raw.name))
opt_cmd = copy.copy(opt_cmd_raw)
if opt_cmd.required is None:
opt_cmd.required = opt_raw.required
if opt_cmd.internal is None:
opt_cmd.internal = opt_raw.internal
if opt_cmd.sequence is None:
opt_cmd.sequence = opt_raw.sequence
if opt_cmd.role_list is None:
# What the option declares for every command, less the roles this command does not have
if opt_raw.cmd_role_list is not None:
opt_cmd.role_list = [role for role in opt_raw.cmd_role_list if role in cmd.role_list]
else:
opt_cmd.role_list = cmd.role_list
opt_cmd.depend = _depend_reconcile(opt_raw, opt_cmd_raw.depend, result)
cmd_opt_list.append(opt_cmd)
result[idx].cmd_list = cmd_opt_list
result[idx].depend = _depend_reconcile(opt_raw, opt_raw.depend, result)
return result
####################################################################################################################################
def _option_resolve_list(opt_list):
"""Order the options so that every option comes after the options it depends on.
The stanza option is resolved first since an error about a missing option is confusing when the stanza it would apply to is what
is actually missing, so stanza must exist and may not depend on anything."""
opt_stanza = _find(opt_list, OPT_STANZA)
if opt_stanza is None:
raise ToolError("option '%s' must exist" % OPT_STANZA)
if opt_stanza.depend is not None:
raise ToolError("option '%s' may not depend on other option" % OPT_STANZA)
for opt_cmd in opt_stanza.cmd_list:
if opt_cmd.depend is not None:
raise ToolError("option '%s' command '%s' may not depend on other option" % (OPT_STANZA, opt_cmd.name))
resolve_list = [OPT_STANZA]
while len(resolve_list) != len(opt_list):
resolved = False
for opt in opt_list:
if opt.name in resolve_list:
continue
if opt.depend is not None and opt.depend.option.name not in resolve_list:
continue
if any(cmd.depend is not None and cmd.depend.option.name not in resolve_list for cmd in opt.cmd_list):
continue
resolve_list.append(opt.name)
resolved = True
# Nothing resolved means the options left depend on each other, since every other reason to skip is already excluded
if not resolved:
raise ToolError(
"unable to resolve dependencies for option(s) '%s'\nHINT: are there circular dependencies?"
% ", ".join(opt.name for opt in opt_list if opt.name not in resolve_list)
)
return [_find(opt_list, name) for name in resolve_list]
####################################################################################################################################
def bld_cfg_parse(path_repo):
"""Parse config.yaml into the configuration declaration."""
path = os.path.join(path_repo, "build/config.yaml")
cfg_raw = yaml_load(file_read(path), path)
cmd_raw = None
opt_grp_raw = None
opt_raw = None
for key, value in cfg_raw:
if key == "command":
cmd_raw = value
elif key == "optionGroup":
opt_grp_raw = value
elif key == "option":
opt_raw = value
else:
raise ToolError("unknown config definition '%s'" % key)
check(cmd_raw is not None and opt_grp_raw is not None and opt_raw is not None, "command, optionGroup, and option are required")
cmd_list = _command_list(cmd_raw)
opt_grp_list = _option_group_list(opt_grp_raw)
opt_list = _option_list(opt_raw, cmd_list, opt_grp_list)
return BldCfg(cmd_list, opt_grp_list, opt_list, _option_resolve_list(opt_list))