Fixes: https://pagure.io/pungi/issue/994 Merges: https://pagure.io/pungi/pull-request/1013 Signed-off-by: Mohan Boddu <mboddu@bhujji.com>
		
			
				
	
	
		
			505 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			505 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python
 | |
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| from __future__ import print_function
 | |
| 
 | |
| import argparse
 | |
| import getpass
 | |
| import json
 | |
| import locale
 | |
| import logging
 | |
| import os
 | |
| import socket
 | |
| import signal
 | |
| import sys
 | |
| import traceback
 | |
| import shutil
 | |
| 
 | |
| from six.moves import shlex_quote
 | |
| 
 | |
| here = sys.path[0]
 | |
| if here != '/usr/bin':
 | |
|     # Git checkout
 | |
|     sys.path[0] = os.path.dirname(here)
 | |
| 
 | |
| from pungi.phases import PHASES_NAMES
 | |
| from pungi import get_full_version, util
 | |
| 
 | |
| 
 | |
| # 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",
 | |
|         choices=PHASES_NAMES,
 | |
|         action="append",
 | |
|         default=[],
 | |
|         help="skip a compose phase",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "--just-phase",
 | |
|         metavar="PHASE",
 | |
|         choices=PHASES_NAMES,
 | |
|         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=util.parse_koji_event,
 | |
|         help="specify a koji event for populating package set, either as event ID "
 | |
|              "or a path to a compose from which to reuse the event",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "--version",
 | |
|         action="version",
 | |
|         version=get_full_version(),
 | |
|         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(
 | |
|         "--print-output-dir",
 | |
|         action="store_true",
 | |
|         default=False,
 | |
|         help="print the compose directory"
 | |
|     )
 | |
|     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.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()
 | |
|     if opts.config.endswith(".json"):
 | |
|         with open(opts.config) as f:
 | |
|             conf.load_from_dict(json.load(f))
 | |
|         conf.opened_files = [opts.config]
 | |
|         conf._open_file = opts.config
 | |
|     else:
 | |
|         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)
 | |
|     if not opts.quiet:
 | |
|         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
 | |
| 
 | |
|     if opts.print_output_dir:
 | |
|         print('Compose dir: %s' % 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
 | |
|     import pungi.util
 | |
| 
 | |
|     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([shlex_quote(arg) for arg in sys.argv]))
 | |
|     compose.log_info("Compose top directory: %s" % compose.topdir)
 | |
|     compose.log_info("Current timezone offset: %s" % pungi.util.get_tz_offset())
 | |
|     compose.read_variants()
 | |
| 
 | |
|     # dump the config file
 | |
|     config_copy_path = os.path.join(compose.paths.log.topdir(), "config-copy")
 | |
|     if not os.path.exists(config_copy_path):
 | |
|         os.makedirs(config_copy_path)
 | |
|     for config_file in compose.conf.opened_files:
 | |
|         shutil.copy2(config_file, config_copy_path)
 | |
|     config_dump_full = compose.paths.log.log_file("global", "config-dump")
 | |
|     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, buildinstall_phase)
 | |
|     ostree_phase = pungi.phases.OSTreePhase(compose)
 | |
|     productimg_phase = pungi.phases.ProductimgPhase(compose, pkgset_phase)
 | |
|     createiso_phase = pungi.phases.CreateisoPhase(compose, buildinstall_phase)
 | |
|     extra_isos_phase = pungi.phases.ExtraIsosPhase(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,
 | |
|                   extra_isos_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)
 | |
|         raise RuntimeError('Configuration is not valid')
 | |
| 
 | |
|     # 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)
 | |
|                 raise RuntimeError(err_msg)
 | |
| 
 | |
|         if signing_key_password:
 | |
|             # Store the password
 | |
|             compose.conf["signing_key_password"] = signing_key_password
 | |
| 
 | |
|     init_phase.start()
 | |
|     init_phase.stop()
 | |
| 
 | |
|     pkgset_phase.start()
 | |
|     pkgset_phase.stop()
 | |
| 
 | |
|     # WEAVER phase - launches other phases which can safely run in parallel
 | |
|     essentials_schema = (
 | |
|         buildinstall_phase,
 | |
|         (gather_phase, extrafiles_phase, createrepo_phase),
 | |
|         (ostree_phase, ostree_installer_phase),
 | |
|     )
 | |
|     essentials_phase = pungi.phases.WeaverPhase(compose, essentials_schema)
 | |
|     essentials_phase.start()
 | |
|     essentials_phase.stop()
 | |
| 
 | |
|     if not buildinstall_phase.skip():
 | |
|         buildinstall_phase.copy_files()
 | |
| 
 | |
|     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, bi=buildinstall_phase)
 | |
| 
 | |
|     # 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
 | |
|     compose_images_schema = (
 | |
|         createiso_phase,
 | |
|         extra_isos_phase,
 | |
|         liveimages_phase,
 | |
|         image_build_phase,
 | |
|         livemedia_phase,
 | |
|         osbs_phase,
 | |
|     )
 | |
|     compose_images_phase = pungi.phases.WeaverPhase(compose, compose_images_schema)
 | |
|     compose_images_phase.start()
 | |
|     compose_images_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.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)
 | |
|             raise
 | |
| 
 | |
|     compose.log_info("Compose finished: %s" % compose.topdir)
 | |
| 
 | |
| 
 | |
| def sigterm_handler(signum, frame):
 | |
|     if COMPOSE:
 | |
|         COMPOSE.log_error("Compose run failed: signal %s" % signum)
 | |
|         COMPOSE.log_error("Traceback:\n%s"
 | |
|                           % '\n'.join(traceback.format_stack(frame)))
 | |
|         COMPOSE.log_critical("Compose failed: %s" % COMPOSE.topdir)
 | |
|         COMPOSE.write_status("TERMINATED")
 | |
|     else:
 | |
|         print("Signal %s captured" % signum)
 | |
|     sys.stdout.flush()
 | |
|     sys.stderr.flush()
 | |
|     sys.exit(1)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     signal.signal(signal.SIGTERM, sigterm_handler)
 | |
| 
 | |
|     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
 | |
|             with open(tb_path, "wb") as f:
 | |
|                 f.write(kobo.tback.Traceback().get_traceback())
 | |
|         else:
 | |
|             print("Exception: %s" % ex)
 | |
|             raise
 | |
|         sys.stdout.flush()
 | |
|         sys.stderr.flush()
 | |
|         sys.exit(1)
 |