79e97dc845
Add a new option 'latest-link-status' to pungi-koji, if this is specified, pungi will only create the latest symbol link to the compose when compose's status matches the specified statuses. The status name is case insensitive. If the option is not specified it will act as before. Example: pungi-koji --target-dir=_composes --config=data/dummy-pungi.conf \ --test --latest-link-status=finished --latest-link-status=finished_incomplete Signed-off-by: Qixiang Wan <qwan@redhat.com>
480 lines
16 KiB
Python
Executable File
480 lines
16 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import os
|
|
import sys
|
|
import optparse
|
|
import logging
|
|
import locale
|
|
import datetime
|
|
import getpass
|
|
import socket
|
|
import pipes
|
|
import json
|
|
|
|
here = sys.path[0]
|
|
if here != '/usr/bin':
|
|
# Git checkout
|
|
sys.path[0] = os.path.dirname(here)
|
|
|
|
from pungi import get_full_version
|
|
|
|
|
|
# force C locales
|
|
locale.setlocale(locale.LC_ALL, "C")
|
|
|
|
|
|
COMPOSE = None
|
|
|
|
|
|
def main():
|
|
global COMPOSE
|
|
|
|
parser = optparse.OptionParser()
|
|
parser.add_option(
|
|
"--target-dir",
|
|
metavar="PATH",
|
|
help="a compose is created under this directory",
|
|
)
|
|
parser.add_option(
|
|
"--label",
|
|
help="specify compose label (example: Snapshot-1.0); required for production composes"
|
|
)
|
|
parser.add_option(
|
|
"--no-label",
|
|
action="store_true",
|
|
default=False,
|
|
help="make a production compose without label"
|
|
)
|
|
parser.add_option(
|
|
"--supported",
|
|
action="store_true",
|
|
default=False,
|
|
help="set supported flag on media (automatically on for 'RC-x.y' labels)"
|
|
)
|
|
parser.add_option(
|
|
"--old-composes",
|
|
metavar="PATH",
|
|
dest="old_composes",
|
|
default=[],
|
|
action="append",
|
|
help="Path to directory with old composes. Reuse an existing repodata from the most recent compose.",
|
|
)
|
|
parser.add_option(
|
|
"--compose-dir",
|
|
metavar="PATH",
|
|
help="reuse an existing compose directory (DANGEROUS!)",
|
|
)
|
|
parser.add_option(
|
|
"--debug-mode",
|
|
action="store_true",
|
|
default=False,
|
|
help="run pungi in DEBUG mode (DANGEROUS!)",
|
|
)
|
|
parser.add_option(
|
|
"--config",
|
|
help="Config file"
|
|
)
|
|
parser.add_option(
|
|
"--skip-phase",
|
|
metavar="PHASE",
|
|
action="append",
|
|
default=[],
|
|
help="skip a compose phase",
|
|
)
|
|
parser.add_option(
|
|
"--just-phase",
|
|
metavar="PHASE",
|
|
action="append",
|
|
default=[],
|
|
help="run only a specified compose phase",
|
|
)
|
|
parser.add_option(
|
|
"--nightly",
|
|
action="store_const",
|
|
const="nightly",
|
|
dest="compose_type",
|
|
help="make a nightly compose",
|
|
)
|
|
parser.add_option(
|
|
"--test",
|
|
action="store_const",
|
|
const="test",
|
|
dest="compose_type",
|
|
help="make a test compose",
|
|
)
|
|
parser.add_option(
|
|
"--ci",
|
|
action="store_const",
|
|
const="ci",
|
|
dest="compose_type",
|
|
help="make a CI compose",
|
|
)
|
|
parser.add_option(
|
|
"--koji-event",
|
|
metavar="ID",
|
|
type="int",
|
|
help="specify a koji event for populating package set",
|
|
)
|
|
parser.add_option(
|
|
"--version",
|
|
action="store_true",
|
|
help="output version information and exit",
|
|
)
|
|
parser.add_option(
|
|
"--notification-script",
|
|
help="script for sending progress notification messages"
|
|
)
|
|
parser.add_option(
|
|
"--no-latest-link",
|
|
action="store_true",
|
|
default=False,
|
|
dest="no_latest_link",
|
|
help="don't create latest symbol link to this compose"
|
|
)
|
|
parser.add_option(
|
|
"--latest-link-status",
|
|
metavar="STATUS",
|
|
action="append",
|
|
default=[],
|
|
help="only create latest symbol link to this compose when compose status matches specified status",
|
|
)
|
|
parser.add_option(
|
|
"--quiet",
|
|
action="store_true",
|
|
default=False,
|
|
help="quiet mode, don't print log on screen"
|
|
)
|
|
|
|
opts, args = parser.parse_args()
|
|
import pungi.notifier
|
|
notifier = pungi.notifier.PungiNotifier(opts.notification_script)
|
|
|
|
def fail_to_start(msg, **kwargs):
|
|
notifier.send('fail-to-start', workdir=opts.target_dir,
|
|
command=sys.argv, target_dir=opts.target_dir,
|
|
config=opts.config, detail=msg, **kwargs)
|
|
|
|
def abort(msg):
|
|
fail_to_start(msg)
|
|
parser.error(msg)
|
|
|
|
if opts.version:
|
|
print("pungi %s" % get_full_version())
|
|
sys.exit(0)
|
|
|
|
if opts.target_dir and opts.compose_dir:
|
|
abort("cannot specify --target-dir and --compose-dir at once")
|
|
|
|
if not opts.target_dir and not opts.compose_dir:
|
|
abort("please specify a target directory")
|
|
|
|
if opts.target_dir and not opts.compose_dir:
|
|
opts.target_dir = os.path.abspath(opts.target_dir)
|
|
if not os.path.isdir(opts.target_dir):
|
|
abort("The target directory does not exist or is not a directory: %s" % opts.target_dir)
|
|
else:
|
|
opts.compose_dir = os.path.abspath(opts.compose_dir)
|
|
if not os.path.isdir(opts.compose_dir):
|
|
abort("The compose directory does not exist or is not a directory: %s" % opts.compose_dir)
|
|
|
|
compose_type = opts.compose_type or "production"
|
|
if compose_type == "production" and not opts.label and not opts.no_label:
|
|
abort("must specify label for a production compose")
|
|
|
|
if not opts.config:
|
|
abort("please specify a config")
|
|
opts.config = os.path.abspath(opts.config)
|
|
|
|
create_latest_link = not opts.no_latest_link
|
|
latest_link_status = opts.latest_link_status or None
|
|
|
|
import kobo.conf
|
|
import kobo.log
|
|
import productmd.composeinfo
|
|
|
|
if opts.label:
|
|
try:
|
|
productmd.composeinfo.verify_label(opts.label)
|
|
except ValueError as ex:
|
|
abort(str(ex))
|
|
|
|
from pungi.compose import Compose
|
|
|
|
logger = logging.getLogger("pungi")
|
|
logger.setLevel(logging.DEBUG)
|
|
if not opts.quiet:
|
|
kobo.log.add_stderr_logger(logger)
|
|
|
|
conf = kobo.conf.PyConfigParser()
|
|
conf.load_from_file(opts.config)
|
|
|
|
# check if all requirements are met
|
|
import pungi.checks
|
|
if not pungi.checks.check(conf):
|
|
sys.exit(1)
|
|
pungi.checks.check_umask(logger)
|
|
errors, warnings = pungi.checks.validate(conf)
|
|
for warning in warnings:
|
|
print >>sys.stderr, warning
|
|
if errors:
|
|
for error in errors:
|
|
print >>sys.stderr, error
|
|
fail_to_start('Config validation failed', errors=errors)
|
|
sys.exit(1)
|
|
|
|
if opts.target_dir:
|
|
compose_dir = Compose.get_compose_dir(opts.target_dir, conf, compose_type=compose_type, compose_label=opts.label)
|
|
else:
|
|
compose_dir = opts.compose_dir
|
|
|
|
compose = Compose(conf,
|
|
topdir=compose_dir,
|
|
debug=opts.debug_mode,
|
|
skip_phases=opts.skip_phase,
|
|
just_phases=opts.just_phase,
|
|
old_composes=opts.old_composes,
|
|
koji_event=opts.koji_event,
|
|
supported=opts.supported,
|
|
logger=logger,
|
|
notifier=notifier)
|
|
notifier.compose = compose
|
|
COMPOSE = compose
|
|
try:
|
|
run_compose(compose, create_latest_link=create_latest_link, latest_link_status=latest_link_status)
|
|
except Exception, ex:
|
|
compose.log_error("Compose run failed: %s" % ex)
|
|
raise
|
|
|
|
|
|
def run_compose(compose, create_latest_link=True, latest_link_status=None):
|
|
import pungi.phases
|
|
import pungi.metadata
|
|
|
|
errors = []
|
|
|
|
compose.write_status("STARTED")
|
|
compose.log_info("Host: %s" % socket.gethostname())
|
|
compose.log_info("Pungi version: %s" % get_full_version())
|
|
compose.log_info("User name: %s" % getpass.getuser())
|
|
compose.log_info("Working directory: %s" % os.getcwd())
|
|
compose.log_info("Command line: %s" % " ".join([pipes.quote(arg) for arg in sys.argv]))
|
|
compose.log_info("Compose top directory: %s" % compose.topdir)
|
|
compose.read_variants()
|
|
|
|
# dump the config file
|
|
date_str = datetime.datetime.strftime(datetime.datetime.now(), "%F_%X").replace(":", "-")
|
|
config_dump = compose.paths.log.log_file("global", "config-copy_%s" % date_str)
|
|
open(config_dump, "w").write(open(compose.conf._open_file, 'r').read())
|
|
config_dump_full = compose.paths.log.log_file("global", "config-dump_%s" % date_str)
|
|
with open(config_dump_full, "w") as f:
|
|
json.dump(compose.conf, f, sort_keys=True, indent=4)
|
|
|
|
# initialize all phases
|
|
init_phase = pungi.phases.InitPhase(compose)
|
|
pkgset_phase = pungi.phases.PkgsetPhase(compose)
|
|
buildinstall_phase = pungi.phases.BuildinstallPhase(compose)
|
|
gather_phase = pungi.phases.GatherPhase(compose, pkgset_phase)
|
|
extrafiles_phase = pungi.phases.ExtraFilesPhase(compose, pkgset_phase)
|
|
createrepo_phase = pungi.phases.CreaterepoPhase(compose)
|
|
ostree_installer_phase = pungi.phases.OstreeInstallerPhase(compose)
|
|
ostree_phase = pungi.phases.OSTreePhase(compose)
|
|
productimg_phase = pungi.phases.ProductimgPhase(compose, pkgset_phase)
|
|
createiso_phase = pungi.phases.CreateisoPhase(compose)
|
|
liveimages_phase = pungi.phases.LiveImagesPhase(compose)
|
|
livemedia_phase = pungi.phases.LiveMediaPhase(compose)
|
|
image_build_phase = pungi.phases.ImageBuildPhase(compose)
|
|
osbs_phase = pungi.phases.OSBSPhase(compose)
|
|
image_checksum_phase = pungi.phases.ImageChecksumPhase(compose)
|
|
test_phase = pungi.phases.TestPhase(compose)
|
|
|
|
# check if all config options are set
|
|
for phase in (init_phase, pkgset_phase, createrepo_phase,
|
|
buildinstall_phase, productimg_phase, gather_phase,
|
|
extrafiles_phase, createiso_phase, liveimages_phase,
|
|
livemedia_phase, image_build_phase, image_checksum_phase,
|
|
test_phase, ostree_phase, ostree_installer_phase,
|
|
osbs_phase):
|
|
if phase.skip():
|
|
continue
|
|
try:
|
|
phase.validate()
|
|
except ValueError as ex:
|
|
for i in str(ex).splitlines():
|
|
errors.append("%s: %s" % (phase.name.upper(), i))
|
|
if errors:
|
|
for i in errors:
|
|
compose.log_error(i)
|
|
print(i)
|
|
sys.exit(1)
|
|
|
|
# PREP
|
|
|
|
# Note: This may be put into a new method of phase classes (e.g. .prep())
|
|
# in same way as .validate() or .run()
|
|
|
|
# Prep for liveimages - Obtain a password for signing rpm wrapped images
|
|
if ("signing_key_password_file" in compose.conf
|
|
and "signing_command" in compose.conf
|
|
and "%(signing_key_password)s" in compose.conf["signing_command"]
|
|
and not liveimages_phase.skip()):
|
|
# TODO: Don't require key if signing is turned off
|
|
# Obtain signing key password
|
|
signing_key_password = None
|
|
|
|
# Use appropriate method
|
|
if compose.conf["signing_key_password_file"] == "-":
|
|
# Use stdin (by getpass module)
|
|
try:
|
|
signing_key_password = getpass.getpass("Signing key password: ")
|
|
except EOFError:
|
|
compose.log_debug("Ignoring signing key password")
|
|
pass
|
|
else:
|
|
# Use text file with password
|
|
try:
|
|
signing_key_password = open(compose.conf["signing_key_password_file"], "r").readline().rstrip('\n')
|
|
except IOError:
|
|
# Filename is not print intentionally in case someone puts password directly into the option
|
|
err_msg = "Cannot load password from file specified by 'signing_key_password_file' option"
|
|
compose.log_error(err_msg)
|
|
print(err_msg)
|
|
sys.exit(1)
|
|
|
|
if signing_key_password:
|
|
# Store the password
|
|
compose.conf["signing_key_password"] = signing_key_password
|
|
|
|
# INIT phase
|
|
init_phase.start()
|
|
init_phase.stop()
|
|
|
|
# PKGSET phase
|
|
pkgset_phase.start()
|
|
pkgset_phase.stop()
|
|
|
|
# BUILDINSTALL phase - start
|
|
buildinstall_phase.start()
|
|
|
|
# GATHER phase
|
|
gather_phase.start()
|
|
gather_phase.stop()
|
|
|
|
# EXTRA_FILES phase
|
|
extrafiles_phase.start()
|
|
extrafiles_phase.stop()
|
|
|
|
# CREATEREPO phase
|
|
createrepo_phase.start()
|
|
createrepo_phase.stop()
|
|
|
|
# BUILDINSTALL phase
|
|
# must finish before PRODUCTIMG
|
|
# must finish before CREATEISO
|
|
buildinstall_phase.stop()
|
|
if not buildinstall_phase.skip():
|
|
buildinstall_phase.copy_files()
|
|
|
|
ostree_phase.start()
|
|
ostree_phase.stop()
|
|
|
|
# PRODUCTIMG phase
|
|
productimg_phase.start()
|
|
productimg_phase.stop()
|
|
|
|
# write treeinfo before ISOs are created
|
|
for variant in compose.get_variants():
|
|
for arch in variant.arches + ["src"]:
|
|
pungi.metadata.write_tree_info(compose, arch, variant)
|
|
|
|
# write .discinfo and media.repo before ISOs are created
|
|
for variant in compose.get_variants():
|
|
if variant.type == "addon" or variant.is_empty:
|
|
continue
|
|
for arch in variant.arches + ["src"]:
|
|
timestamp = pungi.metadata.write_discinfo(compose, arch, variant)
|
|
pungi.metadata.write_media_repo(compose, arch, variant, timestamp)
|
|
|
|
# CREATEISO and LIVEIMAGES phases
|
|
createiso_phase.start()
|
|
liveimages_phase.start()
|
|
image_build_phase.start()
|
|
livemedia_phase.start()
|
|
ostree_installer_phase.start()
|
|
osbs_phase.start()
|
|
|
|
createiso_phase.stop()
|
|
liveimages_phase.stop()
|
|
image_build_phase.stop()
|
|
livemedia_phase.stop()
|
|
ostree_installer_phase.stop()
|
|
osbs_phase.stop()
|
|
|
|
image_checksum_phase.start()
|
|
image_checksum_phase.stop()
|
|
|
|
pungi.metadata.write_compose_info(compose)
|
|
compose.im.dump(compose.paths.compose.metadata("images.json"))
|
|
osbs_phase.dump_metadata()
|
|
|
|
# TEST phase
|
|
test_phase.start()
|
|
test_phase.stop()
|
|
|
|
compose.write_status("FINISHED")
|
|
latest_link = False
|
|
if create_latest_link:
|
|
if latest_link_status is None:
|
|
# create latest symbol link by default if latest_link_status is not specified
|
|
latest_link = True
|
|
else:
|
|
latest_link_status = [s.upper() for s in latest_link_status]
|
|
if compose.get_status() in [s.upper() for s in latest_link_status]:
|
|
latest_link = True
|
|
else:
|
|
compose.log_warning("Compose status (%s) doesn't match with specified latest-link-status (%s), not create latest link."
|
|
% (compose.get_status(), str(latest_link_status)))
|
|
|
|
if latest_link:
|
|
compose_dir = os.path.basename(compose.topdir)
|
|
if len(compose.conf["release_version"].split(".")) == 1:
|
|
symlink_name = "latest-%s-%s" % (compose.conf["release_short"], compose.conf["release_version"])
|
|
else:
|
|
symlink_name = "latest-%s-%s" % (compose.conf["release_short"], ".".join(compose.conf["release_version"].split(".")[:-1]))
|
|
if compose.conf["release_is_layered"]:
|
|
symlink_name += "-%s-%s" % (compose.conf["base_product_short"], compose.conf["base_product_version"])
|
|
symlink = os.path.join(compose.topdir, "..", symlink_name)
|
|
|
|
try:
|
|
os.unlink(symlink)
|
|
except OSError as ex:
|
|
if ex.errno != 2:
|
|
raise
|
|
try:
|
|
os.symlink(compose_dir, symlink)
|
|
except Exception as ex:
|
|
compose.log_error("Couldn't create latest symlink: %s" % ex)
|
|
|
|
compose.log_info("Compose finished: %s" % compose.topdir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (Exception, KeyboardInterrupt) as ex:
|
|
if COMPOSE:
|
|
tb_path = COMPOSE.paths.log.log_file("global", "traceback")
|
|
COMPOSE.log_error("Exception: %s" % ex)
|
|
COMPOSE.log_error("Extended traceback in: %s" % tb_path)
|
|
COMPOSE.log_critical("Compose failed: %s" % COMPOSE.topdir)
|
|
COMPOSE.write_status("DOOMED")
|
|
import kobo.tback
|
|
open(tb_path, "w").write(kobo.tback.Traceback().get_traceback())
|
|
else:
|
|
print("Exception: %s" % ex)
|
|
raise
|
|
sys.stdout.flush()
|
|
sys.stderr.flush()
|
|
sys.exit(1)
|