240 lines
7.9 KiB
Python
Executable file
240 lines
7.9 KiB
Python
Executable file
#!/usr/bin/python3
|
|
# pylint: disable=invalid-name
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
import os
|
|
|
|
import requests
|
|
|
|
|
|
# Composer API for Koji uses a slightly different repository format
|
|
# that osbuild-composer does in /usr/share/osbuild-composer/repositories.
|
|
#
|
|
# This function does the conversion.
|
|
def composer_repository_to_koji_repository(repository):
|
|
koji_repository = {
|
|
"baseurl": repository["baseurl"]
|
|
}
|
|
|
|
if repository.get("check_gpg", False):
|
|
koji_repository["gpgkey"] = repository["gpgkey"]
|
|
|
|
if "cdn.redhat.com" in koji_repository["baseurl"]:
|
|
koji_repository["rhsm"] = True
|
|
|
|
return koji_repository
|
|
|
|
|
|
def compose_request(distro, koji, arch):
|
|
with open(f"/usr/share/tests/osbuild-composer/repositories/{distro}.json", encoding="utf-8") as f:
|
|
test_repositories = json.load(f)
|
|
|
|
repositories = [composer_repository_to_koji_repository(repo) for repo in test_repositories[arch]]
|
|
image_requests = [
|
|
{
|
|
"architecture": arch,
|
|
"image_type": "guest-image",
|
|
"repositories": repositories
|
|
},
|
|
{
|
|
"architecture": arch,
|
|
"image_type": "aws",
|
|
"repositories": repositories
|
|
}
|
|
]
|
|
|
|
req = {
|
|
"distribution": distro,
|
|
"koji": {
|
|
"server": koji,
|
|
"task_id": 1,
|
|
"name": "name",
|
|
"version": "version",
|
|
"release": "release",
|
|
},
|
|
"image_requests": image_requests
|
|
}
|
|
|
|
return req
|
|
|
|
|
|
def upload_options_by_cloud_target(cloud_target):
|
|
if cloud_target == "aws":
|
|
return {
|
|
# the snapshot name is currently not set for Koji composes
|
|
# "snapshot_name": "",
|
|
"region": os.getenv("AWS_REGION"),
|
|
"share_with_accounts": [os.getenv("AWS_API_TEST_SHARE_ACCOUNT")]
|
|
}
|
|
|
|
if cloud_target == "azure":
|
|
return {
|
|
# image name is currently not set for Koji composes
|
|
# "image_name": "",
|
|
"location": os.getenv("AZURE_LOCATION"),
|
|
"resource_group": os.getenv("AZURE_RESOURCE_GROUP"),
|
|
"subscription_id": os.getenv("AZURE_SUBSCRIPTION_ID"),
|
|
"tenant_id": os.getenv("AZURE_TENANT_ID")
|
|
}
|
|
|
|
if cloud_target == "gcp":
|
|
return {
|
|
# image name is currently not set for Koji composes
|
|
# "image_name": "",
|
|
"bucket": os.getenv("GCP_BUCKET"),
|
|
"region": os.getenv("GCP_REGION"),
|
|
"share_with_accounts": [os.getenv("GCP_API_TEST_SHARE_ACCOUNT")]
|
|
}
|
|
|
|
raise RuntimeError(f"unsupported target cloud: {cloud_target}")
|
|
|
|
|
|
def compose_request_cloud_upload(distro, koji, arch, cloud_target, image_type):
|
|
with open(f"/usr/share/tests/osbuild-composer/repositories/{distro}.json", encoding="utf-8") as f:
|
|
test_repositories = json.load(f)
|
|
|
|
repositories = [composer_repository_to_koji_repository(repo) for repo in test_repositories[arch]]
|
|
image_requests = [
|
|
{
|
|
"architecture": arch,
|
|
"image_type": image_type,
|
|
"repositories": repositories,
|
|
"upload_options": upload_options_by_cloud_target(cloud_target)
|
|
},
|
|
]
|
|
|
|
req = {
|
|
"distribution": distro,
|
|
"koji": {
|
|
"server": koji,
|
|
"task_id": 1,
|
|
"name": "name",
|
|
"version": "version",
|
|
"release": "release",
|
|
},
|
|
"image_requests": image_requests
|
|
}
|
|
|
|
return req
|
|
|
|
|
|
# Client for the Composer API.
|
|
class ComposerAPIClient:
|
|
|
|
def __init__(self, api_url, refresh_token, auth_server):
|
|
self.api_url = api_url
|
|
self.refresh_token = refresh_token
|
|
self.auth_server = auth_server
|
|
|
|
def access_token(self):
|
|
resp = requests.post(self.auth_server + "/token", data={
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": self.refresh_token,
|
|
}, timeout=5)
|
|
if resp.status_code != 200:
|
|
raise RuntimeError(f"failed to refresh token: {resp.text}")
|
|
return resp.json()["access_token"]
|
|
|
|
def submit_compose(self, request):
|
|
return requests.post(self.api_url + "/compose", json=request,
|
|
headers={"Authorization": f"Bearer {self.access_token()}"},
|
|
timeout=5)
|
|
|
|
def compose_status(self, compose_id):
|
|
return requests.get(self.api_url + f"/composes/{compose_id}",
|
|
headers={"Authorization": f"Bearer {self.access_token()}"},
|
|
timeout=5)
|
|
|
|
def compose_log(self, compose_id):
|
|
return requests.get(self.api_url + f"/composes/{compose_id}/logs",
|
|
headers={"Authorization": f"Bearer {self.access_token()}"},
|
|
timeout=5)
|
|
|
|
|
|
def get_parser():
|
|
parser = argparse.ArgumentParser(description="Koji compose test")
|
|
parser.add_argument("distro", metavar="DISTRO", help="Distribution to build")
|
|
parser.add_argument("arch", metavar="ARCH", help="Architecture to build")
|
|
parser.add_argument("--refresh-token", metavar="TOKEN", help="JWT refresh token. " +
|
|
"If not provided, read from /etc/osbuild-worker/token")
|
|
parser.add_argument("--auth-server", default="http://localhost:8081", help="Auth server URL")
|
|
parser.add_argument("--koji-url", default="https://localhost:4343/kojihub", help="Koji server to use")
|
|
parser.add_argument("--composer-url", default="http://localhost:443/api/image-builder-composer/v2",
|
|
help="Composer API server to use")
|
|
|
|
cloud_upload_group = parser.add_argument_group("Cloud upload options")
|
|
cloud_upload_group.add_argument("cloud_target", metavar="CLOUD-TARGET", nargs="?", help="Cloud target to use")
|
|
cloud_upload_group.add_argument("image_type", metavar="IMAGE-TYPE", nargs="?", help="Image type to build")
|
|
|
|
return parser
|
|
|
|
|
|
def main():
|
|
parser = get_parser()
|
|
args = parser.parse_args()
|
|
|
|
if args.cloud_target and not args.image_type or not args.cloud_target and args.image_type:
|
|
parser.error("CLOUD-TARGET and IMAGE-TYPE must be used together")
|
|
|
|
refresh_token = args.refresh_token
|
|
if not refresh_token:
|
|
with open("/etc/osbuild-worker/token", encoding="utf-8") as f:
|
|
refresh_token = f.read().strip()
|
|
|
|
composer_api_client = ComposerAPIClient(args.composer_url, refresh_token, args.auth_server)
|
|
|
|
if args.cloud_target is not None:
|
|
cr = compose_request_cloud_upload(args.distro, args.koji_url, args.arch, args.cloud_target, args.image_type)
|
|
else:
|
|
cr = compose_request(args.distro, args.koji_url, args.arch)
|
|
|
|
print(json.dumps(cr), file=sys.stderr)
|
|
|
|
r = composer_api_client.submit_compose(cr)
|
|
if r.status_code != 201:
|
|
print("Failed to create compose", file=sys.stderr)
|
|
print(r.text, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(r.text, file=sys.stderr)
|
|
compose_id = r.json()["id"]
|
|
print(compose_id)
|
|
|
|
while True:
|
|
r = composer_api_client.compose_status(compose_id)
|
|
if r.status_code != 200:
|
|
print("Failed to get compose status", file=sys.stderr)
|
|
print(r.text, file=sys.stderr)
|
|
sys.exit(1)
|
|
status = r.json()["status"]
|
|
print(status, file=sys.stderr)
|
|
|
|
if status == "success":
|
|
print("Compose worked!", file=sys.stderr)
|
|
print(r.text, file=sys.stderr)
|
|
break
|
|
|
|
if status == "failure":
|
|
print("compose failed!", file=sys.stderr)
|
|
print(r.text, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if status not in ("pending", "running"):
|
|
print(f"unexpected status: {status}", file=sys.stderr)
|
|
print(r.text, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
time.sleep(10)
|
|
|
|
r = composer_api_client.compose_log(compose_id)
|
|
logs = r.json()
|
|
assert "image_builds" in logs
|
|
assert isinstance(logs["image_builds"], list)
|
|
assert len(logs["image_builds"]) == len(cr["image_requests"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|