# Copyright (C) 2018-2019 Red Hat, Inc.
#
# 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; either version 2 of the License, or
# (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
""" Setup for composing an image

Adding New Output Types
-----------------------

The new output type must add a kickstart template to ./share/composer/ where the
name of the kickstart (without the trailing .ks) matches the entry in compose_args.

The kickstart should not have any url or repo entries, these will be added at build
time. The %packages section should be the last thing, and while it can contain mandatory
packages required by the output type, it should not have the trailing %end because the
package NEVRAs will be appended to it at build time.

compose_args should have a name matching the kickstart, and it should set the novirt_install
parameters needed to generate the desired output. Other types should be set to False.

"""
from __future__ import print_function
import logging
log = logging.getLogger("lorax-composer")

import os
from glob import glob
from StringIO import StringIO
from math import ceil
import pytoml as toml
import shutil
from uuid import uuid4

from pyanaconda.simpleconfig import SimpleConfigFile

# Use pykickstart to calculate disk image size
from pykickstart.parser import KickstartParser
from pykickstart.version import makeVersion, RHEL7

from pylorax.api.projects import projects_depsolve, projects_depsolve_with_size, dep_nevra
from pylorax.api.projects import ProjectsError
from pylorax.api.recipes import read_recipe_and_id
from pylorax.api.timestamp import TS_CREATED, write_timestamp
from pylorax.executils import runcmd_output
from pylorax.imgutils import default_image_name
from pylorax.sysutils import joinpaths


def test_templates(yb, share_dir):
    """ Try depsolving each of the the templates and report any errors

    :param yb: yum base object
    :type yb: YumBase
    :returns: List of template types and errors
    :rtype: List of errors

    Return a list of templates and errors encountered or an empty list
    """
    template_errors = []
    for compose_type in compose_types(share_dir):
        # Read the kickstart template for this type
        ks_template_path = joinpaths(share_dir, "composer", compose_type) + ".ks"
        ks_template = open(ks_template_path, "r").read()

        # How much space will the packages in the default template take?
        ks_version = makeVersion(RHEL7)
        ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
        ks.readKickstartFromString(ks_template+"\n%end\n")
        pkgs = [(name, "*") for name in ks.handler.packages.packageList]
        grps = [grp.name for grp in ks.handler.packages.groupList]
        try:
            _ = projects_depsolve(yb, pkgs, grps)
        except ProjectsError as e:
            template_errors.append("Error depsolving %s: %s" % (compose_type, str(e)))

    return template_errors

def repo_to_ks(r, url="url"):
    """ Return a kickstart line with the correct args.

    Set url to "baseurl" if it is a repo, leave it as "url" for the installation url.
    """
    cmd = ""
    if url == "url":
        if not r.urls:
            raise RuntimeError("Cannot find a base url for %s" % r.name)

        # url is passed to Anaconda on the cmdline with --repo, so it cannot support a mirror
        # If a mirror is setup yum will return the list of mirrors in .urls
        # So just use the first one.
        cmd += '--%s="%s" ' % (url, r.urls[0])
    elif r.metalink:
        # XXX Total Hack
        # RHEL7 kickstart doesn't support metalink. If the url has 'metalink' in it, rewrite it as 'mirrorlist'
        if "metalink" in r.metalink:
            log.info("RHEL7 does not support metalink, translating to mirrorlist")
            cmd += '--mirrorlist="%s" ' % r.metalink.replace("metalink", "mirrorlist")
        else:
            log.error("Could not convert metalink to mirrorlist. %s", r.metalink)
            raise RuntimeError("Cannot convert metalink to mirrorlist: %s" % r.metalink)
    elif r.mirrorlist:
        cmd += '--mirrorlist="%s" ' % r.mirrorlist
    elif r.baseurl:
        cmd += '--%s="%s" ' % (url, r.baseurl[0])
    else:
        raise RuntimeError("Repo has no baseurl or mirror")

    if r.proxy:
        cmd += '--proxy="%s" ' % r.proxy

    if not r.sslverify:
        cmd += '--noverifyssl'

    if r.sslcacert:
        cmd += ' --sslcacert="%s"' % r.sslcacert
    if r.sslclientcert:
        cmd += ' --sslclientcert="%s"' % r.sslclientcert
    if r.sslclientkey:
        cmd += ' --sslclientkey="%s"' % r.sslclientkey

    return cmd


