예제 #1
0
파일: queue.py 프로젝트: yuvalturg/lorax
def monitor(cfg):
    """Monitor the queue for new compose requests

    :param cfg: Configuration settings
    :type cfg: DataHolder
    :returns: Does not return

    The queue has 2 subdirectories, new and run. When a compose is ready to be run
    a symlink to the uniquely named results directory should be placed in ./queue/new/

    When the it is ready to be run (it is checked every 30 seconds or after a previous
    compose is finished) the symlink will be moved into ./queue/run/ and a STATUS file
    will be created in the results directory.

    STATUS can contain one of: WAITING, RUNNING, FINISHED, FAILED

    If the system is restarted while a compose is running it will move any old symlinks
    from ./queue/run/ to ./queue/new/ and rerun them.
    """
    def queue_sort(uuid):
        """Sort the queue entries by their mtime, not their names"""
        return os.stat(joinpaths(cfg.composer_dir, "queue/new", uuid)).st_mtime

    check_queues(cfg)
    while True:
        uuids = sorted(os.listdir(joinpaths(cfg.composer_dir, "queue/new")),
                       key=queue_sort)

        # Pick the oldest and move it into ./run/
        if not uuids:
            # No composes left to process, sleep for a bit
            time.sleep(5)
        else:
            src = joinpaths(cfg.composer_dir, "queue/new", uuids[0])
            dst = joinpaths(cfg.composer_dir, "queue/run", uuids[0])
            try:
                os.rename(src, dst)
            except OSError:
                # The symlink may vanish if uuid_cancel() has been called
                continue

            log.info("Starting new compose: %s", dst)
            open(joinpaths(dst, "STATUS"), "w").write("RUNNING\n")

            try:
                make_compose(cfg, os.path.realpath(dst))
                log.info("Finished building %s, results are in %s", dst,
                         os.path.realpath(dst))
                open(joinpaths(dst, "STATUS"), "w").write("FINISHED\n")
                write_timestamp(dst, TS_FINISHED)
            except Exception:
                import traceback
                log.error("traceback: %s", traceback.format_exc())

                # TODO - Write the error message to an ERROR-LOG file to include with the status
                #                log.error("Error running compose: %s", e)
                open(joinpaths(dst, "STATUS"), "w").write("FAILED\n")
                write_timestamp(dst, TS_FINISHED)

            os.unlink(dst)
예제 #2
0
    def test_timestamp(self):
        """Test writing and reading compose timestamps"""
        write_timestamp(self.test_dir, TS_CREATED)
        ts = timestamp_dict(self.test_dir)
        self.assertTrue(TS_CREATED in ts)
        self.assertTrue(TS_STARTED not in ts)
        self.assertTrue(TS_FINISHED not in ts)

        write_timestamp(self.test_dir, TS_STARTED)
        ts = timestamp_dict(self.test_dir)
        self.assertTrue(TS_CREATED in ts)
        self.assertTrue(TS_STARTED in ts)
        self.assertTrue(TS_FINISHED not in ts)

        write_timestamp(self.test_dir, TS_FINISHED)
        ts = timestamp_dict(self.test_dir)
        self.assertTrue(TS_CREATED in ts)
        self.assertTrue(TS_STARTED in ts)
        self.assertTrue(TS_FINISHED in ts)
