pungi/bin/pungi-koji
Lubomír Sedlář 87884b6412 Add timezone info into logs
Timestamps in the main log are currently printed in local time. This
patch adds UTC offset to the top of the log so it's possible to find out
exactly when events happen.

Fixes: https://pagure.io/pungi/issue/710
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
2017-09-08 11:05:57 +02:00

479 lines
16 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import sys
import argparse
import logging
import locale
import datetime
import getpass
import socket
import time
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 = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--target-dir",
metavar="PATH",
help="a compose is created under this directory",
)
group.add_argument(
"--compose-dir",
metavar="PATH",
help="reuse an existing compose directory (DANGEROUS!)",
)
parser.add_argument(
"--label",
help="specify compose label (example: Snapshot-1.0); required for production composes"
)
parser.add_argument(
"--no-label",
action="store_true",
default=False,
help="make a production compose without label"
)
parser.add_argument(
"--supported",
action="store_true",
default=False,
help="set supported flag on media (automatically on for 'RC-x.y' labels)"
)
parser.add_argument(
"--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_argument(
"--debug-mode",
action="store_true",
default=False,
help="run pungi in DEBUG mode (DANGEROUS!)",
)
parser.add_argument(
"--config",
help="Config file",
required=True
)
parser.add_argument(
"--skip-phase",
metavar="PHASE",
action="append",
default=[],
help="skip a compose phase",
)
parser.add_argument(
"--just-phase",
metavar="PHASE",
action="append",
default=[],
help="run only a specified compose phase",
)
parser.add_argument(
"--nightly",
action="store_const",
const="nightly",
dest="compose_type",
help="make a nightly compose",
)
parser.add_argument(
"--test",
action="store_const",
const="test",
dest="compose_type",
help="make a test compose",
)
parser.add_argument(
"--ci",
action="store_const",
const="ci",
dest="compose_type",
help="make a CI compose",
)
parser.add_argument(
"--production",
action="store_const",
const="production",
dest="compose_type",
help="make production compose (default unless config specifies otherwise)",
)
parser.add_argument(
"--koji-event",
metavar="ID",
type=int,
help="specify a koji event for populating package set",
)
parser.add_argument(
"--version",
action="store_true",
help="output version information and exit",
)
parser.add_argument(
"--notification-script",
action="append",
default=[],
help="script for sending progress notification messages"
)
parser.add_argument(
"--no-latest-link",
action="store_true",
default=False,
dest="no_latest_link",
help="don't create latest symbol link to this compose"
)
parser.add_argument(
"--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_argument(
"--quiet",
action="store_true",
default=False,
help="quiet mode, don't print log on screen"
)
opts = 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 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)
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)
compose_type = opts.compose_type or conf.get('compose_type', 'production')
if compose_type == "production" and not opts.label and not opts.no_label:
abort("must specify label for a production compose")
# 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(warning, file=sys.stderr)
if errors:
for error in errors:
print(error, file=sys.stderr)
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
run_compose(compose, create_latest_link=create_latest_link, latest_link_status=latest_link_status)
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)
is_dst = time.daylight and time.localtime().tm_isdst > 0
compose.log_info("Current timezone offset: %s"
% time.altzone if is_dst else time.timezone)
compose.read_variants()
# dump the config file
date_str = datetime.datetime.strftime(datetime.datetime.now(), "%F_%X").replace(":", "-")
for config_file in compose.conf.opened_files:
config_dump = compose.paths.log.log_file("global", "config-copy_%s_%s"
% (os.path.basename(config_file), date_str))
open(config_dump, "w").write(open(config_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, we can run gathering, extra files and
# createrepo while buildinstall is in progress.
buildinstall_phase.start()
# If any of the following three phases fail, we must ensure that
# buildinstall is stopped. Otherwise the whole process will hang.
try:
gather_phase.start()
gather_phase.stop()
extrafiles_phase.start()
extrafiles_phase.stop()
createrepo_phase.start()
createrepo_phase.stop()
finally:
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)
# Start all phases for image artifacts
pungi.phases.run_all([createiso_phase,
liveimages_phase,
image_build_phase,
livemedia_phase,
ostree_installer_phase,
osbs_phase])
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("Compose run failed: %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)