def bootloader_append(line, kernel_append):
    """ Insert the kernel_append string into the --append argument

    :param line: The bootloader ... line
    :type line: str
    :param kernel_append: The arguments to append to the --append section
    :type kernel_append: str

    Using pykickstart to process the line is the best way to make sure it
    is parsed correctly, and re-assembled for inclusion into the final kickstart
    """
    ks_version = makeVersion()
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
    ks.readKickstartFromString(line)

    if ks.handler.bootloader.appendLine:
        ks.handler.bootloader.appendLine += " %s" % kernel_append
    else:
        ks.handler.bootloader.appendLine = kernel_append

    # Converting back to a string includes a comment, return just the bootloader line
    return str(ks.handler.bootloader).splitlines()[-1]


def get_kernel_append(recipe):
    """Return the customizations.kernel append value

    :param recipe:
    :type recipe: Recipe object
    :returns: append value or empty string
    :rtype: str
    """
    if "customizations" not in recipe or \
       "kernel" not in recipe["customizations"] or \
       "append" not in recipe["customizations"]["kernel"]:
        return ""
    return recipe["customizations"]["kernel"]["append"]


def timezone_cmd(line, settings):
    """ Update the timezone line with the settings

    :param line: The timezone ... line
    :type line: str
    :param settings: A dict with timezone and/or ntpservers list
    :type settings: dict

    Using pykickstart to process the line is the best way to make sure it
    is parsed correctly, and re-assembled for inclusion into the final kickstart
    """
    ks_version = makeVersion()
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
    ks.readKickstartFromString(line)

    if "timezone" in settings:
        ks.handler.timezone.timezone = settings["timezone"]
    if "ntpservers" in settings:
        ks.handler.timezone.ntpservers = settings["ntpservers"]

    # Converting back to a string includes a comment, return just the timezone line
    return str(ks.handler.timezone).splitlines()[-1]


def get_timezone_settings(recipe):
    """Return the customizations.timezone dict

    :param recipe:
    :type recipe: Recipe object
    :returns: append value or empty string
    :rtype: dict
    """
    if "customizations" not in recipe or \
       "timezone" not in recipe["customizations"]:
        return {}
    return recipe["customizations"]["timezone"]


def lang_cmd(line, languages):
    """ Update the lang line with the languages

    :param line: The lang ... line
    :type line: str
    :param settings: The list of languages
    :type settings: list

    Using pykickstart to process the line is the best way to make sure it
    is parsed correctly, and re-assembled for inclusion into the final kickstart
    """
    ks_version = makeVersion()
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
    ks.readKickstartFromString(line)

    if languages:
        ks.handler.lang.lang = languages[0]

        if len(languages) > 1:
            ks.handler.lang.addsupport = languages[1:]

    # Converting back to a string includes a comment, return just the lang line
    return str(ks.handler.lang).splitlines()[-1]


def get_languages(recipe):
    """Return the customizations.locale.languages list

    :param recipe: The recipe
    :type recipe: Recipe object
    :returns: list of language strings
    :rtype: list
    """
    if "customizations" not in recipe or \
       "locale" not in recipe["customizations"] or \
       "languages" not in recipe["customizations"]["locale"]:
        return []
    return recipe["customizations"]["locale"]["languages"]


