pungi/pungi/checks.py
Tomas Hozza 57739c238f
osbuild: support specifying upload_options
Since version 9, the `koji-osbuild` plugin supports specifying upload
options as part of a Koji build. This enables one to upload the built
image directly to the cloud environment as part of the image build in
Koji.

Extend the configuration schema with `upload_options`.
Extend the documentation and describe valid `upload_options` values.
Add a unit test testing a scenario when `upload_options` are specified.

Signed-off-by: Tomas Hozza <thozza@redhat.com>
2022-09-06 11:20:01 +02:00

1508 lines
66 KiB
Python

# -*- coding: utf-8 -*-
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module exports a couple functions for checking configuration and
environment.
Validation of the configuration is the most complicated part here: here is the
outline of the process:
1. The configuration is checked against JSON schema. The errors encountered are
reported as string. The validator also populates default values.
2. The requirements/conflicts among options are resolved separately. This is
because expressing those relationships in JSON Schema is very verbose and
the error message is not very descriptive.
3. Extra validation can happen in ``validate()`` method of any phase.
When a new config option is added, the schema must be updated (see the
``make_schema`` function). The dependencies should be encoded into
``CONFIG_DEPS`` mapping.
"""
from __future__ import print_function
import multiprocessing
import os.path
import platform
import re
import jsonschema
import six
from kobo.shortcuts import force_list
from pungi.phases import PHASES_NAMES
from pungi.runroot import RUNROOT_TYPES
from productmd.common import RELEASE_TYPES
from productmd.composeinfo import COMPOSE_TYPES
from . import util
def is_jigdo_needed(conf):
return conf.get("create_jigdo")
def is_isohybrid_needed(conf):
"""The isohybrid command is needed locally only for
createiso phase without runroot. If that is not going to run, we don't need
to check for it. Additionally, the syslinux package is only available on
x86_64 and i386.
"""
runroot_tag = conf.get("runroot_tag", "")
if runroot_tag:
return False
if platform.machine() not in ("x86_64", "i686", "i386"):
print(
"Not checking for /usr/bin/isohybrid due to current architecture. "
"Expect failures in productimg phase."
)
return False
return True
def is_genisoimage_needed(conf):
"""This is only needed locally for createiso without runroot."""
runroot_tag = conf.get("runroot_tag", "")
if runroot_tag or conf.get("createiso_use_xorrisofs"):
return False
return True
def is_xorrisofs_needed(conf):
"""This is only needed locally for createiso without runroot and
createiso_use_xorrisofs=True.
"""
runroot_tag = conf.get("runroot_tag", "")
if runroot_tag or not conf.get("createiso_use_xorrisofs"):
return False
return True
def is_createrepo_c_needed(conf):
return conf.get("createrepo_c")
# The first element in the tuple is package name expected to have the
# executable (2nd element of the tuple). The last element is an optional
# function that should determine if the tool is required based on
# configuration.
tools = [
("isomd5sum", "/usr/bin/implantisomd5", None),
("isomd5sum", "/usr/bin/checkisomd5", None),
("jigdo", "/usr/bin/jigdo-lite", is_jigdo_needed),
("genisoimage", "/usr/bin/genisoimage", is_genisoimage_needed),
("xorriso", "/usr/bin/xorrisofs", is_xorrisofs_needed),
("syslinux", "/usr/bin/isohybrid", is_isohybrid_needed),
# createrepo, modifyrepo and mergerepo are not needed by default, only when
# createrepo_c is not configured
(
"createrepo",
"/usr/bin/createrepo",
lambda conf: not is_createrepo_c_needed(conf),
),
(
"createrepo",
"/usr/bin/modifyrepo",
lambda conf: not is_createrepo_c_needed(conf),
),
("createrepo", "/usr/bin/mergerepo", lambda conf: not is_createrepo_c_needed(conf)),
("createrepo_c", "/usr/bin/createrepo_c", is_createrepo_c_needed),
("createrepo_c", "/usr/bin/modifyrepo_c", is_createrepo_c_needed),
("createrepo_c", "/usr/bin/mergerepo_c", is_createrepo_c_needed),
]
def check(conf):
"""Check runtime environment and report errors about missing dependencies."""
fail = False
for package, path, test_if_required in tools:
if test_if_required and not test_if_required(conf):
# The config says this file is not required, so we won't even check it.
continue
if not os.path.exists(path):
print("Program '%s' doesn't exist. Install package '%s'." % (path, package))
fail = True
return not fail
def check_umask(logger):
"""Make sure umask is set to something reasonable. If not, log a warning."""
mask = os.umask(0)
os.umask(mask)
if mask > 0o022:
logger.warning(
"Unusually strict umask detected (0%03o), "
"expect files with broken permissions.",
mask,
)
def check_skip_phases(logger, skip_phases, just_phases):
"""Check if skipped phases are needed by other phase.
:param logger: logger instance for reporting error
:param skip_phases: list of skipped phases
:param just_phases: list of phases just run
:returns: True if checking passed else False
"""
needed_by = {
"pkgset": "gather",
"gather": "createrepo",
}
fail = False
for k, v in needed_by.items():
if (k in skip_phases and needed_by[k] not in skip_phases) or (
needed_by[k] in just_phases and k not in just_phases
):
fail = True
logger.error("%s phase is skipped but it's needed by %s phase" % (k, v))
return not fail
def _validate_requires(schema, conf, valid_options):
"""
Check if all requires and conflicts are ok in configuration.
:param conf: Python dict with configuration to check
:param valid_options: mapping with option dependencies
:param with_default: a set of options that have default value
:returns: list of errors
"""
errors = []
def has_default(x):
return schema["properties"].get(x, {}).get("default") == conf[x]
for name, opt in valid_options.items():
value = conf.get(name)
errors.extend(
_check_dep(
name,
value,
opt.get("conflicts", []),
lambda x: x in conf and not has_default(x),
CONFLICTS,
)
)
errors.extend(
_check_dep(
name, value, opt.get("requires", []), lambda x: x not in conf, REQUIRES
)
)
return errors
def _check_dep(name, value, lst, matcher, fmt):
for deps in [deps for (func, deps) in lst if func(value)]:
for dep in [d for d in deps if matcher(d)]:
yield fmt.format(name, value, dep)
def validate(config, offline=False, schema=None):
"""Test the configuration against schema.
Undefined values for which a default value exists will be filled in.
"""
schema = schema or make_schema()
DefaultValidator = _extend_with_default_and_alias(
jsonschema.Draft4Validator, offline=offline
)
validator = DefaultValidator(
schema,
)
errors = []
warnings = []
for error in validator.iter_errors(config):
if isinstance(error, ConfigDeprecation):
warnings.append(REMOVED.format(".".join(error.path), error.message))
elif isinstance(error, ConfigOptionWarning):
warnings.append(error.message)
elif isinstance(error, ConfigOptionError):
errors.append(error.message)
elif not error.path and error.validator == "additionalProperties":
allowed_keys = set(error.schema["properties"].keys())
used_keys = set(error.instance.keys())
for key in used_keys - allowed_keys:
suggestion = _get_suggestion(key, allowed_keys)
if suggestion:
warnings.append(UNKNOWN_SUGGEST.format(key, suggestion))
else:
warnings.append(UNKNOWN.format(key))
else:
errors.append(
"Failed validation in %s: %s"
% (".".join([str(x) for x in error.path]), error.message)
)
if error.validator in ("anyOf", "oneOf"):
for suberror in error.context:
errors.append(" Possible reason: %s" % suberror.message)
return (errors + _validate_requires(schema, config, CONFIG_DEPS), warnings)
def _get_suggestion(desired, names):
"""Find a value in ``names`` that is the closest match for ``desired``.
The edit distance must be at most half the length of target string.
"""
closest = None
closest_match = len(desired) + 1
for name in names:
match = util.levenshtein(desired, name)
if match < closest_match and match < len(desired) // 2:
closest = name
closest_match = match
return closest
CONFLICTS = "ERROR: Config option {0}={1} conflicts with option {2}."
REQUIRES = "ERROR: Config option {0}={1} requires {2} which is not set."
REMOVED = "WARNING: Config option {0} was removed and has no effect; {1}."
UNKNOWN = "WARNING: Unrecognized config option: {0}."
UNKNOWN_SUGGEST = "WARNING: Unrecognized config option: {0}. Did you mean {1}?"
def _extend_with_default_and_alias(validator_class, offline=False):
validate_properties = validator_class.VALIDATORS["properties"]
validate_type = validator_class.VALIDATORS["type"]
validate_required = validator_class.VALIDATORS["required"]
validate_additional_properties = validator_class.VALIDATORS["additionalProperties"]
resolver = util.GitUrlResolver(offline=offline)
def _hook_errors(properties, instance, schema):
"""
Hook the instance and yield errors and warnings.
"""
for property, subschema in properties.items():
# update instance for alias option
# If alias option for the property is present and property is not specified,
# update the property in instance with value from alias option.
if "alias" in subschema:
if subschema["alias"] in instance:
msg = (
"WARNING: Config option '%s' is deprecated and "
"now an alias to '%s', please use '%s' instead. "
"In:\n%s" % (subschema["alias"], property, property, instance)
)
yield ConfigOptionWarning(msg)
if property in instance:
msg = (
"ERROR: Config option '%s' is an alias of '%s', "
"only one can be used." % (subschema["alias"], property)
)
yield ConfigOptionError(msg)
instance.pop(subschema["alias"])
else:
instance.setdefault(property, instance.pop(subschema["alias"]))
# update instance for append option
# If append is defined in schema, append values from append
# options to property. If property is not present in instance,
# set it to empty list, and append the values from append options.
# Note: property's schema must support a list of values.
if "append" in subschema:
appends = force_list(subschema["append"])
for append in appends:
if append in instance:
msg = (
"WARNING: Config option '%s' is deprecated, "
"its value will be appended to option '%s'. "
"In:\n%s" % (append, property, instance)
)
yield ConfigOptionWarning(msg)
if property in instance:
msg = (
"WARNING: Value from config option '%s' is "
"now appended to option '%s'." % (append, property)
)
yield ConfigOptionWarning(msg)
instance[property] = force_list(instance[property])
instance[property].extend(force_list(instance.pop(append)))
else:
msg = (
"WARNING: Config option '%s' is not found, "
"but '%s' is specified, "
"value from '%s' is now added as '%s'."
% (property, append, append, property)
)
yield ConfigOptionWarning(msg)
instance[property] = instance.pop(append)
def properties_validator(validator, properties, instance, schema):
"""
Assign default values to options that have them defined and are not
specified. Resolve URLs to Git repos
"""
for property, subschema in properties.items():
if "default" in subschema and property not in instance:
instance.setdefault(property, subschema["default"])
# Resolve git URL references to actual commit hashes
if subschema.get("type") == "url" and property in instance:
try:
instance[property] = resolver(instance[property])
except util.GitUrlResolveError as exc:
yield ConfigOptionError(exc)
# Resolve branch in scm dicts.
if (
# Schema says it can be an scm dict
subschema.get("$ref") == "#/definitions/str_or_scm_dict"
# and the data is actually a dict
and isinstance(instance.get(property), dict)
# and it's git
and instance[property]["scm"] == "git"
# and there's a repo URL specified
and "repo" in instance[property]
):
instance[property]["branch"] = resolver(
instance[property]["repo"],
instance[property].get("branch") or "HEAD",
)
for error in _hook_errors(properties, instance, schema):
yield error
for error in validate_properties(validator, properties, instance, schema):
yield error
def _validate_additional_properties(validator, aP, instance, schema):
properties = schema.get("properties", {})
for error in _hook_errors(properties, instance, schema):
yield error
for error in validate_additional_properties(validator, aP, instance, schema):
yield error
def _validate_required(validator, required, instance, schema):
properties = schema.get("properties", {})
for error in _hook_errors(properties, instance, schema):
yield error
for error in validate_required(validator, required, instance, schema):
yield error
def error_on_deprecated(validator, properties, instance, schema):
"""Unconditionally raise deprecation error if encountered."""
yield ConfigDeprecation(properties)
def validate_regex_type(validator, properties, instance, schema):
"""
Extend standard type validation to check correctness in regular
expressions.
"""
if properties == "regex":
try:
re.compile(instance)
except re.error as exc:
yield jsonschema.ValidationError(
"incorrect regular expression: %s" % str(exc),
)
else:
# Not a regular expression, delegate to original validator.
for error in validate_type(validator, properties, instance, schema):
yield error
def _validate_any_of(validator, anyOf, instance, schema):
"""
Overwrite jsonschema's anyOf validator to not yield ValidationError when
ConfigOptionWarning is found.
"""
all_errors = []
for index, subschema in enumerate(anyOf):
errs = list(validator.descend(instance, subschema, schema_path=index))
warnings = [err for err in errs if isinstance(err, ConfigOptionWarning)]
errors = [err for err in errs if err not in warnings]
if not errors:
for warning in warnings:
yield warning
break
all_errors.extend(errors)
else:
yield jsonschema.ValidationError(
"%r is not valid under any of the given schemas" % (instance,),
context=all_errors,
)
def is_array(checker, instance):
return isinstance(instance, (tuple, list))
def is_string_type(checker, instance):
return isinstance(instance, six.string_types)
type_checker = validator_class.TYPE_CHECKER.redefine_many(
{"array": is_array, "regex": is_string_type, "url": is_string_type}
)
return jsonschema.validators.extend(
validator_class,
{
"properties": properties_validator,
"deprecated": error_on_deprecated,
"type": validate_regex_type,
"required": _validate_required,
"additionalProperties": _validate_additional_properties,
"anyOf": _validate_any_of,
},
type_checker=type_checker,
)
class ConfigDeprecation(jsonschema.exceptions.ValidationError):
pass
class ConfigOptionWarning(jsonschema.exceptions.ValidationError):
pass
class ConfigOptionError(jsonschema.exceptions.ValidationError):
pass
def make_schema():
return {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pungi Configuration",
"definitions": {
"multilib_list": {
"type": "object",
"patternProperties": {
"^.+$": {"$ref": "#/definitions/list_of_strings"},
},
"additionalProperties": False,
},
"package_mapping": _variant_arch_mapping(
{"$ref": "#/definitions/list_of_strings"}
),
"scm_dict": {
"type": "object",
"properties": {
"scm": {
"type": "string",
"enum": ["file", "cvs", "git", "rpm", "koji"],
},
"repo": {"type": "string"},
"branch": {"$ref": "#/definitions/optional_string"},
"file": {"type": "string"},
"dir": {"type": "string"},
"command": {"type": "string"},
},
"additionalProperties": False,
},
"str_or_scm_dict": {
"anyOf": [{"type": "string"}, {"$ref": "#/definitions/scm_dict"}]
},
"repo_dict": {
"type": "object",
"properties": {
"name": {"type": "string"},
"baseurl": {"type": "string"},
"exclude": {"type": "string"},
"gpgcheck": {"type": "boolean"},
"enabled": {"type": "string"},
},
"additionalProperties": False,
"required": ["baseurl"],
},
"repo": {
"anyOf": [{"type": "string"}, {"$ref": "#/definitions/repo_dict"}]
},
"repos": _one_or_list({"$ref": "#/definitions/repo"}),
"list_of_strings": {"type": "array", "items": {"type": "string"}},
"strings": _one_or_list({"type": "string"}),
"optional_string": {"anyOf": [{"type": "string"}, {"type": "null"}]},
"live_image_config": {
"type": "object",
"properties": {
"kickstart": {"type": "string"},
"ksurl": {"type": "url"},
"name": {"type": "string"},
"subvariant": {"type": "string"},
"target": {"type": "string"},
"version": {"type": "string"},
"repo": {"$ref": "#/definitions/repos"},
"specfile": {"type": "string"},
"scratch": {"type": "boolean"},
"type": {"type": "string"},
"sign": {"type": "boolean"},
"failable": {"type": "boolean"},
"release": {"$ref": "#/definitions/optional_string"},
},
"required": ["kickstart"],
"additionalProperties": False,
"type": "object",
},
"osbs_config": {
"type": "object",
"properties": {
"url": {"type": "url"},
"target": {"type": "string"},
"scratch": {"type": "boolean"},
"priority": {"type": "number"},
"repo": {"$ref": "#/definitions/repos", "append": "repo_from"},
"gpgkey": {"type": "string"},
"git_branch": {"type": "string"},
},
"required": ["url", "target", "git_branch"],
},
"string_pairs": {
"type": "array",
"items": {
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
"additionalItems": False,
},
},
},
"type": "object",
"properties": {
"release_name": {"type": "string"},
"release_short": {"type": "string"},
"release_version": {"type": "string"},
"release_type": {"type": "string", "enum": RELEASE_TYPES, "default": "ga"},
"release_is_layered": {
"deprecated": "remove it. It's layered if there's configuration for base product" # noqa: E501
},
"release_internal": {"type": "boolean", "default": False},
"release_discinfo_description": {"type": "string"},
"treeinfo_version": {"type": "string"},
"compose_type": {"type": "string", "enum": COMPOSE_TYPES},
"base_product_name": {"type": "string"},
"base_product_short": {"type": "string"},
"base_product_version": {"type": "string"},
"base_product_type": {"type": "string", "default": "ga"},
"runroot": {
"deprecated": "remove it. Please specify 'runroot_method' if you want to enable runroot, otherwise run things locally", # noqa: E501
},
"global_runroot_method": {"type": "string", "enum": RUNROOT_TYPES},
"runroot_method": {
"oneOf": [
{"type": "string", "enum": RUNROOT_TYPES},
{
"type": "object",
"properties": {
"buildinstall": {"type": "string", "enum": RUNROOT_TYPES},
"ostree_installer": {
"type": "string",
"enum": RUNROOT_TYPES,
},
"ostree": {"type": "string", "enum": RUNROOT_TYPES},
"createiso": {"type": "string", "enum": RUNROOT_TYPES},
},
"additionalProperties": False,
},
]
},
"runroot_ssh_username": {"type": "string", "default": "root"},
"runroot_ssh_hostnames": {"type": "object", "default": {}},
"runroot_ssh_init_template": {"type": "string"},
"runroot_ssh_install_packages_template": {"type": "string"},
"runroot_ssh_run_template": {"type": "string"},
"create_jigdo": {"type": "boolean", "default": False},
"check_deps": {"type": "boolean", "default": True},
"require_all_comps_packages": {"type": "boolean", "default": False},
"bootable": {
"deprecated": "remove it. Setting buildinstall_method option if you want a bootable installer" # noqa: E501
},
"gather_method": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"oneOf": [
{"type": "string", "enum": ["hybrid"]},
{
"type": "object",
"patternProperties": {
"^module|comps|json$": {
"type": "string",
"enum": ["deps", "nodeps"],
}
},
},
]
}
},
"additionalProperties": False,
},
{"type": "string", "enum": ["deps", "nodeps", "hybrid"]},
],
},
"gather_source": {"deprecated": "remove it"},
"gather_fulltree": {"type": "boolean", "default": False},
"gather_selfhosting": {"type": "boolean", "default": False},
"gather_prepopulate": {"$ref": "#/definitions/str_or_scm_dict"},
"gather_source_mapping": {"type": "string"},
"gather_backend": {
"type": "string",
"enum": _get_gather_backends(),
"default": _get_default_gather_backend(),
},
"gather_profiler": {"type": "boolean", "default": False},
"gather_allow_reuse": {"type": "boolean", "default": False},
"pkgset_allow_reuse": {"type": "boolean", "default": True},
"createiso_allow_reuse": {"type": "boolean", "default": True},
"extraiso_allow_reuse": {"type": "boolean", "default": True},
"pkgset_source": {"type": "string", "enum": ["koji", "repos"]},
"createrepo_c": {"type": "boolean", "default": True},
"createrepo_checksum": {
"type": "string",
"default": "sha256",
"enum": ["sha1", "sha256", "sha512"],
},
"createrepo_enable_cache": {"type": "boolean", "default": True},
"createrepo_use_xz": {"type": "boolean", "default": False},
"createrepo_num_threads": {"type": "number", "default": get_num_cpus()},
"createrepo_num_workers": {"type": "number", "default": 3},
"createrepo_database": {"type": "boolean"},
"createrepo_extra_args": {
"type": "array",
"default": [],
"items": {"type": "string"},
},
"createrepo_extra_modulemd": {
"type": "object",
"patternProperties": {
".+": {"$ref": "#/definitions/scm_dict"},
"additionalProperties": False,
},
},
"repoclosure_strictness": _variant_arch_mapping(
{
"type": "string",
"default": "lenient",
"enum": ["off", "lenient", "fatal"],
}
),
"repoclosure_backend": {
"type": "string",
# Gather and repoclosure both have the same backends: yum + dnf
"default": _get_default_gather_backend(),
"enum": _get_gather_backends(),
},
"old_composes_per_release_type": {
"deprecated": "remove it. It is the default behavior now"
},
"hashed_directories": {"type": "boolean", "default": False},
"multilib_whitelist": {
"$ref": "#/definitions/multilib_list",
"default": {},
},
"multilib_blacklist": {
"$ref": "#/definitions/multilib_list",
"default": {},
},
"greedy_method": {
"type": "string",
"enum": ["none", "all", "build"],
"default": "none",
},
"additional_packages": {
"$ref": "#/definitions/package_mapping",
"default": [],
},
"filter_packages": {
"$ref": "#/definitions/package_mapping",
"default": [],
},
"filter_modules": {"$ref": "#/definitions/package_mapping", "default": []},
"sigkeys": {
"type": "array",
"items": {"$ref": "#/definitions/optional_string"},
"minItems": 1,
"default": [None],
},
"signed_packages_retries": {"type": "number", "default": 0},
"signed_packages_wait": {"type": "number", "default": 30},
"variants_file": {"$ref": "#/definitions/str_or_scm_dict"},
"comps_file": {"$ref": "#/definitions/str_or_scm_dict"},
"comps_filter_environments": {"type": "boolean", "default": True},
"module_defaults_dir": {"$ref": "#/definitions/str_or_scm_dict"},
"module_defaults_override_dir": {"type": "string"},
"pkgset_repos": {
"type": "object",
"patternProperties": {".+": {"$ref": "#/definitions/strings"}},
"additionalProperties": False,
},
"module_obsoletes_dir": {"$ref": "#/definitions/str_or_scm_dict"},
"create_optional_isos": {"type": "boolean", "default": False},
"symlink_isos_to": {"type": "string"},
"dogpile_cache_backend": {"type": "string"},
"dogpile_cache_expiration_time": {"type": "number"},
"dogpile_cache_arguments": {"type": "object", "default": {}},
"createiso_skip": _variant_arch_mapping({"type": "boolean"}),
"createiso_max_size": _variant_arch_mapping({"type": "number"}),
"createiso_max_size_is_strict": _variant_arch_mapping(
{"type": "boolean", "default": False}
),
"createiso_break_hardlinks": {"type": "boolean", "default": False},
"createiso_use_xorrisofs": {"type": "boolean", "default": False},
"iso_level": {
"anyOf": [
{"type": "number", "enum": [1, 2, 3, 4]},
_variant_arch_mapping({"type": "number", "enum": [1, 2, 3, 4]}),
],
},
"iso_hfs_ppc64le_compatible": {"type": "boolean", "default": True},
"multilib": _variant_arch_mapping(
{"$ref": "#/definitions/list_of_strings"}
),
"runroot_tag": {"type": "string"},
"runroot_channel": {"$ref": "#/definitions/optional_string"},
"runroot_weights": {
"type": "object",
"default": {},
"properties": {
"buildinstall": {"type": "number"},
"createiso": {"type": "number"},
"ostree": {"type": "number"},
"ostree_installer": {"type": "number"},
},
"additionalProperties": False,
},
"createrepo_deltas": {
"anyOf": [
# Deprecated in favour of more granular settings.
{"type": "boolean", "default": False},
_variant_arch_mapping({"type": "boolean"}),
]
},
"buildinstall_allow_reuse": {"type": "boolean", "default": False},
"buildinstall_method": {
"type": "string",
"enum": ["lorax", "buildinstall"],
},
"buildinstall_topdir": {"type": "string"},
"buildinstall_kickstart": {"$ref": "#/definitions/str_or_scm_dict"},
"buildinstall_use_guestmount": {"type": "boolean", "default": True},
"buildinstall_skip": _variant_arch_mapping({"type": "boolean"}),
"buildinstall_packages": {
"$ref": "#/definitions/package_mapping",
"default": [],
},
"global_ksurl": {"type": "url"},
"global_version": {"type": "string"},
"global_target": {"type": "string"},
"global_release": {"$ref": "#/definitions/optional_string"},
"pdc_url": {"deprecated": "Koji is queried instead"},
"pdc_develop": {"deprecated": "Koji is queried instead"},
"pdc_insecure": {"deprecated": "Koji is queried instead"},
"cts_url": {"type": "string"},
"cts_keytab": {"type": "string"},
"koji_profile": {"type": "string"},
"koji_event": {"type": "number"},
"pkgset_koji_tag": {"$ref": "#/definitions/strings"},
"pkgset_koji_builds": {"$ref": "#/definitions/strings"},
"pkgset_koji_scratch_tasks": {"$ref": "#/definitions/strings"},
"pkgset_koji_module_tag": {"$ref": "#/definitions/strings", "default": []},
"pkgset_koji_module_builds": {
"type": "object",
"patternProperties": {
"^.+$": {"$ref": "#/definitions/list_of_strings"}
},
"additionalProperties": False,
},
"pkgset_koji_inherit": {"type": "boolean", "default": True},
"pkgset_koji_inherit_modules": {"type": "boolean", "default": False},
"pkgset_exclusive_arch_considers_noarch": {
"type": "boolean",
"default": True,
},
"pkgset_scratch_modules": {
"type": "object",
"patternProperties": {
"^.+$": {"$ref": "#/definitions/list_of_strings"}
},
"additionalProperties": False,
},
"mbs_api_url": {"type": "string"},
"disc_types": {"type": "object", "default": {}},
"paths_module": {"type": "string"},
"skip_phases": {
"type": "array",
"items": {"type": "string", "enum": PHASES_NAMES + ["productimg"]},
"default": [],
},
"image_name_format": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": _one_or_list({"type": "string"}),
},
"additionalProperties": False,
},
],
},
"image_volid_formats": {
"$ref": "#/definitions/list_of_strings",
"default": [
"{release_short}-{version} {variant}.{arch}",
"{release_short}-{version} {arch}",
],
},
"image_volid_layered_product_formats": {
"$ref": "#/definitions/list_of_strings",
"default": [
"{release_short}-{version} {base_product_short}-{base_product_version} {variant}.{arch}", # noqa: E501
"{release_short}-{version} {base_product_short}-{base_product_version} {arch}", # noqa: E501
],
},
"restricted_volid": {"type": "boolean", "default": False},
"volume_id_substitutions": {"type": "object", "default": {}},
"live_images_no_rename": {"type": "boolean", "default": False},
"live_images_ksurl": {"type": "url"},
"live_images_target": {"type": "string"},
"live_images_release": {"$ref": "#/definitions/optional_string"},
"live_images_version": {"type": "string"},
"image_build_ksurl": {"type": "url"},
"image_build_target": {"type": "string"},
"image_build_release": {"$ref": "#/definitions/optional_string"},
"image_build_version": {"type": "string"},
"live_media_ksurl": {"type": "url"},
"live_media_target": {"type": "string"},
"live_media_release": {"$ref": "#/definitions/optional_string"},
"live_media_version": {"type": "string"},
"media_checksums": {
"$ref": "#/definitions/list_of_strings",
"default": ["md5", "sha1", "sha256"],
},
"media_checksum_one_file": {"type": "boolean", "default": False},
"media_checksum_base_filename": {"type": "string", "default": ""},
"filter_system_release_packages": {"type": "boolean", "default": True},
"keep_original_comps": {
"deprecated": "remove <groups> tag from respective variant in variants XML" # noqa: E501
},
"link_type": {
"type": "string",
"enum": [
"hardlink",
"copy",
"hardlink-or-copy",
"symlink",
"abspath-symlink",
],
"default": "hardlink-or-copy",
},
"product_id": {"$ref": "#/definitions/str_or_scm_dict"},
"product_id_allow_missing": {"type": "boolean", "default": False},
"product_id_allow_name_prefix": {"type": "boolean", "default": True},
# Deprecated in favour of regular local/phase/global setting.
"live_target": {"type": "string"},
"tree_arches": {"$ref": "#/definitions/list_of_strings", "default": []},
"tree_variants": {"$ref": "#/definitions/list_of_strings", "default": []},
"translate_paths": {"$ref": "#/definitions/string_pairs", "default": []},
"failable_deliverables": _variant_arch_mapping(
{"$ref": "#/definitions/list_of_strings"}
),
"extra_isos": {
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": {
"type": "array",
"items": {
"type": "object",
"properties": {
"include_variants": {"$ref": "#/definitions/strings"},
"extra_files": _one_or_list(
{
"type": "object",
"properties": {
"scm": {"type": "string"},
"repo": {"type": "string"},
"branch": {
"$ref": "#/definitions/optional_string"
},
"file": {"$ref": "#/definitions/strings"},
"dir": {"$ref": "#/definitions/strings"},
"target": {"type": "string"},
},
"additionalProperties": False,
}
),
"filename": {"type": "string"},
"volid": {"$ref": "#/definitions/strings"},
"arches": {"$ref": "#/definitions/list_of_strings"},
"failable_arches": {
"$ref": "#/definitions/list_of_strings"
},
"skip_src": {"type": "boolean", "default": False},
"inherit_extra_files": {
"type": "boolean",
"default": False,
},
"max_size": {"type": "number"},
},
"required": ["include_variants"],
"additionalProperties": False,
},
}
},
},
"live_media": {
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": {
"type": "array",
"items": {
"type": "object",
"properties": {
"install_tree_from": {"type": "string"},
"kickstart": {"type": "string"},
"ksversion": {"type": "string"},
"ksurl": {"type": "url"},
"version": {"type": "string"},
"scratch": {"type": "boolean"},
"skip_tag": {"type": "boolean"},
"name": {"type": "string"},
"subvariant": {"type": "string"},
"repo": {"$ref": "#/definitions/repos"},
"target": {"type": "string"},
"arches": {"$ref": "#/definitions/list_of_strings"},
"failable": {"$ref": "#/definitions/list_of_strings"},
"release": {"$ref": "#/definitions/optional_string"},
"nomacboot": {"type": "boolean"},
},
"required": ["name", "kickstart"],
"additionalProperties": False,
},
}
},
"additionalProperties": False,
},
"ostree": {
"anyOf": [
{
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": _one_or_list(
{
"type": "object",
"properties": {
"treefile": {"type": "string"},
"config_url": {"type": "string"},
"repo": {"$ref": "#/definitions/repos"},
"keep_original_sources": {"type": "boolean"},
"ostree_repo": {"type": "string"},
"arches": {
"$ref": "#/definitions/list_of_strings"
},
"failable": {
"$ref": "#/definitions/list_of_strings"
},
"update_summary": {"type": "boolean"},
"force_new_commit": {"type": "boolean"},
"unified_core": {"type": "boolean"},
"version": {"type": "string"},
"config_branch": {"type": "string"},
"tag_ref": {"type": "boolean"},
"ostree_ref": {"type": "string"},
},
"required": [
"treefile",
"config_url",
"repo",
"ostree_repo",
],
"additionalProperties": False,
}
),
},
"additionalProperties": False,
},
# Deprecated in favour of the dict version above.
_variant_arch_mapping(
{
"type": "object",
"properties": {
"treefile": {"type": "string"},
"config_url": {"type": "string"},
"repo": {
"$ref": "#/definitions/repos",
"alias": "extra_source_repos",
"append": ["repo_from", "source_repo_from"],
},
"keep_original_sources": {"type": "boolean"},
"ostree_repo": {"type": "string"},
"failable": {"$ref": "#/definitions/list_of_strings"},
"update_summary": {"type": "boolean"},
"force_new_commit": {"type": "boolean"},
"unified_core": {"type": "boolean"},
"version": {"type": "string"},
"config_branch": {"type": "string"},
"tag_ref": {"type": "boolean"},
"ostree_ref": {"type": "string"},
},
"required": ["treefile", "config_url", "ostree_repo"],
"additionalProperties": False,
}
),
]
},
"ostree_installer": _variant_arch_mapping(
{
"type": "object",
"properties": {
"repo": {"$ref": "#/definitions/repos"},
"release": {"$ref": "#/definitions/optional_string"},
"failable": {"$ref": "#/definitions/list_of_strings"},
"installpkgs": {"$ref": "#/definitions/list_of_strings"},
"add_template": {"$ref": "#/definitions/list_of_strings"},
"add_arch_template": {"$ref": "#/definitions/list_of_strings"},
"add_template_var": {"$ref": "#/definitions/list_of_strings"},
"add_arch_template_var": {
"$ref": "#/definitions/list_of_strings"
},
"rootfs_size": {"type": "string"},
"template_repo": {"type": "string"},
"template_branch": {"type": "string"},
"extra_runroot_pkgs": {"$ref": "#/definitions/list_of_strings"},
"skip_branding": {"type": "boolean"},
},
"additionalProperties": False,
}
),
"ostree_use_koji_plugin": {"type": "boolean", "default": False},
"ostree_installer_use_koji_plugin": {"type": "boolean", "default": False},
"ostree_installer_overwrite": {"type": "boolean", "default": False},
"live_images": _variant_arch_mapping(
_one_or_list({"$ref": "#/definitions/live_image_config"})
),
"image_build_allow_reuse": {"type": "boolean", "default": False},
"image_build": {
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": {
"type": "array",
"items": {
"type": "object",
"properties": {
"image-build": {
"type": "object",
"properties": {
"failable": {
"$ref": "#/definitions/list_of_strings"
},
"disc_size": {"type": "number"},
"distro": {"type": "string"},
"name": {"type": "string"},
"kickstart": {"type": "string"},
"ksurl": {"type": "url"},
"arches": {
"$ref": "#/definitions/list_of_strings"
},
"repo": {
"$ref": "#/definitions/repos",
"append": "repo_from",
},
"install_tree_from": {"type": "string"},
"subvariant": {"type": "string"},
"format": {
"anyOf": [
# The variant with explicit extension is deprecated. # noqa: E501
{"$ref": "#/definitions/string_pairs"},
{"$ref": "#/definitions/strings"},
]
},
},
},
"factory-parameters": {"type": "object"},
},
"required": ["image-build"],
"additionalProperties": False,
},
}
},
"additionalProperties": False,
},
"osbuild_target": {"type": "string"},
"osbuild_release": {"$ref": "#/definitions/optional_string"},
"osbuild_version": {"type": "string"},
"osbuild": {
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"version": {"type": "string"},
"distro": {"type": "string"},
"target": {"type": "string"},
# Only a single image_type can be specified
# https://github.com/osbuild/koji-osbuild/commit/c7252650814f82281ee57b598cb2ad970b580451
# https://github.com/osbuild/koji-osbuild/commit/f21a2de39b145eb94f3d49cb4d8775a33ba56752
"image_types": {
"oneOf": [
{
"type": "array",
"items": {"type": "string"},
"description": "Deprecated variant",
"minItems": 1,
"maxItems": 1,
},
{"type": "string"},
]
},
"arches": {"$ref": "#/definitions/list_of_strings"},
"release": {"type": "string"},
"repo": {"$ref": "#/definitions/list_of_strings"},
"failable": {"$ref": "#/definitions/list_of_strings"},
"subvariant": {"type": "string"},
"ostree_url": {"type": "string"},
"ostree_ref": {"type": "string"},
"ostree_parent": {"type": "string"},
"upload_options": {
"oneOf": [
# AWSEC2UploadOptions
{
"type": "object",
"additionalProperties": False,
"required": [
"region",
"share_with_accounts",
],
"properties": {
"region": {
"type": "string",
},
"snapshot_name": {
"type": "string",
},
"share_with_accounts": {
"type": "array",
"items": {"type": "string"},
},
},
},
# AWSS3UploadOptions
{
"type": "object",
"additionalProperties": False,
"required": ["region"],
"properties": {
"region": {"type": "string"}
},
},
# AzureUploadOptions
{
"type": "object",
"additionalProperties": False,
"required": [
"tenant_id",
"subscription_id",
"resource_group",
"location",
],
"properties": {
"tenant_id": {"type": "string"},
"subscription_id": {"type": "string"},
"resource_group": {"type": "string"},
"location": {"type": "string"},
"image_name": {
"type": "string",
},
},
},
# GCPUploadOptions
{
"type": "object",
"additionalProperties": False,
"required": ["region", "bucket"],
"properties": {
"region": {"type": "string"},
"bucket": {"type": "string"},
"image_name": {
"type": "string",
},
"share_with_accounts": {
"type": "array",
"items": {"type": "string"},
},
},
},
# ContainerUploadOptions
{
"type": "object",
"additionalProperties": False,
"properties": {
"name": {"type": "string"},
"tag": {"type": "string"},
},
},
]
},
},
"required": ["name", "distro", "image_types"],
"additionalProperties": False,
},
},
},
},
"lorax_options": _variant_arch_mapping(
{
"type": "object",
"properties": {
"bugurl": {"type": "string"},
"nomacboot": {"type": "boolean"},
"noupgrade": {"type": "boolean"},
"add_template": {"$ref": "#/definitions/list_of_strings"},
"add_arch_template": {"$ref": "#/definitions/list_of_strings"},
"add_template_var": {"$ref": "#/definitions/list_of_strings"},
"add_arch_template_var": {
"$ref": "#/definitions/list_of_strings"
},
"rootfs_size": {"type": "integer"},
"version": {"type": "string"},
"dracut_args": {"$ref": "#/definitions/list_of_strings"},
"skip_branding": {"type": "boolean"},
"squashfs_only": {"type": "boolean"},
"configuration_file": {"$ref": "#/definitions/str_or_scm_dict"},
},
"additionalProperties": False,
}
),
"lorax_extra_sources": _variant_arch_mapping(
{"$ref": "#/definitions/strings"}
),
"lorax_use_koji_plugin": {"type": "boolean", "default": False},
"signing_key_id": {"type": "string"},
"signing_key_password_file": {"type": "string"},
"signing_command": {"type": "string"},
"productimg": {
"deprecated": "remove it. Productimg phase has been removed"
},
"iso_size": {
"anyOf": [{"type": "string"}, {"type": "number"}],
"default": 4700000000,
},
"split_iso_reserve": {
"anyOf": [{"type": "string"}, {"type": "number"}],
"default": 10 * 1024 * 1024,
},
"osbs_allow_reuse": {"type": "boolean", "default": False},
"osbs": {
"type": "object",
"patternProperties": {
# Warning: this pattern is a variant uid regex, but the
# format does not let us validate it as there is no regular
# expression to describe all regular expressions.
".+": _one_or_list({"$ref": "#/definitions/osbs_config"}),
},
"additionalProperties": False,
},
"osbs_registries": {
"type": "object",
"patternProperties": {
# There are no restrictions on what the value can be.
".+": {}
},
"additionalProperties": False,
},
"image_container": {
"type": "object",
"patternProperties": {
".+": _one_or_list(
{
"type": "object",
"properties": {
"url": {"type": "url"},
"target": {"type": "string"},
"priority": {"type": "number"},
"failable": {"type": "boolean"},
"git_branch": {"type": "string"},
"image_spec": {"type": "object"},
},
"required": ["url", "target", "git_branch", "image_spec"],
}
),
},
"additionalProperties": False,
},
"extra_files": _variant_arch_mapping(
{
"type": "array",
"items": {
"type": "object",
"properties": {
"scm": {"type": "string"},
"repo": {"type": "string"},
"branch": {"$ref": "#/definitions/optional_string"},
"file": {"$ref": "#/definitions/strings"},
"dir": {"type": "string"},
"target": {"type": "string"},
},
"additionalProperties": False,
},
}
),
"gather_lookaside_repos": _variant_arch_mapping(
{"$ref": "#/definitions/strings"}
),
"variant_as_lookaside": {
"type": "array",
"items": {
"type": "array",
"items": [{"type": "string"}, {"type": "string"}],
"minItems": 2,
"maxItems": 2,
},
},
},
"required": [
"release_name",
"release_short",
"release_version",
"variants_file",
"pkgset_source",
"gather_method",
],
"additionalProperties": False,
}
def _variant_arch_mapping(value):
return {
"type": "array",
"items": {
"type": "array",
"items": [
{"type": "regex"},
{
"type": "object",
"patternProperties": {".+": value},
"additionalProperties": False,
},
],
"additionalItems": False,
"minItems": 2,
},
"default": [],
}
def _one_or_list(value):
"""Require either `value` or a list of `value`s."""
return {
"anyOf": [value, {"type": "array", "items": value}],
}
def get_num_cpus():
try:
return multiprocessing.cpu_count()
except NotImplementedError:
return 3
# This is a mapping of configuration option dependencies and conflicts.
#
# The key in this mapping is the trigger for the check. When the option is
# encountered and its value satisfies the lambda, an error is reported for each
# missing (for requires) option in the list.
CONFIG_DEPS = {
"buildinstall_method": {
"conflicts": (
(lambda val: val == "buildinstall", ["lorax_options"]),
(lambda val: not val, ["lorax_options", "buildinstall_kickstart"]),
),
},
"base_product_name": {
"requires": ((lambda x: x, ["base_product_short", "base_product_version"]),),
"conflicts": (
(lambda x: not x, ["base_product_short", "base_product_version"]),
),
},
"base_product_short": {
"requires": ((lambda x: x, ["base_product_name", "base_product_version"]),),
"conflicts": (
(lambda x: not x, ["base_product_name", "base_product_version"]),
),
},
"base_product_version": {
"requires": ((lambda x: x, ["base_product_name", "base_product_short"]),),
"conflicts": ((lambda x: not x, ["base_product_name", "base_product_short"]),),
},
"cts_url": {"requires": ((lambda x: x, ["translate_paths"]),)},
"product_id": {"conflicts": [(lambda x: not x, ["product_id_allow_missing"])]},
"pkgset_scratch_modules": {"requires": ((lambda x: x, ["mbs_api_url"]),)},
"pkgset_source": {
"requires": [(lambda x: x == "repos", ["pkgset_repos"])],
"conflicts": [
(lambda x: x == "koji", ["pkgset_repos"]),
(lambda x: x == "repos", ["pkgset_koji_tag", "pkgset_koji_inherit"]),
],
},
}
def update_schema(schema, update_dict):
"""
Updates the leaf values in `schema` by the leaf values from `update_dict`.
This is different from `schema.update(update_dict)`, because
the `schema.update()` would override also non-leaf values.
:param schema: Schema to update.
:param updated_dict: Dict matching the schema with updated leaf values.
:returns: Updated schema
"""
for k, v in update_dict.items():
if isinstance(v, dict):
schema[k] = update_schema(schema.get(k, {}), v)
else:
schema[k] = v
return schema
def _get_gather_backends():
if six.PY2:
return ["yum", "dnf"]
return ["dnf"]
def _get_default_gather_backend():
return "yum" if six.PY2 else "dnf"