예제 #3
0
def start_build(cfg,
                dnflock,
                gitlock,
                branch,
                recipe_name,
                compose_type,
                test_mode=0):
    """ Start the build

    :param cfg: Configuration object
    :type cfg: ComposerConfig
    :param dnflock: Lock and YumBase for depsolving
    :type dnflock: 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)))

    # Some image types (live-iso) need extra packages for composer to execute the output template
    with dnflock.lock:
        extra_pkgs = get_extra_pkgs(dnflock.dbo, share_dir, compose_type)
    log.debug("Extra packages needed for %s: %s", compose_type, extra_pkgs)

    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
    package_nver.extend([(name, '*') for name in extra_pkgs])

    projects = sorted(set(module_nver + package_nver),
                      key=lambda p: p[0].lower())
    deps = []
    log.info("depsolving %s", recipe["name"])
    try:
        # This can possibly update repodata and reset the YumBase object.
        with dnflock.lock_check:
            (installed_size,
             deps) = projects_depsolve_with_size(dnflock.dbo,
                                                 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 default template take?
    ks_version = makeVersion()
    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 dnflock.lock:
            (template_size, _) = projects_depsolve_with_size(
                dnflock.dbo,
                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)))
    log.debug("installed_size = %d, template_size=%d", installed_size,
              template_size)

    # Minimum LMC disk size is 1GiB, and anaconda bumps the estimated size up by 10% (which doesn't always work).
    installed_size = int((installed_size + template_size)) * 1.2
    log.debug("/ partition size = %d", installed_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}))

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

    with dnflock.lock:
        repos = list(dnflock.dbo.repos.iter_enabled())
    if not repos:
        raise RuntimeError("No enabled repos, canceling build.")

    # Create the git rpms, if any, and return the path to the repo under results_dir
    gitrpm_repo = create_gitrpm_repo(results_dir, recipe)

    # 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))

        if gitrpm_repo:
            log.debug("repo gitrpms = %s", gitrpm_repo)
            f.write('repo --name="gitrpms" --baseurl="file://%s"\n' %
                    gitrpm_repo)

        # Setup the disk for booting
        # TODO Add GPT and UEFI boot support
        f.write('clearpart --all --initlabel\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")

        # Include the rpms from the gitrpm repo directory
        if gitrpm_repo:
            for rpm in glob(os.path.join(gitrpm_repo, "*.rpm")):
                f.write(os.path.basename(rpm)[:-4] + "\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 = flatconfig("/etc/os-release")

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

    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],
        "logfile": log_dir,
        "timeout": 60,  # 60 minute timeout
    })
    with open(joinpaths(results_dir, "config.toml"), "w") as f:
        f.write(toml.dumps(cfg_args))

    # 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
예제 #4
0
def make_compose(cfg, results_dir):
    """Run anaconda with the final-kickstart.ks from results_dir

    :param cfg: Configuration settings
    :type cfg: DataHolder
    :param results_dir: The directory containing the metadata and results for the build
    :type results_dir: str
    :returns: Nothing
    :raises: May raise various exceptions

    This takes the final-kickstart.ks, and the settings in config.toml and runs Anaconda
    in no-virt mode (directly on the host operating system). Exceptions should be caught
    at the higer level.

    If there is a failure, the build artifacts will be cleaned up, and any logs will be
    moved into logs/anaconda/ and their ownership will be set to the user from the cfg
    object.
    """

    # Check on the ks's presence
    ks_path = joinpaths(results_dir, "final-kickstart.ks")
    if not os.path.exists(ks_path):
        raise RuntimeError("Missing kickstart file at %s" % ks_path)

    # Load the compose configuration
    cfg_path = joinpaths(results_dir, "config.toml")
    if not os.path.exists(cfg_path):
        raise RuntimeError("Missing config.toml for %s" % results_dir)
    cfg_dict = toml.loads(open(cfg_path, "r").read())

    # The keys in cfg_dict correspond to the arguments setup in livemedia-creator
    # keys that define what to build should be setup in compose_args, and keys with
    # defaults should be setup here.

    # Make sure that image_name contains no path components
    cfg_dict["image_name"] = os.path.basename(cfg_dict["image_name"])

    # Only support novirt installation, set some other defaults
    cfg_dict["no_virt"] = True
    cfg_dict["disk_image"] = None
    cfg_dict["fs_image"] = None
    cfg_dict["keep_image"] = False
    cfg_dict["domacboot"] = False
    cfg_dict["anaconda_args"] = ""
    cfg_dict["proxy"] = ""
    cfg_dict["armplatform"] = ""
    cfg_dict["squashfs_args"] = None

    cfg_dict["lorax_templates"] = find_templates(cfg.share_dir)
    cfg_dict["tmp"] = cfg.tmp
    cfg_dict["dracut_args"] = None  # Use default args for dracut

    # TODO How to support other arches?
    cfg_dict["arch"] = None

    # Compose things in a temporary directory inside the results directory
    cfg_dict["result_dir"] = joinpaths(results_dir, "compose")
    os.makedirs(cfg_dict["result_dir"])

    install_cfg = DataHolder(**cfg_dict)

    # Some kludges for the 99-copy-logs %post, failure in it will crash the build
    for f in ["/tmp/NOSAVE_INPUT_KS", "/tmp/NOSAVE_LOGS"]:
        open(f, "w")

    # Placing a CANCEL file in the results directory will make execWithRedirect send anaconda a SIGTERM
    def cancel_build():
        return os.path.exists(joinpaths(results_dir, "CANCEL"))

    log.debug("cfg  = %s", install_cfg)
    try:
        test_path = joinpaths(results_dir, "TEST")
        write_timestamp(results_dir, TS_STARTED)
        if os.path.exists(test_path):
            # Pretend to run the compose
            time.sleep(5)
            try:
                test_mode = int(open(test_path, "r").read())
            except Exception:
                test_mode = 1
            if test_mode == 1:
                raise RuntimeError("TESTING FAILED compose")
            else:
                open(joinpaths(results_dir, install_cfg.image_name),
                     "w").write("TEST IMAGE")
        else:
            run_creator(install_cfg, cancel_func=cancel_build)

            # Extract the results of the compose into results_dir and cleanup the compose directory
            move_compose_results(install_cfg, results_dir)
    finally:
        # Make sure any remaining temporary directories are removed (eg. if there was an exception)
        for d in glob(joinpaths(cfg.tmp, "lmc-*")):
            if os.path.isdir(d):
                shutil.rmtree(d)
            elif os.path.isfile(d):
                os.unlink(d)

        # Make sure that everything under the results directory is owned by the user
        user = pwd.getpwuid(cfg.uid).pw_name
        group = grp.getgrgid(cfg.gid).gr_name
        log.debug("Install finished, chowning results to %s:%s", user, group)
        subprocess.call(["chown", "-R", "%s:%s" % (user, group), results_dir])
예제 #5
0
def monitor(cfg):
    """Monitor the queue for new compose requests

    :param cfg: Configuration settings
    :type cfg: DataHolder
    :returns: Does not return

    The queue has 2 subdirectories, new and run. When a compose is ready to be run
    a symlink to the uniquely named results directory should be placed in ./queue/new/

    When the it is ready to be run (it is checked every 30 seconds or after a previous
    compose is finished) the symlink will be moved into ./queue/run/ and a STATUS file
    will be created in the results directory.

    STATUS can contain one of: WAITING, RUNNING, FINISHED, FAILED

    If the system is restarted while a compose is running it will move any old symlinks
    from ./queue/run/ to ./queue/new/ and rerun them.
    """
    def queue_sort(uuid):
        """Sort the queue entries by their mtime, not their names"""
        return os.stat(joinpaths(cfg.composer_dir, "queue/new", uuid)).st_mtime

    check_queues(cfg)
    while True:
        uuids = sorted(os.listdir(joinpaths(cfg.composer_dir, "queue/new")),
                       key=queue_sort)

        # Pick the oldest and move it into ./run/
        if not uuids:
            # No composes left to process, sleep for a bit
            time.sleep(5)
        else:
            src = joinpaths(cfg.composer_dir, "queue/new", uuids[0])
            dst = joinpaths(cfg.composer_dir, "queue/run", uuids[0])
            try:
                os.rename(src, dst)
            except OSError:
                # The symlink may vanish if uuid_cancel() has been called
                continue

            # The anaconda logs are also copied into ./anaconda/ in this directory
            os.makedirs(joinpaths(dst, "logs"), exist_ok=True)

            def open_handler(loggers, file_name):
                handler = logging.FileHandler(joinpaths(
                    dst, "logs", file_name))
                handler.setLevel(logging.DEBUG)
                handler.setFormatter(
                    logging.Formatter(
                        "%(asctime)s %(levelname)s: %(message)s"))
                for logger in loggers:
                    logger.addHandler(handler)
                return (handler, loggers)

            loggers = (((log, program_log, dnf_log),
                        "combined.log"), ((log, ), "composer.log"),
                       ((program_log, ), "program.log"), ((dnf_log, ),
                                                          "dnf.log"))
            handlers = [
                open_handler(loggers, file_name)
                for loggers, file_name in loggers
            ]

            log.info("Starting new compose: %s", dst)
            open(joinpaths(dst, "STATUS"), "w").write("RUNNING\n")

            try:
                make_compose(cfg, os.path.realpath(dst))
                log.info("Finished building %s, results are in %s", dst,
                         os.path.realpath(dst))
                open(joinpaths(dst, "STATUS"), "w").write("FINISHED\n")
                write_timestamp(dst, TS_FINISHED)
            except Exception:
                import traceback
                log.error("traceback: %s", traceback.format_exc())

                # TODO - Write the error message to an ERROR-LOG file to include with the status
                #                log.error("Error running compose: %s", e)
                open(joinpaths(dst, "STATUS"), "w").write("FAILED\n")
                write_timestamp(dst, TS_FINISHED)
            finally:
                for handler, loggers in handlers:
                    for logger in loggers:
                        logger.removeHandler(handler)
                    handler.close()

            os.unlink(dst)