Blob Blame Raw
# -*- coding: utf-8 -*-
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
__all__ = (
    "Compose",
)
import errno
import os
import time
import tempfile
import shutil
import kobo.log
from productmd.composeinfo import ComposeInfo
from productmd.images import Images
from pungi.wrappers.variants import VariantsXmlParser
from pungi.paths import Paths
from pungi.wrappers.scm import get_file_from_scm
from pungi.util import makedirs
from pungi.metadata import compose_to_composeinfo
def get_compose_dir(topdir, conf, compose_type="production", compose_date=None, compose_respin=None, compose_label=None, already_exists_callbacks=None):
    already_exists_callbacks = already_exists_callbacks or []
    # create an incomplete composeinfo to generate compose ID
    ci = ComposeInfo()
    ci.compose.name = conf["product_name"]
    ci.release.name = conf["product_name"]
    ci.compose.short = conf["product_short"]
    ci.release.short = conf["product_short"]
    ci.compose.version = conf["product_version"]
    ci.release.version = conf["product_version"]
    ci.compose.is_layered = bool(conf.get("product_is_layered", False))
    if ci.compose.is_layered:
        ci.base_product.name = conf["base_product_name"]
        ci.base_product.short = conf["base_product_short"]
        ci.base_product.version = conf["base_product_version"]
    ci.compose.label = compose_label
    ci.compose.type = compose_type
    ci.compose.date = compose_date or time.strftime("%Y%m%d", time.localtime())
    ci.compose.respin = compose_respin or 0
    while 1:
        ci.compose.id = ci.create_compose_id()
        compose_dir = os.path.join(topdir, ci.compose.id)
        exists = False
        # TODO: callbacks to determine if a composeid was already used
        # for callback in already_exists_callbacks:
        #     if callback(data):
        #         exists = True
        #         break
        # already_exists_callbacks fallback: does target compose_dir exist?
        if not exists:
            try:
                os.makedirs(compose_dir)
            except OSError as ex:
                if ex.errno == errno.EEXIST:
                    exists = True
                else:
                    raise
        if exists:
            ci.compose.respin += 1
            continue
        break
    open(os.path.join(compose_dir, "COMPOSE_ID"), "w").write(ci.compose.id)
    work_dir = os.path.join(compose_dir, "work", "global")
    makedirs(work_dir)
    ci.dump(os.path.join(work_dir, "composeinfo-base.json"))
    return compose_dir
class Compose(kobo.log.LoggingBase):
    def __init__(self, conf, topdir, debug=False, skip_phases=None, just_phases=None, old_composes=None, koji_event=None, supported=False, logger=None):
        kobo.log.LoggingBase.__init__(self, logger)
        # TODO: check if minimal conf values are set
        self.conf = conf
        self.variants = {}
        self.topdir = os.path.abspath(topdir)
        self.skip_phases = skip_phases or []
        self.just_phases = just_phases or []
        self.old_composes = old_composes or []
        self.koji_event = koji_event
        # intentionally upper-case (visible in the code)
        self.DEBUG = debug
        # path definitions
        self.paths = Paths(self)
        # to provide compose_id, compose_date and compose_respin
        self.ci_base = ComposeInfo()
        self.ci_base.load(os.path.join(self.paths.work.topdir(arch="global"), "composeinfo-base.json"))
        self.supported = supported
        if self.compose_label and self.compose_label.split("-")[0] == "RC":
            self.log_info("Automatically setting 'supported' flag for a Release Candidate (%s) compose." % self.compose_label)
            self.supported = True
        self.im = Images()
        if self.DEBUG:
            try:
                self.im.load(self.paths.compose.metadata("images.json"))
            except RuntimeError:
                pass
        self.im.compose.id = self.compose_id
        self.im.compose.type = self.compose_type
        self.im.compose.date = self.compose_date
        self.im.compose.respin = self.compose_respin
        self.im.metadata_path = self.paths.compose.metadata()
    get_compose_dir = staticmethod(get_compose_dir)
    def __getitem__(self, name):
        return self.variants[name]
    @property
    def compose_id(self):
        return self.ci_base.compose.id
    @property
    def compose_date(self):
        return self.ci_base.compose.date
    @property
    def compose_respin(self):
        return self.ci_base.compose.respin
    @property
    def compose_type(self):
        return self.ci_base.compose.type
    @property
    def compose_type_suffix(self):
        return self.ci_base.compose.type_suffix
    @property
    def compose_label(self):
        return self.ci_base.compose.label
    @property
    def has_comps(self):
        return bool(self.conf.get("comps_file", False))
    @property
    def config_dir(self):
        return os.path.dirname(self.conf._open_file or "")
    def read_variants(self):
        # TODO: move to phases/init ?
        variants_file = self.paths.work.variants_file(arch="global")
        msg = "Writing variants file: %s" % variants_file
        if self.DEBUG and os.path.isfile(variants_file):
            self.log_warning("[SKIP ] %s" % msg)
        else:
            scm_dict = self.conf["variants_file"]
            if isinstance(scm_dict, dict):
                file_name = os.path.basename(scm_dict["file"])
                if scm_dict["scm"] == "file":
                    scm_dict["file"] = os.path.join(self.config_dir, os.path.basename(scm_dict["file"]))
            else:
                file_name = os.path.basename(scm_dict)
                scm_dict = os.path.join(self.config_dir, os.path.basename(scm_dict))
            self.log_debug(msg)
            tmp_dir = tempfile.mkdtemp(prefix="variants_file_")
            get_file_from_scm(scm_dict, tmp_dir, logger=self._logger)
            shutil.copy2(os.path.join(tmp_dir, file_name), variants_file)
            shutil.rmtree(tmp_dir)
        file_obj = open(variants_file, "r")
        tree_arches = self.conf.get("tree_arches", None)
        self.variants = VariantsXmlParser(file_obj, tree_arches).parse()
        # populate ci_base with variants - needed for layered-products (compose_id)
        ####FIXME - compose_to_composeinfo is no longer needed and has been
        ####        removed, but I'm not entirely sure what this is needed for
        ####        or if it is at all
        self.ci_base = compose_to_composeinfo(self)
    def get_variants(self, types=None, arch=None, recursive=False):
        result = []
        types = types or ["variant", "optional", "addon", "layered-product"]
        for i in self.variants.values():
            if i.type in types:
                if arch and arch not in i.arches:
                    continue
                result.append(i)
            result.extend(i.get_variants(types=types, arch=arch, recursive=recursive))
        return sorted(set(result))
    def get_arches(self):
        result = set()
        tree_arches = self.conf.get("tree_arches", None)
        for variant in self.get_variants():
            for arch in variant.arches:
                if tree_arches:
                    if arch in tree_arches:
                        result.add(arch)
                else:
                    result.add(arch)
        return sorted(result)
    def write_status(self, stat_msg):
        if stat_msg not in ("STARTED", "FINISHED", "DOOMED"):
            self.log_warning("Writing nonstandard compose status: %s" % stat_msg)
        old_status = self.get_status()
        if stat_msg == old_status:
            return
        if old_status == "FINISHED":
            msg = "Could not modify a FINISHED compose: %s" % self.topdir
            self.log_error(msg)
            raise RuntimeError(msg)
        open(os.path.join(self.topdir, "STATUS"), "w").write(stat_msg + "\n")
    def get_status(self):
        path = os.path.join(self.topdir, "STATUS")
        if not os.path.isfile(path):
            return
        return open(path, "r").read().strip()