From b2f995d516fccd0f7bd639a1ff52846d7a720a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Sedl=C3=A1=C5=99?= Date: Wed, 9 May 2018 15:39:36 +0200 Subject: [PATCH] Add a phase for creating extra ISOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lubomír Sedlář --- bin/pungi-config-validate | 1 + bin/pungi-koji | 4 +- doc/_static/phases.png | Bin 25108 -> 24469 bytes doc/_static/phases.svg | 300 ++++++++++--------- doc/configuration.rst | 79 +++++ doc/phases.rst | 9 + pungi/checks.py | 41 +++ pungi/paths.py | 12 + pungi/phases/__init__.py | 1 + pungi/phases/createiso.py | 20 +- pungi/phases/extra_isos.py | 201 +++++++++++++ pungi/util.py | 9 +- tests/data/dummy-pungi.conf | 7 + tests/test_extra_isos_phase.py | 529 +++++++++++++++++++++++++++++++++ 14 files changed, 1057 insertions(+), 156 deletions(-) create mode 100644 pungi/phases/extra_isos.py create mode 100644 tests/test_extra_isos_phase.py diff --git a/bin/pungi-config-validate b/bin/pungi-config-validate index 815d14fe..a2bc5814 100755 --- a/bin/pungi-config-validate +++ b/bin/pungi-config-validate @@ -107,6 +107,7 @@ def run(config, topdir, has_old): pungi.phases.OSTreePhase(compose), pungi.phases.ProductimgPhase(compose, pkgset_phase), pungi.phases.CreateisoPhase(compose, buildinstall_phase), + pungi.phases.ExtraIsosPhase(compose), pungi.phases.LiveImagesPhase(compose), pungi.phases.LiveMediaPhase(compose), pungi.phases.ImageBuildPhase(compose), diff --git a/bin/pungi-koji b/bin/pungi-koji index 0d1b892e..b3b49de5 100755 --- a/bin/pungi-koji +++ b/bin/pungi-koji @@ -300,6 +300,7 @@ def run_compose(compose, create_latest_link=True, latest_link_status=None): 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) @@ -313,7 +314,7 @@ def run_compose(compose, create_latest_link=True, latest_link_status=None): extrafiles_phase, createiso_phase, liveimages_phase, livemedia_phase, image_build_phase, image_checksum_phase, test_phase, ostree_phase, ostree_installer_phase, - osbs_phase): + extra_isos_phase, osbs_phase): if phase.skip(): continue try: @@ -402,6 +403,7 @@ def run_compose(compose, create_latest_link=True, latest_link_status=None): # Start all phases for image artifacts compose_images_schema = ( createiso_phase, + extra_isos_phase, liveimages_phase, image_build_phase, livemedia_phase, diff --git a/doc/_static/phases.png b/doc/_static/phases.png index a6d40383e1df3aa7dbe86ebee3811710db742c4c..bca203e0ec0a5c64c16348d38aee406ea4332592 100644 GIT binary patch literal 24469 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 D>C7|n literal 25108 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 DpC}MV diff --git a/doc/_static/phases.svg b/doc/_static/phases.svg index 66184b15..7bbc5e1f 100644 --- a/doc/_static/phases.svg +++ b/doc/_static/phases.svg @@ -9,12 +9,12 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="839.33331" - height="220.33334" - viewBox="0 0 839.33334 220.33335" + width="771.66455" + height="221.50018" + viewBox="0 0 771.66458 221.50019" id="svg2" version="1.1" - inkscape:version="0.91 r13725" + inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="phases.svg" inkscape:export-filename="/home/lsedlar/repos/pungi/doc/_static/phases.png" inkscape:export-xdpi="90" @@ -26,21 +26,25 @@ borderopacity="1.0" inkscape:pageopacity="1" inkscape:pageshadow="2" - inkscape:zoom="1.6532468" - inkscape:cx="337.4932" - inkscape:cy="70.825454" + inkscape:zoom="1.169022" + inkscape:cx="396.63448" + inkscape:cy="97.894202" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1920" - inkscape:window-height="1020" + inkscape:window-height="1016" inkscape:window-x="1920" - inkscape:window-y="31" + inkscape:window-y="27" inkscape:window-maximized="1" units="px" inkscape:document-rotation="0" showguides="true" - inkscape:guide-bbox="true" /> + inkscape:guide-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> image/svg+xml - + @@ -115,8 +119,7 @@ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" x="556.95709" y="971.54041" - id="text3384-0" - sodipodi:linespacing="0%"> OSTreeInstaller + y="1039.4121" + style="font-size:11.99999714px;line-height:0">OSTreeInstaller - - - + + Createiso - - - - Createiso + + + + LiveImages - - - - LiveImages + + + + ImageBuild - - - - ImageBuild + + + + LiveMedia - - - - LiveMedia + + + + OSBS - + y="1065.7078">OSBS + + + + ExtraIsos diff --git a/doc/configuration.rst b/doc/configuration.rst index d28ffdca..c8135093 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -1450,6 +1450,85 @@ Example config } +Extra ISOs +========== + +Create an ISO image that contains packages from multiple variants. Such ISO +always belongs to one variant, and will be stored in ISO directory of that +variant. + +The ISO will be bootable if buildinstall phase runs for the parent variant. It +will reuse boot configuration from that variant. + +**extra_isos** + (*dict*) -- a mapping from variant UID regex to a list of configuration + blocks. + + * ``include_variants`` -- (*list*) list of variant UIDs from which content + should be added to the ISO; the variant of this image is added + automatically. + + Rest of configuration keys is optional. + + * ``filename`` -- (*str*) template for naming the image. In addition to the + regular placeholders ``filename`` is available with the name generated + using ``image_name_format`` option. + + * ``volid`` -- (*str*) template for generating volume ID. Again ``volid`` + placeholder can be used similarly as for file name. This can also be a + list of templates that will be tried sequentially until one generates a + volume ID that fits into 32 character limit. + + * ``extra_files`` -- (*list*) a list of :ref:`scm_dict ` + objects. These files will be put in the top level directory of the image. + + * ``arches`` -- (*list*) a list of architectures for which to build this + image. By default all arches from the variant will be used. This option + can be used to limit them. + + * ``failable_arches`` -- (*list*) a list of architectures for which the + image can fail to be generated and not fail the entire compose. + + * ``skip_src`` -- (*bool*) allows to disable creating an image with source + packages. + +Example config +-------------- +:: + + extra_isos = { + 'Server': [{ + # Will generate foo-DP-1.0-20180510.t.43-Server-x86_64-dvd1.iso + 'filename': 'foo-{filename}', + 'volid': 'foo-{arch}', + + 'extra_files': [{ + 'scm': 'git', + 'repo': 'https://pagure.io/pungi.git', + 'file': 'setup.py' + }], + + 'include_variants': ['Client'] + }] + } + # This should create image with the following layout: + # . + # ├── Client + # │   ├── Packages + # │   │ ├── a + # │   │ └── b + # │   └── repodata + # ├── Server + # │   ├── extra_files.json # extra file from Server + # │   ├── LICENSE # extra file from Server + # │   ├── Packages + # │   │ ├── a + # │   │ └── b + # │   └── repodata + # └── setup.py + + + Media Checksums Settings ======================== diff --git a/doc/phases.rst b/doc/phases.rst index 61704406..6b04932a 100644 --- a/doc/phases.rst +++ b/doc/phases.rst @@ -99,6 +99,15 @@ packages fit on a single image. There can also be images with source repositories. These are never bootable. +ExtraIsos +--------- + +This phase is very similar to ``createiso``, except it combines content from +multiple variants onto a single image. Packages, repodata and extra files from +each configured variant are put into a subdirectory. Additional extra files can +be put into top level of the image. The image will be bootable if the main +variant is bootable. + LiveImages, LiveMedia --------------------- diff --git a/pungi/checks.py b/pungi/checks.py index 335034f6..7d5e01c4 100644 --- a/pungi/checks.py +++ b/pungi/checks.py @@ -863,6 +863,47 @@ def make_schema(): "$ref": "#/definitions/list_of_strings" }), + "extra_isos": { + "type": "object", + "patternProperties": { + # Warning: this pattern is a variant uid regex, but the + # format does not let us validate it as there is no regular + # expression to describe all regular expressions. + ".+": { + "type": "array", + "items": { + "type": "object", + "properties": { + "include_variants": {"$ref": "#/definitions/strings"}, + "extra_files": _one_or_list({ + "type": "object", + "properties": { + "scm": {"type": "string"}, + "repo": {"type": "string"}, + "branch": {"$ref": "#/definitions/optional_string"}, + "file": {"$ref": "#/definitions/strings"}, + "target": {"type": "string"}, + }, + "additionalProperties": False, + }), + "filename": {"type": "string"}, + "volid": {"$ref": "#/definitions/strings"}, + "arches": {"$ref": "#/definitions/list_of_strings"}, + "failable_arches": { + "$ref": "#/definitions/list_of_strings" + }, + "skip_src": { + "type": "boolean", + "default": False, + }, + }, + "required": ["include_variants"], + "additionalProperties": False + } + } + } + }, + "live_media": { "type": "object", "patternProperties": { diff --git a/pungi/paths.py b/pungi/paths.py index cfed2903..bb07005b 100644 --- a/pungi/paths.py +++ b/pungi/paths.py @@ -264,6 +264,18 @@ class WorkPaths(object): makedirs(path) return path + def extra_iso_extra_files_dir(self, arch, variant, create_dir=True): + """ + Examples: + work/x86_64/Server/extra-iso-extra-files + """ + if arch == "global": + raise RuntimeError("Global extra files dir makes no sense.") + path = os.path.join(self.topdir(arch, create_dir=create_dir), variant.uid, "extra-iso-extra-files") + if create_dir: + makedirs(path) + return path + def repo_package_list(self, arch, variant, pkg_type=None, create_dir=True): """ Examples: diff --git a/pungi/phases/__init__.py b/pungi/phases/__init__.py index 095a3136..0ed3e564 100644 --- a/pungi/phases/__init__.py +++ b/pungi/phases/__init__.py @@ -25,6 +25,7 @@ from .product_img import ProductimgPhase # noqa from .buildinstall import BuildinstallPhase # noqa from .extra_files import ExtraFilesPhase # noqa from .createiso import CreateisoPhase # noqa +from .extra_isos import ExtraIsosPhase # noqa from .live_images import LiveImagesPhase # noqa from .image_build import ImageBuildPhase # noqa from .test import TestPhase # noqa diff --git a/pungi/phases/createiso.py b/pungi/phases/createiso.py index 6e7a053c..29e10e6f 100644 --- a/pungi/phases/createiso.py +++ b/pungi/phases/createiso.py @@ -395,13 +395,7 @@ def prepare_iso(compose, arch, variant, disc_num=1, disc_count=None, split_iso_d if i in ti.checksums.checksums.keys(): del ti.checksums.checksums[i] - # make a copy of isolinux/isolinux.bin, images/boot.img - they get modified when mkisofs is called - for i in ("isolinux/isolinux.bin", "images/boot.img"): - src_path = os.path.join(tree_dir, i) - dst_path = os.path.join(iso_dir, i) - if os.path.exists(src_path): - makedirs(os.path.dirname(dst_path)) - shutil.copy2(src_path, dst_path) + copy_boot_images(tree_dir, iso_dir) if disc_count > 1: # remove repodata/repomd.xml from checksums, create a new one later @@ -454,3 +448,15 @@ def prepare_iso(compose, arch, variant, disc_num=1, disc_count=None, split_iso_d gp = "%s-graft-points" % iso_dir iso.write_graft_points(gp, data, exclude=["*/lost+found", "*/boot.iso"]) return gp + + +def copy_boot_images(src, dest): + """When mkisofs is called it tries to modify isolinux/isolinux.bin and + images/boot.img. Therefore we need to make copies of them. + """ + for i in ("isolinux/isolinux.bin", "images/boot.img"): + src_path = os.path.join(src, i) + dst_path = os.path.join(dest, i) + if os.path.exists(src_path): + makedirs(os.path.dirname(dst_path)) + shutil.copy2(src_path, dst_path) diff --git a/pungi/phases/extra_isos.py b/pungi/phases/extra_isos.py new file mode 100644 index 00000000..574fe9cf --- /dev/null +++ b/pungi/phases/extra_isos.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + + +# 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; version 2 of the License. +# +# 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 Library 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 . + +import os + +from kobo.shortcuts import force_list +from kobo.threads import ThreadPool, WorkerThread + +from pungi import createiso +from pungi.phases.base import ConfigGuardedPhase, PhaseBase, PhaseLoggerMixin +from pungi.phases.createiso import (add_iso_to_metadata, copy_boot_images, + run_createiso_command) +from pungi.util import failable, get_format_substs, get_variant_data, get_volid +from pungi.wrappers import iso +from pungi.wrappers.scm import get_dir_from_scm, get_file_from_scm + + +class ExtraIsosPhase(PhaseLoggerMixin, ConfigGuardedPhase, PhaseBase): + name = "extra_isos" + + def __init__(self, compose): + super(ExtraIsosPhase, self).__init__(compose) + self.pool = ThreadPool(logger=self.logger) + + def validate(self): + for variant in self.compose.get_variants(types=['variant']): + for config in get_variant_data(self.compose.conf, self.name, variant): + extra_arches = set(config.get('arches', [])) - set(variant.arches) + if extra_arches: + self.compose.log_warning( + 'Extra iso config for %s mentions non-existing arches: %s' + % (variant, ', '.join(sorted(extra_arches)))) + + def run(self): + commands = [] + + for variant in self.compose.get_variants(types=['variant']): + for config in get_variant_data(self.compose.conf, self.name, variant): + arches = set(variant.arches) + if config.get('arches'): + arches &= set(config['arches']) + if not config['skip_src']: + arches.add('src') + for arch in sorted(arches): + commands.append((config, variant, arch)) + + for (config, variant, arch) in commands: + self.pool.add(ExtraIsosThread(self.pool)) + self.pool.queue_put((self.compose, config, variant, arch)) + + self.pool.start() + + +class ExtraIsosThread(WorkerThread): + def process(self, item, num): + self.num = num + compose, config, variant, arch = item + can_fail = arch in config.get('failable_arches', []) + with failable(compose, can_fail, variant, arch, 'extra_iso', logger=self.pool._logger): + self.worker(compose, config, variant, arch) + + def worker(self, compose, config, variant, arch): + filename = get_filename(compose, variant, arch, config.get('filename')) + volid = get_volume_id(compose, variant, arch, config.get('volid', [])) + iso_dir = compose.paths.compose.iso_dir(arch, variant) + iso_path = os.path.join(iso_dir, filename) + + msg = "Creating ISO (arch: %s, variant: %s): %s" % (arch, variant, filename) + self.pool.log_info("[BEGIN] %s" % msg) + + get_extra_files(compose, variant, arch, config.get('extra_files', [])) + + bootable = arch != "src" and compose.conf['bootable'] + + graft_points = get_iso_contents(compose, variant, arch, + config['include_variants'], + filename, bootable) + + opts = createiso.CreateIsoOpts( + output_dir=iso_dir, + iso_name=filename, + volid=volid, + graft_points=graft_points, + arch=arch, + supported=compose.supported, + ) + + if bootable: + opts = opts._replace(buildinstall_method=compose.conf['buildinstall_method']) + + script_file = os.path.join(compose.paths.work.tmp_dir(arch, variant), + 'extraiso-%s.sh' % filename) + with open(script_file, 'w') as f: + createiso.write_script(opts, f) + + run_createiso_command(compose.conf["runroot"], self.num, compose, bootable, arch, + ['bash', script_file], [compose.topdir], + log_file=compose.paths.log.log_file( + arch, "extraiso-%s" % os.path.basename(iso_path)), + with_jigdo=False) + + add_iso_to_metadata(compose, variant, arch, iso_path, bootable, 1, 1) + + self.pool.log_info("[DONE ] %s" % msg) + + +def get_extra_files(compose, variant, arch, extra_files): + """Clone the configured files into a directory from where they can be + included in the ISO. + """ + extra_files_dir = compose.paths.work.extra_iso_extra_files_dir(arch, variant) + for scm_dict in extra_files: + getter = get_file_from_scm if 'file' in scm_dict else get_dir_from_scm + target_path = os.path.join(extra_files_dir, scm_dict.get('target', '').lstrip('/')) + getter(scm_dict, target_path, logger=compose._logger) + + +def get_iso_contents(compose, variant, arch, include_variants, filename, bootable): + """Find all files that should be on the ISO. For bootable image we start + with the boot configuration. Then for each variant we add packages, + repodata and extra files. Finally we add top-level extra files. + """ + iso_dir = compose.paths.work.iso_dir(arch, filename) + + files = {} + if bootable: + buildinstall_dir = compose.paths.work.buildinstall_dir(arch, create_dir=False) + if compose.conf['buildinstall_method'] == 'lorax': + buildinstall_dir = os.path.join(buildinstall_dir, variant.uid) + + copy_boot_images(buildinstall_dir, iso_dir) + files = iso.get_graft_points([buildinstall_dir, iso_dir]) + + variants = [variant.uid] + include_variants + for variant_uid in variants: + var = compose.all_variants[variant_uid] + + # Get packages... + package_dir = compose.paths.compose.packages(arch, var) + for k, v in iso.get_graft_points([package_dir]).items(): + files[os.path.join(var.uid, 'Packages', k)] = v + + # Get repodata... + tree_dir = compose.paths.compose.repository(arch, var) + repo_dir = os.path.join(tree_dir, 'repodata') + for k, v in iso.get_graft_points([repo_dir]).items(): + files[os.path.join(var.uid, 'repodata', k)] = v + + # Get extra files... + extra_files_dir = compose.paths.work.extra_files_dir(arch, var) + for k, v in iso.get_graft_points([extra_files_dir]).items(): + files[os.path.join(var.uid, k)] = v + + # Add extra files specific for the ISO + extra_files_dir = compose.paths.work.extra_iso_extra_files_dir(arch, variant) + files.update(iso.get_graft_points([extra_files_dir])) + + gp = "%s-graft-points" % iso_dir + iso.write_graft_points(gp, files, exclude=["*/lost+found", "*/boot.iso"]) + return gp + + +def get_filename(compose, variant, arch, format): + disc_type = compose.conf['disc_types'].get('dvd', 'dvd') + base_filename = compose.get_image_name( + arch, variant, disc_type=disc_type, disc_num=1) + if not format: + return base_filename + kwargs = { + 'arch': arch, + 'disc_type': disc_type, + 'disc_num': 1, + 'suffix': '.iso', + 'filename': base_filename, + 'variant': variant, + } + args = get_format_substs(compose, **kwargs) + try: + return (format % args).format(**args) + except KeyError as err: + raise RuntimeError('Failed to create image name: unknown format element: %s' % err) + + +def get_volume_id(compose, variant, arch, formats): + disc_type = compose.conf['disc_types'].get('dvd', 'dvd') + # Get volume ID for regular ISO so that we can substitute it in. + volid = get_volid(compose, arch, variant, disc_type=disc_type) + return get_volid(compose, arch, variant, disc_type=disc_type, + formats=force_list(formats), volid=volid) diff --git a/pungi/util.py b/pungi/util.py index e2b07de6..ea0f6e48 100644 --- a/pungi/util.py +++ b/pungi/util.py @@ -330,7 +330,8 @@ def _apply_substitutions(compose, volid): return volid -def get_volid(compose, arch, variant=None, escape_spaces=False, disc_type=False): +def get_volid(compose, arch, variant=None, escape_spaces=False, disc_type=False, + formats=None, **kwargs): """Get ISO volume ID for arch and variant""" if variant and variant.type == "addon": # addons are part of parent variant media @@ -359,9 +360,10 @@ def get_volid(compose, arch, variant=None, escape_spaces=False, disc_type=False) all_products = layered_products + products else: all_products = products + formats = formats or all_products tried = set() - for i in all_products: + for i in formats: if not variant_uid and "%(variant)s" in i: continue try: @@ -372,7 +374,8 @@ def get_volid(compose, arch, variant=None, escape_spaces=False, disc_type=False) arch=arch, disc_type=disc_type or '', base_product_short=base_product_short, - base_product_version=base_product_version) + base_product_version=base_product_version, + **kwargs) volid = (i % args).format(**args) except KeyError as err: raise RuntimeError('Failed to create volume id: unknown format element: %s' % err) diff --git a/tests/data/dummy-pungi.conf b/tests/data/dummy-pungi.conf index 2d1f21f1..120c44f1 100644 --- a/tests/data/dummy-pungi.conf +++ b/tests/data/dummy-pungi.conf @@ -114,4 +114,11 @@ createiso_skip = [ }), ] +extra_isos = { + '^Server$': [{ + 'include_variants': ['Client'] + 'filename': 'extra-{filename}', + }] +} + create_jigdo = False diff --git a/tests/test_extra_isos_phase.py b/tests/test_extra_isos_phase.py new file mode 100644 index 00000000..420d0ffe --- /dev/null +++ b/tests/test_extra_isos_phase.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +try: + import unittest2 as unittest +except ImportError: + import unittest +import mock + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from tests import helpers +from pungi.phases import extra_isos + + +@mock.patch('pungi.phases.extra_isos.ThreadPool') +class ExtraIsosPhaseTest(helpers.PungiTestCase): + + def test_logs_extra_arches(self, ThreadPool): + cfg = { + 'include_variants': ['Client'], + 'arches': ['x86_64', 'ppc64le', 'aarch64'], + } + compose = helpers.DummyCompose(self.topdir, { + 'extra_isos': { + '^Server$': [cfg] + } + }) + + phase = extra_isos.ExtraIsosPhase(compose) + phase.validate() + + self.assertEqual(len(compose.log_warning.call_args_list), 1) + + def test_one_task_for_each_arch(self, ThreadPool): + cfg = { + 'include_variants': ['Client'], + } + compose = helpers.DummyCompose(self.topdir, { + 'extra_isos': { + '^Server$': [cfg] + } + }) + + phase = extra_isos.ExtraIsosPhase(compose) + phase.run() + + self.assertEqual(len(ThreadPool.return_value.add.call_args_list), 3) + self.assertItemsEqual( + ThreadPool.return_value.queue_put.call_args_list, + [mock.call((compose, cfg, compose.variants['Server'], 'x86_64')), + mock.call((compose, cfg, compose.variants['Server'], 'amd64')), + mock.call((compose, cfg, compose.variants['Server'], 'src'))] + ) + + def test_filter_arches(self, ThreadPool): + cfg = { + 'include_variants': ['Client'], + 'arches': ['x86_64'], + } + compose = helpers.DummyCompose(self.topdir, { + 'extra_isos': { + '^Server$': [cfg] + } + }) + + phase = extra_isos.ExtraIsosPhase(compose) + phase.run() + + self.assertEqual(len(ThreadPool.return_value.add.call_args_list), 2) + self.assertItemsEqual( + ThreadPool.return_value.queue_put.call_args_list, + [mock.call((compose, cfg, compose.variants['Server'], 'x86_64')), + mock.call((compose, cfg, compose.variants['Server'], 'src'))] + ) + + def test_skip_source(self, ThreadPool): + cfg = { + 'include_variants': ['Client'], + 'skip_src': True, + } + compose = helpers.DummyCompose(self.topdir, { + 'extra_isos': { + '^Server$': [cfg] + } + }) + + phase = extra_isos.ExtraIsosPhase(compose) + phase.run() + + self.assertEqual(len(ThreadPool.return_value.add.call_args_list), 2) + self.assertItemsEqual( + ThreadPool.return_value.queue_put.call_args_list, + [mock.call((compose, cfg, compose.variants['Server'], 'x86_64')), + mock.call((compose, cfg, compose.variants['Server'], 'amd64'))] + ) + + +@mock.patch('pungi.phases.extra_isos.get_volume_id') +@mock.patch('pungi.phases.extra_isos.get_filename') +@mock.patch('pungi.phases.extra_isos.get_iso_contents') +@mock.patch('pungi.phases.extra_isos.get_extra_files') +@mock.patch('pungi.phases.extra_isos.run_createiso_command') +@mock.patch('pungi.phases.extra_isos.add_iso_to_metadata') +class ExtraIsosThreadTest(helpers.PungiTestCase): + + def test_binary_bootable_image(self, aitm, rcc, gef, gic, gfn, gvi): + compose = helpers.DummyCompose(self.topdir, { + 'bootable': True, + 'buildinstall_method': 'lorax' + }) + server = compose.variants['Server'] + cfg = { + 'include_variants': ['Client'], + } + + gfn.return_value = 'my.iso' + gvi.return_value = 'my volume id' + gic.return_value = '/tmp/iso-graft-points' + + t = extra_isos.ExtraIsosThread(mock.Mock()) + with mock.patch('time.sleep'): + t.process((compose, cfg, server, 'x86_64'), 1) + + self.assertEqual(gfn.call_args_list, + [mock.call(compose, server, 'x86_64', None)]) + self.assertEqual(gvi.call_args_list, + [mock.call(compose, server, 'x86_64', [])]) + self.assertEqual(gef.call_args_list, + [mock.call(compose, server, 'x86_64', [])]) + self.assertEqual(gic.call_args_list, + [mock.call(compose, server, 'x86_64', ['Client'], 'my.iso', True)]) + self.assertEqual( + rcc.call_args_list, + [mock.call(False, 1, compose, True, 'x86_64', + ['bash', os.path.join(self.topdir, 'work/x86_64/tmp-Server/extraiso-my.iso.sh')], + [self.topdir], + log_file=os.path.join(self.topdir, 'logs/x86_64/extraiso-my.iso.x86_64.log'), + with_jigdo=False)] + + ) + self.assertEqual( + aitm.call_args_list, + [mock.call(compose, server, 'x86_64', + os.path.join(self.topdir, 'compose/Server/x86_64/iso/my.iso'), + True, 1, 1)] + ) + + def test_binary_image_custom_naming(self, aitm, rcc, gef, gic, gfn, gvi): + compose = helpers.DummyCompose(self.topdir, {}) + server = compose.variants['Server'] + cfg = { + 'include_variants': ['Client'], + 'filename': 'fn', + 'volid': ['v1', 'v2'], + } + + gfn.return_value = 'my.iso' + gvi.return_value = 'my volume id' + gic.return_value = '/tmp/iso-graft-points' + + t = extra_isos.ExtraIsosThread(mock.Mock()) + with mock.patch('time.sleep'): + t.process((compose, cfg, server, 'x86_64'), 1) + + self.assertEqual(gfn.call_args_list, + [mock.call(compose, server, 'x86_64', 'fn')]) + self.assertEqual(gvi.call_args_list, + [mock.call(compose, server, 'x86_64', ['v1', 'v2'])]) + self.assertEqual(gef.call_args_list, + [mock.call(compose, server, 'x86_64', [])]) + self.assertEqual(gic.call_args_list, + [mock.call(compose, server, 'x86_64', ['Client'], 'my.iso', False)]) + self.assertEqual( + rcc.call_args_list, + [mock.call(False, 1, compose, False, 'x86_64', + ['bash', os.path.join(self.topdir, 'work/x86_64/tmp-Server/extraiso-my.iso.sh')], + [self.topdir], + log_file=os.path.join(self.topdir, 'logs/x86_64/extraiso-my.iso.x86_64.log'), + with_jigdo=False)] + + ) + self.assertEqual( + aitm.call_args_list, + [mock.call(compose, server, 'x86_64', + os.path.join(self.topdir, 'compose/Server/x86_64/iso/my.iso'), + False, 1, 1)] + ) + + def test_source_is_not_bootable(self, aitm, rcc, gef, gic, gfn, gvi): + compose = helpers.DummyCompose(self.topdir, { + 'bootable': True, + 'buildinstall_method': 'lorax' + }) + server = compose.variants['Server'] + cfg = { + 'include_variants': ['Client'], + } + + gfn.return_value = 'my.iso' + gvi.return_value = 'my volume id' + gic.return_value = '/tmp/iso-graft-points' + + t = extra_isos.ExtraIsosThread(mock.Mock()) + with mock.patch('time.sleep'): + t.process((compose, cfg, server, 'src'), 1) + + self.assertEqual(gfn.call_args_list, + [mock.call(compose, server, 'src', None)]) + self.assertEqual(gvi.call_args_list, + [mock.call(compose, server, 'src', [])]) + self.assertEqual(gef.call_args_list, + [mock.call(compose, server, 'src', [])]) + self.assertEqual(gic.call_args_list, + [mock.call(compose, server, 'src', ['Client'], 'my.iso', False)]) + self.assertEqual( + rcc.call_args_list, + [mock.call(False, 1, compose, False, 'src', + ['bash', os.path.join(self.topdir, 'work/src/tmp-Server/extraiso-my.iso.sh')], + [self.topdir], + log_file=os.path.join(self.topdir, 'logs/src/extraiso-my.iso.src.log'), + with_jigdo=False)] + + ) + self.assertEqual( + aitm.call_args_list, + [mock.call(compose, server, 'src', + os.path.join(self.topdir, 'compose/Server/source/iso/my.iso'), + False, 1, 1)] + ) + + def test_failable_failed(self, aitm, rcc, gef, gic, gfn, gvi): + compose = helpers.DummyCompose(self.topdir, {}) + server = compose.variants['Server'] + cfg = { + 'include_variants': ['Client'], + 'failable_arches': ['x86_64'], + } + + gfn.return_value = 'my.iso' + gvi.return_value = 'my volume id' + gic.return_value = '/tmp/iso-graft-points' + rcc.side_effect = helpers.mk_boom() + + t = extra_isos.ExtraIsosThread(mock.Mock()) + with mock.patch('time.sleep'): + t.process((compose, cfg, server, 'x86_64'), 1) + + self.assertEqual(aitm.call_args_list, []) + + def test_non_failable_failed(self, aitm, rcc, gef, gic, gfn, gvi): + compose = helpers.DummyCompose(self.topdir, {}) + server = compose.variants['Server'] + cfg = { + 'include_variants': ['Client'], + } + + gfn.return_value = 'my.iso' + gvi.return_value = 'my volume id' + gic.return_value = '/tmp/iso-graft-points' + rcc.side_effect = helpers.mk_boom(RuntimeError) + + t = extra_isos.ExtraIsosThread(mock.Mock()) + with self.assertRaises(RuntimeError): + with mock.patch('time.sleep'): + t.process((compose, cfg, server, 'x86_64'), 1) + + self.assertEqual(aitm.call_args_list, []) + + +@mock.patch('pungi.phases.extra_isos.get_file_from_scm') +@mock.patch('pungi.phases.extra_isos.get_dir_from_scm') +class GetExtraFilesTest(helpers.PungiTestCase): + + def setUp(self): + super(GetExtraFilesTest, self).setUp() + self.compose = helpers.DummyCompose(self.topdir, {}) + self.variant = self.compose.variants['Server'] + self.arch = 'x86_64' + + def test_no_config(self, get_dir, get_file): + extra_isos.get_extra_files(self.compose, self.variant, self.arch, []) + + self.assertEqual(get_dir.call_args_list, []) + self.assertEqual(get_file.call_args_list, []) + + def test_get_file(self, get_dir, get_file): + cfg = { + 'scm': 'git', + 'repo': 'https://pagure.io/pungi.git', + 'file': 'GPL', + 'target': 'legalese', + } + extra_isos.get_extra_files(self.compose, self.variant, self.arch, [cfg]) + + self.assertEqual(get_dir.call_args_list, []) + self.assertEqual(get_file.call_args_list, + [mock.call(cfg, + os.path.join(self.topdir, 'work', + self.arch, self.variant.uid, + 'extra-iso-extra-files/legalese'), + logger=self.compose._logger)]) + + def test_get_dir(self, get_dir, get_file): + cfg = { + 'scm': 'git', + 'repo': 'https://pagure.io/pungi.git', + 'dir': 'docs', + 'target': 'foo', + } + extra_isos.get_extra_files(self.compose, self.variant, self.arch, [cfg]) + + self.assertEqual(get_file.call_args_list, []) + self.assertEqual(get_dir.call_args_list, + [mock.call(cfg, + os.path.join(self.topdir, 'work', + self.arch, self.variant.uid, + 'extra-iso-extra-files/foo'), + logger=self.compose._logger)]) + + +@mock.patch('pungi.wrappers.iso.write_graft_points') +@mock.patch('pungi.wrappers.iso.get_graft_points') +class GetIsoContentsTest(helpers.PungiTestCase): + + def setUp(self): + super(GetIsoContentsTest, self).setUp() + self.compose = helpers.DummyCompose(self.topdir, {}) + self.variant = self.compose.variants['Server'] + + def test_non_bootable_binary(self, ggp, wgp): + gp = { + 'compose/Client/x86_64/os/Packages': {'f/foo.rpm': '/mnt/f/foo.rpm'}, + 'compose/Client/x86_64/os/repodata': {'primary.xml': '/mnt/repodata/primary.xml'}, + 'compose/Server/x86_64/os/Packages': {'b/bar.rpm': '/mnt/b/bar.rpm'}, + 'compose/Server/x86_64/os/repodata': {'repomd.xml': '/mnt/repodata/repomd.xml'}, + 'work/x86_64/Client/extra-files': {'GPL': '/mnt/GPL'}, + 'work/x86_64/Server/extra-files': {'AUTHORS': '/mnt/AUTHORS'}, + 'work/x86_64/Server/extra-iso-extra-files': {'EULA': '/mnt/EULA'}, + } + + ggp.side_effect = lambda x: gp[x[0][len(self.topdir) + 1:]] + gp_file = os.path.join(self.topdir, 'work/x86_64/iso/my.iso-graft-points') + + self.assertEqual( + extra_isos.get_iso_contents(self.compose, self.variant, 'x86_64', + ['Client'], 'my.iso', False), + gp_file + ) + + expected = { + 'Client/GPL': '/mnt/GPL', + 'Client/Packages/f/foo.rpm': '/mnt/f/foo.rpm', + 'Client/repodata/primary.xml': '/mnt/repodata/primary.xml', + 'EULA': '/mnt/EULA', + 'Server/AUTHORS': '/mnt/AUTHORS', + 'Server/Packages/b/bar.rpm': '/mnt/b/bar.rpm', + 'Server/repodata/repomd.xml': '/mnt/repodata/repomd.xml', + } + + self.assertItemsEqual( + ggp.call_args_list, + [mock.call([os.path.join(self.topdir, x)]) for x in gp] + ) + self.assertEqual(len(wgp.call_args_list), 1) + self.assertEqual(wgp.call_args_list[0][0][0], gp_file) + self.assertDictEqual(dict(wgp.call_args_list[0][0][1]), expected) + self.assertEqual(wgp.call_args_list[0][1], {'exclude': ["*/lost+found", "*/boot.iso"]}) + + def test_source(self, ggp, wgp): + gp = { + 'compose/Client/source/tree/Packages': {'f/foo.rpm': '/mnt/f/foo.rpm'}, + 'compose/Client/source/tree/repodata': {'primary.xml': '/mnt/repodata/primary.xml'}, + 'compose/Server/source/tree/Packages': {'b/bar.rpm': '/mnt/b/bar.rpm'}, + 'compose/Server/source/tree/repodata': {'repomd.xml': '/mnt/repodata/repomd.xml'}, + 'work/src/Client/extra-files': {'GPL': '/mnt/GPL'}, + 'work/src/Server/extra-files': {'AUTHORS': '/mnt/AUTHORS'}, + 'work/src/Server/extra-iso-extra-files': {'EULA': '/mnt/EULA'}, + } + + ggp.side_effect = lambda x: gp[x[0][len(self.topdir) + 1:]] + gp_file = os.path.join(self.topdir, 'work/src/iso/my.iso-graft-points') + + self.assertEqual( + extra_isos.get_iso_contents(self.compose, self.variant, 'src', + ['Client'], 'my.iso', False), + gp_file + ) + + expected = { + 'Client/GPL': '/mnt/GPL', + 'Client/Packages/f/foo.rpm': '/mnt/f/foo.rpm', + 'Client/repodata/primary.xml': '/mnt/repodata/primary.xml', + 'EULA': '/mnt/EULA', + 'Server/AUTHORS': '/mnt/AUTHORS', + 'Server/Packages/b/bar.rpm': '/mnt/b/bar.rpm', + 'Server/repodata/repomd.xml': '/mnt/repodata/repomd.xml', + } + + self.assertItemsEqual( + ggp.call_args_list, + [mock.call([os.path.join(self.topdir, x)]) for x in gp] + ) + self.assertEqual(len(wgp.call_args_list), 1) + self.assertEqual(wgp.call_args_list[0][0][0], gp_file) + self.assertDictEqual(dict(wgp.call_args_list[0][0][1]), expected) + self.assertEqual(wgp.call_args_list[0][1], {'exclude': ["*/lost+found", "*/boot.iso"]}) + + def test_bootable(self, ggp, wgp): + self.compose.conf['buildinstall_method'] = 'lorax' + + bi_dir = os.path.join(self.topdir, 'work/x86_64/buildinstall/Server') + iso_dir = os.path.join(self.topdir, 'work/x86_64/iso/my.iso') + helpers.touch(os.path.join(bi_dir, 'isolinux/isolinux.bin')) + helpers.touch(os.path.join(bi_dir, 'images/boot.img')) + + gp = { + 'compose/Client/x86_64/os/Packages': {'f/foo.rpm': '/mnt/f/foo.rpm'}, + 'compose/Client/x86_64/os/repodata': {'primary.xml': '/mnt/repodata/primary.xml'}, + 'compose/Server/x86_64/os/Packages': {'b/bar.rpm': '/mnt/b/bar.rpm'}, + 'compose/Server/x86_64/os/repodata': {'repomd.xml': '/mnt/repodata/repomd.xml'}, + 'work/x86_64/Client/extra-files': {'GPL': '/mnt/GPL'}, + 'work/x86_64/Server/extra-files': {'AUTHORS': '/mnt/AUTHORS'}, + 'work/x86_64/Server/extra-iso-extra-files': {'EULA': '/mnt/EULA'}, + } + bi_gp = { + 'isolinux/isolinux.bin': os.path.join(iso_dir, 'isolinux/isolinux.bin'), + 'images/boot.img': os.path.join(iso_dir, 'images/boot.img'), + } + + ggp.side_effect = lambda x: gp[x[0][len(self.topdir) + 1:]] if len(x) == 1 else bi_gp + gp_file = os.path.join(self.topdir, 'work/x86_64/iso/my.iso-graft-points') + + self.assertEqual( + extra_isos.get_iso_contents(self.compose, self.variant, 'x86_64', + ['Client'], 'my.iso', True), + gp_file + ) + + self.maxDiff = None + + expected = { + 'Client/GPL': '/mnt/GPL', + 'Client/Packages/f/foo.rpm': '/mnt/f/foo.rpm', + 'Client/repodata/primary.xml': '/mnt/repodata/primary.xml', + 'EULA': '/mnt/EULA', + 'Server/AUTHORS': '/mnt/AUTHORS', + 'Server/Packages/b/bar.rpm': '/mnt/b/bar.rpm', + 'Server/repodata/repomd.xml': '/mnt/repodata/repomd.xml', + 'isolinux/isolinux.bin': os.path.join(iso_dir, 'isolinux/isolinux.bin'), + 'images/boot.img': os.path.join(iso_dir, 'images/boot.img'), + } + + self.assertItemsEqual( + ggp.call_args_list, + [mock.call([os.path.join(self.topdir, x)]) for x in gp] + [mock.call([bi_dir, iso_dir])] + ) + self.assertEqual(len(wgp.call_args_list), 1) + self.assertEqual(wgp.call_args_list[0][0][0], gp_file) + self.assertDictEqual(dict(wgp.call_args_list[0][0][1]), expected) + self.assertEqual(wgp.call_args_list[0][1], {'exclude': ["*/lost+found", "*/boot.iso"]}) + + # Check files were copied to temp directory + self.assertTrue(os.path.exists(os.path.join(iso_dir, 'isolinux/isolinux.bin'))) + self.assertTrue(os.path.exists(os.path.join(iso_dir, 'images/boot.img'))) + + +class GetFilenameTest(helpers.PungiTestCase): + def test_use_original_name(self): + compose = helpers.DummyCompose(self.topdir, {}) + + fn = extra_isos.get_filename(compose, compose.variants['Server'], 'x86_64', + 'foo-{variant}-{arch}-{filename}') + + self.assertEqual(fn, 'foo-Server-x86_64-image-name') + + def test_use_default_without_format(self): + compose = helpers.DummyCompose(self.topdir, {}) + + fn = extra_isos.get_filename(compose, compose.variants['Server'], 'x86_64', + None) + + self.assertEqual(fn, 'image-name') + + def test_reports_unknown_placeholder(self): + compose = helpers.DummyCompose(self.topdir, {}) + + with self.assertRaises(RuntimeError) as ctx: + extra_isos.get_filename(compose, compose.variants['Server'], 'x86_64', + 'foo-{boom}') + + self.assertIn('boom', str(ctx.exception)) + + +class GetVolumeIDTest(helpers.PungiTestCase): + def test_use_original_volume_id(self): + compose = helpers.DummyCompose(self.topdir, {}) + + volid = extra_isos.get_volume_id(compose, compose.variants['Server'], + 'x86_64', + 'f-{volid}') + + self.assertEqual(volid, 'f-test-1.0 Server.x86_64') + + def test_falls_back_to_shorter(self): + compose = helpers.DummyCompose(self.topdir, {}) + + volid = extra_isos.get_volume_id(compose, compose.variants['Server'], + 'x86_64', + ['long-foobar-{volid}', 'f-{volid}']) + + self.assertEqual(volid, 'f-test-1.0 Server.x86_64') + + def test_reports_unknown_placeholder(self): + compose = helpers.DummyCompose(self.topdir, {}) + + with self.assertRaises(RuntimeError) as ctx: + extra_isos.get_volume_id(compose, compose.variants['Server'], + 'x86_64', 'f-{boom}') + + self.assertIn('boom', str(ctx.exception)) + + +if __name__ == '__main__': + unittest.main()