def keyboard_cmd(line, layout):
    """ Update the keyboard line with the layout

    :param line: The keyboard ... line
    :type line: str
    :param settings: The keyboard layout
    :type settings: str

    Using pykickstart to process the line is the best way to make sure it
    is parsed correctly, and re-assembled for inclusion into the final kickstart
    """
    ks_version = makeVersion()
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
    ks.readKickstartFromString(line)

    if layout:
        ks.handler.keyboard.keyboard = layout
        ks.handler.keyboard.vc_keymap = ""
        ks.handler.keyboard.x_layouts = []

    # Converting back to a string includes a comment, return just the keyboard line
    return str(ks.handler.keyboard).splitlines()[-1]


def get_keyboard_layout(recipe):
    """Return the customizations.locale.keyboard list

    :param recipe: The recipe
    :type recipe: Recipe object
    :returns: The keyboard layout string
    :rtype: str
    """
    if "customizations" not in recipe or \
       "locale" not in recipe["customizations"] or \
       "keyboard" not in recipe["customizations"]["locale"]:
        return []
    return recipe["customizations"]["locale"]["keyboard"]


def firewall_cmd(line, settings):
    """ Update the firewall line with the new ports and services

    :param line: The firewall ... line
    :type line: str
    :param settings: A dict with the list of services and ports to enable and disable
    :type settings: dict

    Using pykickstart to process the line is the best way to make sure it
    is parsed correctly, and re-assembled for inclusion into the final kickstart
    """
    ks_version = makeVersion()
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
    ks.readKickstartFromString(line)

    # Do not override firewall --disabled
    if ks.handler.firewall.enabled != False and settings:
        ks.handler.firewall.ports = sorted(set(settings["ports"] + ks.handler.firewall.ports))
        ks.handler.firewall.services = sorted(set(settings["enabled"] + ks.handler.firewall.services))
        ks.handler.firewall.remove_services = sorted(set(settings["disabled"] + ks.handler.firewall.remove_services))

    # Converting back to a string includes a comment, return just the keyboard line
    return str(ks.handler.firewall).splitlines()[-1]


def get_firewall_settings(recipe):
    """Return the customizations.firewall settings

    :param recipe: The recipe
    :type recipe: Recipe object
    :returns: A dict of settings
    :rtype: dict
    """
    settings = {"ports": [], "enabled": [], "disabled": []}

    if "customizations" not in recipe or \
       "firewall" not in recipe["customizations"]:
        return settings

    settings["ports"] = recipe["customizations"]["firewall"].get("ports", [])

    if "services" in recipe["customizations"]["firewall"]:
        settings["enabled"] = recipe["customizations"]["firewall"]["services"].get("enabled", [])
        settings["disabled"] = recipe["customizations"]["firewall"]["services"].get("disabled", [])
    return settings


def services_cmd(line, settings):
    """ Update the services line with additional services to enable/disable

    :param line: The services ... line
    :type line: str
    :param settings: A dict with the list of services to enable and disable
    :type settings: dict

    Using pykickstart to process the line is the best way to make sure it
    is parsed correctly, and re-assembled for inclusion into the final kickstart
    """
    # Empty services and no additional settings, return an empty string
    if not line and not settings["enabled"] and not settings["disabled"]:
        return ""

    ks_version = makeVersion()
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)

    # Allow passing in a 'default' so that the enable/disable may be applied to it, without
    # parsing it and emitting a kickstart error message
    if line != "services":
        ks.readKickstartFromString(line)

    # Add to any existing services, removing any duplicates
    ks.handler.services.enabled = sorted(set(settings["enabled"] + ks.handler.services.enabled))
    ks.handler.services.disabled = sorted(set(settings["disabled"] + ks.handler.services.disabled))

    # Converting back to a string includes a comment, return just the keyboard line
    return str(ks.handler.services).splitlines()[-1]


def get_services(recipe):
    """Return the customizations.services settings

    :param recipe: The recipe
    :type recipe: Recipe object
    :returns: A dict of settings
    :rtype: dict
    """
    settings = {"enabled": [], "disabled": []}

    if "customizations" not in recipe or \
       "services" not in recipe["customizations"]:
        return settings

    settings["enabled"] = sorted(recipe["customizations"]["services"].get("enabled", []))
    settings["disabled"] = sorted(recipe["customizations"]["services"].get("disabled", []))
    return settings


