Add new stage for creating YUM / DNF repo files

Add a new stage `org.osbuild.yum.repos` for creating YUM / DNF `.repo`
files in `/etc/yum.repos.d`. All repo-specific options are supported but
only a subset of options which can be set for a repo as well as in the
[main] section are supported.

Add unit test for the new stage.

Fix #907

Signed-off-by: Tomas Hozza <thozza@redhat.com>
This commit is contained in:
Tomas Hozza 2021-12-07 21:03:04 +01:00 committed by Christian Kellner
parent f965ca8510
commit cd4ac1c75a
6 changed files with 1879 additions and 0 deletions

193
stages/org.osbuild.yum.repos Executable file
View file

@ -0,0 +1,193 @@
#!/usr/bin/python3
"""
Create YUM / DNF repo file in /etc/yum.repos.d
All repo-specific options, except the 'type' option, are supported. The 'type'
repo options is not supported, since it accepts only a single value, therefore
the ability to set it adds no value.
Only a subset of options which can be used in both, a repo or [main] section
configuration, is supported, specifically:
- gpgcheck
- repo_gpgcheck
"""
import os
import sys
import configparser
import osbuild.api
SCHEMA = r"""
"definitions": {
"repo": {
"type": "object",
"additionalProperties": false,
"oneOf": [
{
"required": ["id", "baseurl"]
},
{
"required": ["id", "metalink"]
},
{
"required": ["id", "mirrorlist"]
}
],
"description": "YUM / DNF repo definition.",
"properties": {
"id": {
"type": "string",
"description": "Repository ID.",
"pattern": "^[\\w.\\-:]+$"
},
"baseurl": {
"type": "array",
"description": "List of URLs for the repository.",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"cost": {
"type": "integer",
"description": "The relative cost of accessing this repository, defaulting to 1000."
},
"enabled": {
"type": "boolean",
"description": "Include this repository as a package source."
},
"gpgkey": {
"type": "array",
"description": "URLs of a GPG key files that can be used for signing metadata and packages of this repository.",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"metalink": {
"type": "string",
"description": "URL of a metalink for the repository.",
"minLength": 1
},
"mirrorlist": {
"type": "string",
"description": "URL of a mirrorlist for the repository.",
"minLength": 1
},
"module_hotfixes": {
"type": "boolean",
"description": "Set this to True to disable module RPM filtering and make all RPMs from the repository available."
},
"name": {
"type": "string",
"description": "A human-readable name of the repository. Defaults to the ID of the repository.",
"minLength": 1
},
"priority": {
"type": "integer",
"description": "The priority value of this repository."
},
"gpgcheck": {
"type": "boolean",
"description": "Whether to perform GPG signature check on packages found in this repository."
},
"repo_gpgcheck": {
"type": "boolean",
"description": "Whether to perform GPG signature check on this repository's metadata."
}
}
}
},
"additionalProperties": false,
"description": "YUM / DNF repo file configuration.",
"properties": {
"filename": {
"type": "string",
"pattern": "^[\\w.-]{1,250}\\.repo$",
"description": "Repo file name."
},
"repos": {
"type": "array",
"description": "YUM / DNF repo definitions.",
"minItems": 1,
"items": {
"$ref": "#/definitions/repo"
}
}
}
"""
# List of repo options which should be listed in this specific order if set
# in the stage options.
#
# Reasoning: repo configurations as shipped by distributions or created by
# various tools (COPR, RHSM) tend to order some options in a specific way,
# therefore if we just iterated over the dictionary items, the order would
# be different than how are repository configurations usually structured.
SPECIFIC_ORDER_OPTIONS = [
"name",
"baseurl",
"metalink",
"mirrorlist",
"enabled",
"gpgcheck",
"repo_gpgcheck",
"gpgkey"
]
def option_value_to_str(value):
"""
Convert allowed types of option values to string.
DNF allows string lists as a option value.
'dnf.conf' man page says:
"list It is an option that could represent one or more strings separated by space or comma characters."
"""
if isinstance(value, list):
value = " ".join(value)
elif isinstance(value, bool):
value = "1" if value else "0"
elif not isinstance(value, str):
value = str(value)
return value
def main(tree, options):
filename = options.get("filename")
repos = options.get("repos")
yum_repos_dir = f"{tree}/etc/yum.repos.d"
os.makedirs(yum_repos_dir, exist_ok=True)
parser = configparser.ConfigParser()
for repo in repos:
repo_id = repo.pop("id")
parser.add_section(repo_id)
# Set some options in a specific order in which they tend to be
# written in repo files.
for option in SPECIFIC_ORDER_OPTIONS:
option_value = repo.pop(option, None)
if option_value is not None:
parser.set(repo_id, option, option_value_to_str(option_value))
for key, value in repo.items():
parser.set(repo_id, key, option_value_to_str(value))
# ensure that we won't overwrite an existing file
with open(f"{yum_repos_dir}/{filename}", "x") as f:
parser.write(f, space_around_delimiters=False)
return 0
if __name__ == '__main__':
args = osbuild.api.arguments()
r = main(args["tree"], args["options"])
sys.exit(r)