debian-forge-composer/dnf-json
Major Hayden acf91a4e54 🏃 Enable fastestmirror in dnf-json
The time it takes to depsolve a blueprint varies widely depending on
where the job is running and which mirrors are randomly chosen based on
the data returned in the metalink XML.

Use dnf's fastestmirror plugin to choose the fastest mirror for
downloading metadata. This returns consistent results in PSI + AWS and
every depsolve completed in under 60 seconds after 25 tests in each
cloud.

Fixes #845.

Signed-off-by: Major Hayden <major@redhat.com>
2020-07-10 12:20:02 -05:00

178 lines
5.8 KiB
Python
Executable file

#!/usr/bin/python3
import datetime
import dnf
import hashlib
import hawkey
import json
import shutil
import sys
import tempfile
DNF_ERROR_EXIT_CODE = 10
def timestamp_to_rfc3339(timestamp):
d = datetime.datetime.utcfromtimestamp(package.buildtime)
return d.strftime('%Y-%m-%dT%H:%M:%SZ')
def dnfrepo(desc, parent_conf=None):
"""Makes a dnf.repo.Repo out of a JSON repository description"""
repo = dnf.repo.Repo(desc["id"], parent_conf)
if "baseurl" in desc:
repo.baseurl = desc["baseurl"]
elif "metalink" in desc:
repo.metalink = desc["metalink"]
elif "mirrorlist" in desc:
repo.mirrorlist = desc["mirrorlist"]
else:
assert False
if desc.get("ignoressl", False):
repo.sslverify = False
if "sslcacert" in desc:
repo.sslcacert = desc["sslcacert"]
if "sslclientkey" in desc:
repo.sslclientkey = desc["sslclientkey"]
if "sslclientcert" in desc:
repo.sslclientcert = desc["sslclientcert"]
# In dnf, the default metadata expiration time is 48 hours. However,
# some repositories never expire the metadata, and others expire it much
# sooner than that. Therefore we must make this configurable. If nothing
# is provided, we default to never expiring the metadata, as hardcoding
# some arbitrary does not seem very helpful.
repo.metadata_expire = desc.get("metadata_expire", "-1")
return repo
def create_base(repos, module_platform_id, persistdir, cachedir, arch):
base = dnf.Base()
# Enable fastestmirror to ensure we choose the fastest mirrors for
# downloading metadata (when depsolving) and downloading packages.
base.conf.fastestmirror = True
# Initialize all plugins (including fastestmirror).
base.init_plugins()
# Try another mirror if it takes longer than 5 seconds to connect.
base.conf.timeout = 5
# Set the rest of the dnf configuration.
base.conf.module_platform_id = module_platform_id
base.conf.config_file_path = "/dev/null"
base.conf.persistdir = persistdir
base.conf.cachedir = cachedir
base.conf.substitutions['arch'] = arch
base.conf.substitutions['basearch'] = dnf.rpm.basearch(arch)
for repo in repos:
base.repos.add(dnfrepo(repo, base.conf))
base.fill_sack(load_system_repo=False)
return base
def exit_with_dnf_error(kind: str, reason: str):
json.dump({"kind": kind, "reason": reason}, sys.stdout)
sys.exit(DNF_ERROR_EXIT_CODE)
def repo_checksums(base):
checksums = {}
for repo in base.repos.iter_enabled():
# Uses the same algorithm as libdnf to find cache dir:
# https://github.com/rpm-software-management/libdnf/blob/master/libdnf/repo/Repo.cpp#L1288
if repo.metalink:
url = repo.metalink
elif repo.mirrorlist:
url = repo.mirrorlist
elif repo.baseurl:
url = repo.baseurl[0]
else:
assert False
digest = hashlib.sha256(url.encode()).hexdigest()[:16]
with open(f"{base.conf.cachedir}/{repo.id}-{digest}/repodata/repomd.xml", "rb") as f:
repomd = f.read()
checksums[repo.id] = "sha256:" + hashlib.sha256(repomd).hexdigest()
return checksums
call = json.load(sys.stdin)
command = call["command"]
arguments = call["arguments"]
repos = arguments.get("repos", {})
arch = arguments["arch"]
cachedir = arguments["cachedir"]
module_platform_id = arguments["module_platform_id"]
with tempfile.TemporaryDirectory() as persistdir:
try:
base = create_base(repos, module_platform_id, persistdir, cachedir, arch)
except dnf.exceptions.Error as e:
exit_with_dnf_error(type(e).__name__, f"Error occurred when setting up repo: {e}")
if command == "dump":
packages = []
for package in base.sack.query().available():
packages.append({
"name": package.name,
"summary": package.summary,
"description": package.description,
"url": package.url,
"epoch": package.epoch,
"version": package.version,
"release": package.release,
"arch": package.arch,
"buildtime": timestamp_to_rfc3339(package.buildtime),
"license": package.license
})
json.dump({
"checksums": repo_checksums(base),
"packages": packages
}, sys.stdout)
elif command == "depsolve":
errors = []
try:
base.install_specs(arguments["package-specs"], exclude=arguments.get("exclude-specs", []))
except dnf.exceptions.MarkingErrors as e:
exit_with_dnf_error("MarkingErrors", f"Error occurred when marking packages for installation: {e}")
try:
base.resolve()
except dnf.exceptions.DepsolveError as e:
exit_with_dnf_error("DepsolveError", f"There was a problem depsolving {arguments['package-specs']}: {e}")
dependencies = []
for tsi in base.transaction:
# avoid using the install_set() helper, as it does not guarantee a stable order
if tsi.action not in dnf.transaction.FORWARD_ACTIONS:
continue
package = tsi.pkg
dependencies.append({
"name": package.name,
"epoch": package.epoch,
"version": package.version,
"release": package.release,
"arch": package.arch,
"repo_id": package.reponame,
"path": package.relativepath,
"remote_location": package.remote_location(),
"checksum": f"{hawkey.chksum_name(package.chksum[0])}:{package.chksum[1].hex()}",
})
json.dump({
"checksums": repo_checksums(base),
"dependencies": dependencies
}, sys.stdout)