def get_default_services(recipe):
    """Get the default string for services, based on recipe
    :param recipe: The recipe

    :type recipe: Recipe object
    :returns: string with "services" or ""
    :rtype: str

    When no services have been selected we don't need to add anything to the kickstart
    so return an empty string. Otherwise return "services" which will be updated with
    the settings.
    """
    services = get_services(recipe)

    if services["enabled"] or services["disabled"]:
        return "services"
    else:
        return ""


def customize_ks_template(ks_template, recipe):
    """ Customize the kickstart template and return it

    :param ks_template: The kickstart template
    :type ks_template: str
    :param recipe:
    :type recipe: Recipe object

    Apply customizations to existing template commands, or add defaults for ones that are
    missing and required.

    Apply customizations.kernel.append to the bootloader argument in the template.
    Add bootloader line if it is missing.

    Add default timezone if needed. It does NOT replace an existing timezone entry
    """
    # Commands to be modified [NEW-COMMAND-FUNC, NEW-VALUE, DEFAULT, REPLACE]
    # The function is called with a kickstart command string and the value to replace
    # The value is specific to the command, and is understood by the function
    # The default is a complete kickstart command string, suitable for writing to the template
    # If REPLACE is False it will not change an existing entry only add a missing one
    commands = {"bootloader": [bootloader_append,
                               get_kernel_append(recipe),
                               'bootloader --location=none', True],
                "timezone":   [timezone_cmd,
                               get_timezone_settings(recipe),
                               'timezone UTC', False],
                "lang":       [lang_cmd,
                               get_languages(recipe),
                               'lang en_US.UTF-8', True],
                "keyboard":   [keyboard_cmd,
                               get_keyboard_layout(recipe),
                               'keyboard --xlayouts us --vckeymap us', True],
                "firewall":   [firewall_cmd,
                               get_firewall_settings(recipe),
                               'firewall --enabled', True],
                "services":   [services_cmd,
                               get_services(recipe),
                               get_default_services(recipe), True]
               }
    found = {}

    output = StringIO()
    for line in ks_template.splitlines():
        for cmd in sorted(commands.keys()):
            (new_command, value, default, replace) = commands[cmd]
            if line.startswith(cmd):
                found[cmd] = True
                if value and replace:
                    log.debug("Replacing %s with %s", cmd, value)
                    print(new_command(line, value), file=output)
                else:
                    log.debug("Skipping %s", cmd)
                    print(line, file=output)
                break
        else:
            # No matches, write the line as-is
            print(line, file=output)

    # Write out defaults for the ones not found
    # These must go FIRST because the template still needs to have the packages added
    defaults = StringIO()
    for cmd in sorted(commands.keys()):
        if cmd in found:
            continue
        (new_command, value, default, _) = commands[cmd]
        if value and default:
            log.debug("Setting %s to use %s", cmd, value)
            print(new_command(default, value), file=defaults)
        elif default:
            log.debug("Setting %s to %s", cmd, default)
            print(default, file=defaults)

    return defaults.getvalue() + output.getvalue()


def write_ks_root(f, user):
    """ Write kickstart root password and sshkey entry

    :param f: kickstart file object
    :type f: open file object
    :param user: A blueprint user dictionary
    :type user: dict

    If the entry contains a ssh key, use sshkey to write it
    If it contains password, use rootpw to set it

    root cannot be used with the user command. So only key and password are supported
    for root.
    """
    # ssh key uses the sshkey kickstart command
    if "key" in user:
        f.write('sshkey --user %s "%s"\n' % (user["name"], user["key"]))

    if "password" in user:
        if any(user["password"].startswith(prefix) for prefix in ["$2b$", "$6$", "$5$"]):
            log.debug("Detected pre-crypted password")
            f.write('rootpw --iscrypted "%s"\n' % user["password"])
        else:
            log.debug("Detected plaintext password")
            f.write('rootpw --plaintext "%s"\n' % user["password"])

