#!/usr/bin/python3
"""Fetch OSTree commits from an repository
Uses ostree to pull specific commits from (remote) repositories
at the provided `url`. Can verify the commit, if one or more
gpg keys are provided via `gpgkeys`.
"""
import json
import os
import sys
import subprocess
import uuid
SCHEMA = """
"additionalProperties": false,
"definitions": {
"item": {
"description": "The commits to fetch indexed their checksum",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"[0-9a-f]{5,64}": {
"type": "object",
"additionalProperties": false,
"required": ["remote"],
"properties": {
"remote": {
"type": "object",
"additionalProperties": false,
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "URL of the repository."
},
"gpgkeys": {
"type": "array",
"items": {
"type": "string",
"description": "GPG keys to verify the commits"
}
}
}
}
}
}
}
}
},
"properties": {
"items": {"$ref": "#/definitions/item"},
"commits": {"$ref": "#/definitions/item"}
},
"oneOf": [{
"required": ["items"]
}, {
"required": ["commits"]
}]
"""
def ostree(*args, _input=None, **kwargs):
args = list(args) + [f'--{k}={v}' for k, v in kwargs.items()]
print("ostree " + " ".join(args), file=sys.stderr)
subprocess.run(["ostree"] + args,
encoding="utf-8",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
input=_input,
check=True)
def download(commits, checksums, cache):
# Prepare the cache and the output repo
repo_cache = os.path.join(cache, "repo")
ostree("init", mode="archive", repo=repo_cache)
# Make sure the cache repository uses locks to protect the metadata during
# shared access. This is the default since `2018.5`, but lets document this
# explicitly here.
ostree("config", "set", "repo.locking", "true", repo=repo_cache)
for commit in checksums:
remote = commits[commit]["remote"]
url = remote["url"]
gpg = remote.get("gpgkeys", [])
uid = str(uuid.uuid4())
verify_args = []
if not gpg:
verify_args = ["--no-gpg-verify"]
ostree("remote", "add",
uid, url,
*verify_args,
repo=repo_cache)
for key in gpg:
ostree("remote", "gpg-import", "--stdin", uid,
repo=repo_cache, _input=key)
# Transfer the commit: remote → cache
print(f"pulling {commit}", file=sys.stderr)
ostree("pull", uid, commit, repo=repo_cache)
# Remove the temporary remotes again
ostree("remote", "delete", uid,
repo=repo_cache)
def export(checksums, cache, output):
repo_cache = os.path.join(cache, "repo")
repo_out = os.path.join(output, "repo")
ostree("init", mode="archive", repo=repo_out)
for commit in checksums:
# Transfer the commit: remote → cache
print(f"exporting {commit}", file=sys.stderr)
ostree("pull-local", repo_cache, commit,
repo=repo_out)
json.dump({}, sys.stdout)
def main(commits, options, checksums, cache, output):
cache = os.path.join(cache, "org.osbuild.ostree")
download_only = not output
if not commits:
commits = options.get("commits", {})
if commits:
if not checksums and download_only:
checksums = [k for k, _ in commits.items()]
os.makedirs(cache, exist_ok=True)
try:
download(commits, checksums, cache)
except subprocess.CalledProcessError as e:
output = e.output.strip()
json.dump({"error": output}, sys.stdout)
return 1
if download_only:
json.dump({}, sys.stdout)
return 0
os.makedirs(output, exist_ok=True)
export(checksums, cache, output)
return 0
if __name__ == '__main__':
source_args = json.load(sys.stdin)
r = main(source_args["items"],
source_args["options"],
source_args["checksums"],
source_args["cache"],
source_args.get("output"))
sys.exit(r)