diff --git a/plugins/builder/osbuild.py b/plugins/builder/osbuild.py index 8205515..8ab98b6 100644 --- a/plugins/builder/osbuild.py +++ b/plugins/builder/osbuild.py @@ -1,38 +1,167 @@ - -import urllib.request +#!/usr/bin/python3 +import enum import json import sys import time +import urllib.parse +import urllib.request +from string import Template +from typing import Dict, List import koji from koji.tasks import BaseTaskHandler -def compose_request(distro, koji): - req = { - "distribution": distro, - "koji": { - "server": koji - }, - "image_requests": [{ - "architecture": "x86_64", - "image_type": "qcow2", - "repositories": [{ - "baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/32/Everything/x86_64/os/" - }] - }] - } +class Repository: + def __init__(self, baseurl: str, gpgkey: str = None): + self.baseurl = baseurl + self.gpgkey = gpgkey - return req + def as_dict(self, arch: str = ""): + tmp = Template(self.baseurl) + url = tmp.substitute(arch=arch) + res = {"baseurl": url} + if self.gpgkey: + res["gpgkey"] = self.gpgkey + return res + + +class ImageRequest: + def __init__(self, arch: str, image_type: str, repos: List): + self.architecture = arch + self.image_type = image_type + self.repositories = repos + + def as_dict(self): + arch = self.architecture + return { + "architecture": self.architecture, + "image_type": self.image_type, + "repositories": [ + repo.as_dict(arch) for repo in self.repositories + ] + } + + +class ComposeRequest: + def __init__(self, distro: str, images: ImageRequest, koji: str): + self.distribution = distro + self.image_requests = images + self.koji = koji + + def as_dict(self): + return { + "distribution": self.distribution, + "koji": { + "server": str(self.koji) + }, + "image_requests": [ + img.as_dict() for img in self.image_requests + ] + } + + def to_json(self, encoding=None): + data = json.dumps(self.as_dict()) + if encoding: + data = data.encode('utf-8') + return data + + +class ImageStatus(enum.Enum): + SUCCESS = "success" + FAILED = "failed" + PENDING = "pending" + BUILDING = "building" + UPLOADING = "uploading" + WAITING = "waiting" + FINISHED = "finished" + RUNNING = "running" + + +class ComposeStatus: + SUCCESS = "success" + FAILED = "failed" + PENDING = "pending" + RUNNING = "running" + WAITING = "waiting" + REGISTERING = "registering" + FINISHED = "finished" + + def __init__(self, status: str, images: List, koji_task_id: str): + self.status = status + self.images = images + self.koji_task_id = koji_task_id + + @classmethod + def from_dict(cls, data: Dict): + status = data["status"].lower() + koji_task_id = data["koji_task_id"] + images = [ImageStatus(s["status"].lower()) for s in data["image_statuses"]] + return cls(status, images, koji_task_id) + + @property + def is_finished(self): + if self.is_success: + return True + return self.status in [self.FAILED] + + @property + def is_success(self): + return self.status in [self.SUCCESS, self.FINISHED] + + +class Client: + def __init__(self, url): + self.url = url + + def compose_create(self, distro: str, images: List[ImageRequest], koji: str): + url = urllib.parse.urljoin(self.url, f"/compose") + req = urllib.request.Request(url) + cro = ComposeRequest(distro, images, koji) + dat = json.dumps(cro.as_dict()) + raw = dat.encode('utf-8') + req = urllib.request.Request(url, raw) + req.add_header('Content-Type', 'application/json') + req.add_header('Content-Length', len(raw)) + + with urllib.request.urlopen(req, raw) as res: + payload = res.read().decode('utf-8') + ps = json.loads(payload) + compose_id = ps["id"] + return compose_id + + def compose_status(self, compose_id: str): + url = urllib.parse.urljoin(self.url, f"/compose/{compose_id}") + req = urllib.request.Request(url) + with urllib.request.urlopen(req) as res: + data = res.read().decode('utf-8') + js = json.loads(data) + + return ComposeStatus.from_dict(js) + + def wait_for_compose(self, compose_id: str, *, sleep_time=2): + while True: + status = self.compose_status(compose_id) + if status.is_finished: + return status + + time.sleep(sleep_time) class OSBuildImage(BaseTaskHandler): Methods = ['osbuildImage'] _taskWeight = 2.0 + def __init__(self, task_id, method, params, session, options): + super().__init__(task_id, method, params, session, options) + + self.composer_url = "http://composer:8701/" + self.koji_url = "https://localhost/kojihub" + self.client = Client(self.composer_url) + def handler(self, name, version, arches, target, opts): - self.logger.debug("Building image %s, %s, %s, %s", + self.logger.debug("Building image via osbuild %s, %s, %s, %s", name, str(arches), str(target), str(opts)) #self.logger.debug("Event id: %s", str(self.event_id)) @@ -48,56 +177,37 @@ class OSBuildImage(BaseTaskHandler): #if buildconfig: # self.logger.debug("build-config: %s", str(buildconfig)) - # <<<>>> + client = self.client - cr = compose_request("fedora-32", "https://localhost/kojihub") - data = json.dumps(cr) + distro = f"{name}-{version}" + images = [] + formats = ["qcow2"] + repo_url = "http://download.fedoraproject.org/pub/fedora/linux/releases/32/Everything/$arch/os/" + repos = [Repository(repo_url)] + for fmt in formats: + for arch in arches: + ireq = ImageRequest(arch, fmt, repos) + images.append(ireq) - req = urllib.request.Request("http://composer:8701/compose") - req.add_header('Content-Type', 'application/json') - raw = data.encode('utf-8') - req.add_header('Content-Length', len(raw)) - with urllib.request.urlopen(req, raw) as res: - payload = res.read().decode('utf-8') - if res.status != 201: - self.logger.debug("Failed to create compose: %s", str(payload)) - return { - 'repositories': [], - 'koji_builds': [], - 'build': 'skipped', - } - ps = json.loads(payload) - compose_id = ps["id"] + self.logger.debug("Creating compose: %s\n koji: %s\n images: %s", + distro, self.koji_url, + str([i.as_dict() for i in images])) - req = urllib.request.Request(f"http://composer:8701/compose/{compose_id}") - while True: - with urllib.request.urlopen(req) as res: - payload = res.read().decode('utf-8') - if res.status != 200: - self.logger.debug("Failed to get compose status: %s", str(payload)) - return { - 'repositories': [], - 'koji_builds': [], - 'build': 'skipped', - } + cid = client.compose_create(distro, images, self.koji_url) + self.logger.info("Compose id: %s", cid) - ps = json.loads(payload) - status = ps["status"] - self.logger.debug("Compose status: %s", status) - if status != "RUNNING" and status != "WAITING": - break - time.sleep(2) + self.logger.debug("Waiting for comose to finish") + status = client.wait_for_compose(cid) - if status == "FAILED": - self.logger.debug("Compose failed: %s", str(payload)) + if not status.is_success: + self.logger.error("Compose failed: %s", str(status)) return { - 'repositories': [], - 'koji_builds': [], - 'build': 'skipped', + 'koji_builds': [] } return { - 'repositories': [], 'koji_builds': [], - 'build': f'{compose_id}-1', + 'build': f'{cid}-1-1', } + +