def write_ks_user(f, user):
    """ Write kickstart user and sshkey entry

    :param f: kickstart file object
    :type f: open file object
    :param user: A blueprint user dictionary
    :type user: dict

    If the entry contains a ssh key, use sshkey to write it
    All of the user fields are optional, except name, write out a kickstart user entry
    with whatever options are relevant.
    """
    # ssh key uses the sshkey kickstart command
    if "key" in user:
        f.write('sshkey --user %s "%s"\n' % (user["name"], user["key"]))

    # Write out the user kickstart command, much of it is optional
    f.write("user --name %s" % user["name"])
    if "home" in user:
        f.write(" --homedir %s" % user["home"])

    if "password" in user:
        if any(user["password"].startswith(prefix) for prefix in ["$2b$", "$6$", "$5$"]):
            log.debug("Detected pre-crypted password")
            f.write(" --iscrypted")
        else:
            log.debug("Detected plaintext password")
            f.write(" --plaintext")

        f.write(" --password \"%s\"" % user["password"])

    if "shell" in user:
        f.write(" --shell %s" % user["shell"])

    if "uid" in user:
        f.write(" --uid %d" % int(user["uid"]))

    if "gid" in user:
        f.write(" --gid %d" % int(user["gid"]))

    if "description" in user:
        f.write(" --gecos \"%s\"" % user["description"])

    if "groups" in user:
        f.write(" --groups %s" % ",".join(user["groups"]))

    f.write("\n")


def write_ks_group(f, group):
    """ Write kickstart group entry

    :param f: kickstart file object
    :type f: open file object
    :param group: A blueprint group dictionary
    :type user: dict

    gid is optional
    """
    if "name" not in group:
        raise RuntimeError("group entry requires a name")

    f.write("group --name %s" % group["name"])
    if "gid" in group:
        f.write(" --gid %d" % int(group["gid"]))

    f.write("\n")


def add_customizations(f, recipe):
    """ Add customizations to the kickstart file

    :param f: kickstart file object
    :type f: open file object
    :param recipe:
    :type recipe: Recipe object
    :returns: None
    :raises: RuntimeError if there was a problem writing to the kickstart
    """
    if "customizations" not in recipe:
        return
    customizations = recipe["customizations"]

    # allow customizations to be incorrectly specified as [[customizations]] instead of [customizations]
    if isinstance(customizations, list):
        customizations = customizations[0]

    if "hostname" in customizations:
        f.write("network --hostname=%s\n" % customizations["hostname"])

    # TODO - remove this, should use user section to define this
    if "sshkey" in customizations:
        # This is a list of entries
        for sshkey in customizations["sshkey"]:
            if "user" not in sshkey or "key" not in sshkey:
                log.error("%s is incorrect, skipping", sshkey)
                continue
            f.write('sshkey --user %s "%s"\n' % (sshkey["user"], sshkey["key"]))

    # Creating a user also creates a group. Make a list of the names for later
    user_groups = []
    if "user" in customizations:
        # only name is required, everything else is optional
        for user in customizations["user"]:
            if "name" not in user:
                raise RuntimeError("user entry requires a name")

            # root is special, cannot use normal user command for it
            if user["name"] == "root":
                write_ks_root(f, user)
                continue

            write_ks_user(f, user)
            user_groups.append(user["name"])

    if "group" in customizations:
        for group in customizations["group"]:
            if group["name"] not in user_groups:
                write_ks_group(f, group)
            else:
                log.warning("Skipping group %s, already created by user", group["name"])


def get_md_size(yum_path):
    """Estimate the amount of space needed by anaconda

    Anaconda doesn't download the filelists or 'other' metadata, which can add
    up to a significant difference, so exclude those from the calculation.
    """
    try:
        du_output = runcmd_output(["/usr/bin/du",
                                   "--exclude", "*other*",
                                   "--exclude", "*filelists*",
                                   "-sb",
                                   yum_path])
        return int(du_output.split()[0])
    except (ValueError, IndexError) as e:
        log.error("Problem calculating metadata size from '%s': %s", du_output, str(e))
        return 0


