stages/dnf: drop stage

This has now been entirely replaced by the rpm stage in all its
users. The dnf stage does not fit very nicely into the osbuild
module, in particular it requires direct network access, which
we would like to avoid.

Signed-off-by: Tom Gundersen <teg@jklm.no>
This commit is contained in:
Tom Gundersen 2020-04-15 03:04:09 +02:00
parent 7e80ca9bbe
commit 892342b978

View file

@ -1,313 +0,0 @@
#!/usr/bin/python3
import contextlib
import hashlib
import json
import os
import pathlib
import subprocess
import sys
import tempfile
import osbuild.sources
STAGE_DESC = "Install packages using DNF"
STAGE_INFO = """
Depsolves, downloads, and installs packages (and dependencies) using DNF.
Writes the `repos` into `/tmp/dnf.conf`, does some tree setup, and then runs
the buildhost's `dnf` command with `--installroot`, plus the following
arguments generated from the stage options:
* `--forcearch {basearch}`
* `--releasever {releasever}`
* `--setopt install_weak_deps={install_weak_deps}`
* `--setopt fastestmirror={fastestmirror}`
* `--config /tmp/dnf.conf`
* `--exclude {pkg}` for each item in `exclude_packages`
Also disables the "generate_completion_cache" plugin, and sets `reposdir` to ""
to ensure the buildhost's repo files are *not* being used.
To prepare the tree, this stage sets `/etc/machine-id` to "ffff..." (32 chars)
and bind-mounts `/dev`, `/sys`, and `/proc` from the buildhost into the tree.
Each repo listed in `repos` needs to have a `checksum` and at least one of
`mirrorlist`, `metalink`, or `baseurl`. If a `gpgkey` is provided, `gpgcheck`
will be turned on for that repo, and DNF will exit with an error unless every
package downloaded from that repo is signed by one of the trusted `gpgkey`s.
The provided `checksum` must start with "sha256:" and then have the hex-encoded
SHA256 of the repo's `repomd.xml` file. If the metadata for any repo has
changed and no longer matches `checksum`, this stage will fail after package
installation.
NOTE: Any pipeline that uses a repo that changes frequently (like Fedora's
"updates") will quickly become un-reproduceable. This is an unavoidable
consequence of Fedora removing "out-of-date" metadata and packages: it's
impossible to reproduce a build that requires files that have been deleted.
To quote Douglas Adams: "We apologize for the inconvenience."
After DNF finishes, this stage cleans up the tree by removing
`/etc/machine-id`, `/var/lib/systemd/random-seed`, and everything under
`/var/cache/dnf`.
Buildhost commands used: `/bin/sh`, `dnf`, `mkdir`, `mount`, `chmod`.
"""
STAGE_OPTS = """
"required": ["repos", "packages", "releasever", "basearch"],
"properties": {
"repos": {
"description": "Array of checksums to repository sources or repo objects",
"type": "array",
"minItems": 1,
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"metalink": {
"description": "metalink URL for this repo",
"type": "string"
},
"mirrorlist": {
"description": "mirrorlist URL for this repo",
"type": "string"
},
"baseurl": {
"description": "baseurl for this repo",
"type": "string"
},
"checksum": {
"description": "checksum for the expected repo metadata",
"type": "string",
"pattern": "sha256:[a-fA-F0-9]{32}"
},
"gpgkey": {
"description": "GPG public key contents (to check signatures)",
"type": "string"
}
},
"anyOf": [
{"required": ["checksum", "metalink"]},
{"required": ["checksum", "mirrorlist"]},
{"required": ["checksum", "baseurl"]}
]
},
{
"type": "string"
}
]
}
},
"packages": {
"description": "List of package-specs to pass to DNF",
"type": "array",
"minItems": 1,
"items": { "type": "string" }
},
"releasever": {
"description": "DNF $releasever value",
"type": "string"
},
"basearch": {
"description": "DNF $basearch value",
"type": "string"
},
"operation": {
"description": "DNF command to use",
"type": "string",
"default": "install"
},
"install_weak_deps": {
"description": "Whether DNF should install weak deps",
"type": "boolean",
"default": true
},
"fastestmirror": {
"description": "Whether DNF should choose the fastest mirror available",
"type": "boolean",
"default": true
},
"exclude_packages": {
"description": "List of package-specs to --exclude",
"type": "array",
"items": { "type": "string" },
"default": []
},
"module_platform_id": {
"description": "DNF's module_platform_id option. Corresponds to PLATFORM_ID from /etc/os-release",
"type": "string"
}
}
"""
def fetch_repos(repos):
"""Use osbuild's source api to fill in details about those repos in @repos
that only have a "checksum" set. All others are passed through unchanged.
"""
checksums = []
result = []
for repo in repos:
if isinstance(repo, str):
checksums.append(repo)
else:
result.append(repo)
return result + osbuild.sources.get("org.osbuild.dnf", checksums)
def write_repofile(f, repoid, repo, keydir):
f.write(f"[{repoid}]\n")
def write_option(key, value):
f.write(f"{key}={value}\n")
# silence dnf warning about missing name
write_option("name", repoid)
for key in ("metalink", "mirrorlist", "baseurl"):
value = repo.get(key)
if value:
write_option(key, value)
for cert in ("sslcacert", "sslclientcert", "sslclientkey"):
if cert in repo:
path = f"{keydir}/{cert}.pem"
with open(path, "w") as certfile:
certfile.write(repo[cert])
write_option(cert, path)
if "gpgkey" in repo:
keyfile = f"{keydir}/{repoid}.asc"
with open(keyfile, "w") as key:
key.write(repo["gpgkey"])
write_option("gpgcheck", 1)
write_option("gpgkey", f"file://{keyfile}")
def dnf_cachedir(repoid, repo, releasever, basearch):
"""Return the relative cache directory for a repository.
Using the same algorithm as libdnf:
https://github.com/rpm-software-management/libdnf/blob/master/libdnf/repo/Repo.cpp#L1288
"""
if "metalink" in repo:
url = repo["metalink"]
elif "mirrorlist" in repo:
url = repo["mirrorlist"]
elif "baseurl" in repo:
url = repo["baseurl"]
else:
raise RuntimeError(f"one of metalink, mirrorlist, or baseurl must be given for repository '{repoid}'")
url = url.replace("$basearch", basearch).replace("$releasever", releasever)
digest = hashlib.sha256(url.encode()).hexdigest()[:16]
return f"{repoid}-{digest}"
def main(tree, options):
repos = fetch_repos(options["repos"])
packages = options["packages"]
releasever = options["releasever"]
basearch = options["basearch"]
operation = options.get("operation", "install")
weak_deps = options.get("install_weak_deps", True)
fastestmirror = options.get("fastestmirror", True)
exclude_packages = options.get("exclude_packages", [])
module_platform_id = options.get("module_platform_id", None)
script = f"""
set -e
mkdir -p {tree}/dev {tree}/sys {tree}/proc
mount -o bind /dev {tree}/dev
mount -o bind /sys {tree}/sys
mount -o bind /proc {tree}/proc
"""
machine_id_set_previously = os.path.exists(f"{tree}/etc/machine-id")
if not machine_id_set_previously:
# create a fake machine ID to improve reproducibility
print("creating a fake machine id")
script += f"""
mkdir -p {tree}/etc
echo "ffffffffffffffffffffffffffffffff" > {tree}/etc/machine-id
chmod 0444 {tree}/etc/machine-id
"""
try:
subprocess.run(["/bin/sh", "-c", script], check=True)
except subprocess.CalledProcessError as err:
print(f"setting up API VFS in target tree failed: {err.returncode}")
return err.returncode
with tempfile.TemporaryDirectory(prefix="org.osbuild.dnf.") as confdir:
dnfconf = f"{confdir}/dnf.conf"
with open(dnfconf, "w") as conf:
if module_platform_id:
conf.write("[main]\n")
conf.write(f"module_platform_id={module_platform_id}\n")
for num, repo in enumerate(repos):
write_repofile(conf, f"repo{num}", repo, confdir)
base_cmd = [
"dnf", "-yv",
"--installroot", tree,
"--forcearch", basearch,
"--setopt", "reposdir=",
"--setopt", f"install_weak_deps={weak_deps}",
"--setopt", f"fastestmirror={fastestmirror}",
"--setopt", f"skip_if_unavailable=false",
"--releasever", releasever,
"--noplugins",
"--config", dnfconf
]
cmd = base_cmd + [operation] + packages
for x in exclude_packages:
cmd += ["--exclude", x]
print(" ".join(cmd), flush=True)
subprocess.run(cmd, check=True)
# verify metadata checksum
for repoid, repo in enumerate(repos):
algorithm, expected_checksum = repo["checksum"].split(":")
assert algorithm == "sha256"
cachedir = dnf_cachedir(f"repo{repoid}", repo, releasever, basearch)
with open(f"{tree}/var/cache/dnf/{cachedir}/repodata/repomd.xml", "rb") as f:
repomd = f.read()
checksum = hashlib.sha256(repomd).hexdigest()
if checksum != expected_checksum:
print(f"error: repo was configured with checksum {expected_checksum}, but actually got {checksum}")
return 1
# delete cache manually, because `dnf clean all` leaves some contents behind
fd = os.open(f"{tree}/var/cache/dnf", os.O_DIRECTORY)
for _, dirs, files, dirfd in os.fwalk(".", topdown=False, dir_fd=fd):
for name in files:
os.unlink(name, dir_fd=dirfd)
for name in dirs:
os.rmdir(name, dir_fd=dirfd)
os.close(fd)
# remove temporary machine ID if it was created by us
if not machine_id_set_previously:
print("deleting the fake machine id")
machine_id_file = pathlib.Path(f"{tree}/etc/machine-id")
machine_id_file.unlink()
machine_id_file.touch()
# remove random seed from the tree if exists
with contextlib.suppress(FileNotFoundError):
os.unlink(f"{tree}/var/lib/systemd/random-seed")
return 0
if __name__ == '__main__':
args = json.load(sys.stdin)
r = main(args["tree"], args["options"])
sys.exit(r)