def start_build(cfg, yumlock, gitlock, branch, recipe_name, compose_type, test_mode=0):
    """ Start the build

    :param cfg: Configuration object
    :type cfg: ComposerConfig
    :param yumlock: Lock and YumBase for depsolving
    :type yumlock: YumLock
    :param recipe: The recipe to build
    :type recipe: str
    :param compose_type: The type of output to create from the recipe
    :type compose_type: str
    :returns: Unique ID for the build that can be used to track its status
    :rtype: str
    """
    share_dir = cfg.get("composer", "share_dir")
    lib_dir = cfg.get("composer", "lib_dir")

    # Make sure compose_type is valid
    if compose_type not in compose_types(share_dir):
        raise RuntimeError("Invalid compose type (%s), must be one of %s" % (compose_type, compose_types(share_dir)))

    with gitlock.lock:
        (commit_id, recipe) = read_recipe_and_id(gitlock.repo, branch, recipe_name)

    # Combine modules and packages and depsolve the list
    module_nver = recipe.module_nver
    package_nver = recipe.package_nver
    projects = sorted(set(module_nver+package_nver), key=lambda p: p[0].lower())
    deps = []
    try:
        # This can possibly update repodata and reset the YumBase object.
        with yumlock.lock_check:
            (installed_size, anaconda_size, deps) = projects_depsolve_with_size(yumlock.yb, projects, recipe.group_names, with_core=False)
    except ProjectsError as e:
        log.error("start_build depsolve: %s", str(e))
        raise RuntimeError("Problem depsolving %s: %s" % (recipe["name"], str(e)))

    # Read the kickstart template for this type
    ks_template_path = joinpaths(share_dir, "composer", compose_type) + ".ks"
    ks_template = open(ks_template_path, "r").read()

    # How much space will the packages in the selected template take?
    ks_version = makeVersion(RHEL7)
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
    ks.readKickstartFromString(ks_template+"\n%end\n")
    pkgs = [(name, "*") for name in ks.handler.packages.packageList]
    grps = [grp.name for grp in ks.handler.packages.groupList]
    try:
        with yumlock.lock:
            (template_size, anaconda_tmpl_size, _) = projects_depsolve_with_size(yumlock.yb, pkgs, grps, with_core=not ks.handler.packages.nocore)
    except ProjectsError as e:
        log.error("start_build depsolve: %s", str(e))
        raise RuntimeError("Problem depsolving %s: %s" % (recipe["name"], str(e)))

    # Anaconda also stores the metadata on the disk once it is partitioned, try to take this into account by
    # adding the size of the lorax-composer metadata storage.
    with yumlock.lock:
        metadata_size = get_md_size(yumlock.yb.conf.installroot)

    # Anaconda estimates size differently, only taking into account installed size and adding 35%
    # But we must make sure that our actual disk size is at least as big as the anaconda size, otherwise the install will fail
    anaconda_minimum = int((anaconda_size+anaconda_tmpl_size) * 1.35)

    log.debug("anaconda_size = %d, anaconda_template_size=%d, anaconda_minimum=%d", anaconda_size, anaconda_tmpl_size, anaconda_minimum)
    log.debug("installed_size = %d, template_size=%d, metadata_size=%d", installed_size, template_size, metadata_size)

    # Add 10% to the composer estimate
    installed_size = int((installed_size+template_size+metadata_size) * 1.10)

    # Select the largest size for the partition
    partition_size = max(1024**3, anaconda_minimum, installed_size)
    log.debug("/ partition size = %d", partition_size)

    # Create the results directory
    build_id = str(uuid4())
    results_dir = joinpaths(lib_dir, "results", build_id)
    os.makedirs(results_dir)

    # Write the recipe commit hash
    commit_path = joinpaths(results_dir, "COMMIT")
    with open(commit_path, "w") as f:
        f.write(commit_id)

    # Write the original recipe
    recipe_path = joinpaths(results_dir, "blueprint.toml")
    with open(recipe_path, "w") as f:
        f.write(recipe.toml())

    # Write the frozen recipe
    frozen_recipe = recipe.freeze(deps)
    recipe_path = joinpaths(results_dir, "frozen.toml")
    with open(recipe_path, "w") as f:
        f.write(frozen_recipe.toml())

    # Write out the dependencies to the results dir
    deps_path = joinpaths(results_dir, "deps.toml")
    with open(deps_path, "w") as f:
        f.write(toml.dumps({"packages":deps}).encode("UTF-8"))

    # Save a copy of the original kickstart
    shutil.copy(ks_template_path, results_dir)

    with yumlock.lock:
        repos = yumlock.yb.repos.listEnabled()
    if not repos:
        raise RuntimeError("No enabled repos, canceling build.")

    # Create the final kickstart with repos and package list
    ks_path = joinpaths(results_dir, "final-kickstart.ks")
    with open(ks_path, "w") as f:
        ks_url = repo_to_ks(repos[0], "url")
        log.debug("url = %s", ks_url)
        f.write('url %s\n' % ks_url)
        for idx, r in enumerate(repos[1:]):
            ks_repo = repo_to_ks(r, "baseurl")
            log.debug("repo composer-%s = %s", idx, ks_repo)
            f.write('repo --name="composer-%s" %s\n' % (idx, ks_repo))

        # Setup the disk for booting
        # TODO Add GPT and UEFI boot support
        f.write('clearpart --all\n')

        # Write the root partition and it's size in MB (rounded up)
        f.write('part / --size=%d\n' % ceil(installed_size / 1024**2))

        # Some customizations modify the template before writing it
        f.write(customize_ks_template(ks_template, recipe))

        for d in deps:
            f.write(dep_nevra(d)+"\n")
        f.write("%end\n")

        # Other customizations can be appended to the kickstart
        add_customizations(f, recipe)

    # Setup the config to pass to novirt_install
    log_dir = joinpaths(results_dir, "logs/")
    cfg_args = compose_args(compose_type)

    # Get the title, project, and release version from the host
    if not os.path.exists("/etc/os-release"):
        log.error("/etc/os-release is missing, cannot determine product or release version")
    os_release = SimpleConfigFile("/etc/os-release")
    os_release.read()

    log.debug("os_release = %s", os_release)

    cfg_args["title"] = os_release.get("PRETTY_NAME")
    cfg_args["project"] = os_release.get("NAME")
    cfg_args["releasever"] = os_release.get("VERSION_ID")
    cfg_args["volid"] = ""
    cfg_args["extra_boot_args"] = get_kernel_append(recipe)

    cfg_args.update({
        "compression":      "xz",
        "compress_args":    [],
        "ks":               [ks_path],
        "project":          "Red Hat Enterprise Linux",
        "releasever":       "7",
        "logfile":          log_dir
    })
    with open(joinpaths(results_dir, "config.toml"), "w") as f:
        f.write(toml.dumps(cfg_args).encode("UTF-8"))

    # Set the initial status
    open(joinpaths(results_dir, "STATUS"), "w").write("WAITING")

    # Set the test mode, if requested
    if test_mode > 0:
        open(joinpaths(results_dir, "TEST"), "w").write("%s" % test_mode)

    write_timestamp(results_dir, TS_CREATED)
    log.info("Adding %s (%s %s) to compose queue", build_id, recipe["name"], compose_type)
    os.symlink(results_dir, joinpaths(lib_dir, "queue/new/", build_id))

    return build_id

# Supported output types
def compose_types(share_dir):
    r""" Returns a list of the supported output types

    The output types come from the kickstart names in /usr/share/lorax/composer/\*ks
    """
    return sorted([os.path.basename(ks)[:-3] for ks in glob(joinpaths(share_dir, "composer/*.ks"))])

def compose_args(compose_type):
    """ Returns the settings to pass to novirt_install for the compose type

    :param compose_type: The type of compose to create, from `compose_types()`
    :type compose_type: str

    This will return a dict of options that match the ArgumentParser options for livemedia-creator.
    These are the ones the define the type of output, it's filename, etc.
    Other options will be filled in by `make_compose()`
    """
    _MAP = {"tar":              {"make_iso":                False,
                                 "make_disk":               False,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                True,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   False,
                                 "qcow2_args":              [],
                                 "image_name":              default_image_name("xz", "root.tar"),
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None
                                },
            "live-iso":         {"make_iso":                True,
                                 "make_disk":               False,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   False,
                                 "qcow2_args":              [],
                                 "image_name":              "live.iso",
                                 "fs_label":                "Anaconda",     # Live booting may expect this to be 'Anaconda'
                                 "image_only":              False,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None
                                },
            "partitioned-disk": {"make_iso":                False,
                                 "make_disk":               True,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                  "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   False,
                                 "qcow2_args":              [],
                                 "image_name":              "disk.img",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None
                                },
            "qcow2":            {"make_iso":                False,
                                 "make_disk":               True,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                  "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   True,
                                 "qcow2_args":              [],
                                 "image_name":              "disk.qcow2",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None
                                },
            "ext4-filesystem":  {"make_iso":                False,
                                 "make_disk":               False,
                                 "make_fsimage":            True,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   False,
                                 "qcow2_args":              [],
                                 "image_name":              "filesystem.img",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None
                                },
            "ami":              {"make_iso":                False,
                                 "make_disk":               True,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   False,
                                 "qcow2_args":              [],
                                 "image_name":              "disk.ami",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None,
                                },
            "vhd":              {"make_iso":                False,
                                 "make_disk":               True,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   True,
                                 "qcow2_args":              ["-O", "vpc", "-o", "subformat=fixed"],
                                 "image_name":              "disk.vhd",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None,
                                },
            "vmdk":             {"make_iso":                False,
                                 "make_disk":               True,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   True,
                                 "qcow2_args":              ["-O", "vmdk"],
                                 "image_name":              "disk.vmdk",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None,
                                },
            "openstack":        {"make_iso":                False,
                                 "make_disk":               True,
                                 "make_fsimage":            False,
                                 "make_appliance":          False,
                                 "make_ami":                False,
                                 "make_tar":                False,
                                 "make_pxe_live":           False,
                                 "make_ostree_live":        False,
                                 "ostree":                  False,
                                 "live_rootfs_keep_size":   False,
                                 "live_rootfs_size":        0,
                                 "qcow2":                   True,
                                 "qcow2_args":              [],
                                 "image_name":              "disk.qcow2",
                                 "fs_label":                "",
                                 "image_only":              True,
                                 "app_name":                None,
                                 "app_template":            None,
                                 "app_file":                None,
                                },
            }
    return _MAP[compose_type]

def move_compose_results(cfg, results_dir):
    """Move the final image to the results_dir and cleanup the unneeded compose files

    :param cfg: Build configuration
    :type cfg: DataHolder
    :param results_dir: Directory to put the results into
    :type results_dir: str
    """
    if cfg["make_tar"]:
        shutil.move(joinpaths(cfg["result_dir"], cfg["image_name"]), results_dir)
    elif cfg["make_iso"]:
        # Output from live iso is always a boot.iso under images/, move and rename it
        shutil.move(joinpaths(cfg["result_dir"], "images/boot.iso"), joinpaths(results_dir, cfg["image_name"]))
    elif cfg["make_disk"] or cfg["make_fsimage"]:
        shutil.move(joinpaths(cfg["result_dir"], cfg["image_name"]), joinpaths(results_dir, cfg["image_name"]))


    # Cleanup the compose directory, but only if it looks like a compose directory
    if os.path.basename(cfg["result_dir"]) == "compose":
        shutil.rmtree(cfg["result_dir"])
    else:
        log.error("Incorrect compose directory, not cleaning up")
