#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)# Build and run a bots/image-customize command to prepare a VM for testing Cockpit.## Copyright (C) 2022 Red Hat, Inc.# SPDX-License-Identifier: LGPL-2.1-or-laterimport argparseimport osimport shleximport shutilimport subprocessimport sysfrom collections.abc import Iteratorfrom contextlib import contextmanagerfrom lib import testmapfrom lib.constants import DEFAULT_IMAGEfrom machine.machine_core import machine_virtualBASE_DIR = os.path.realpath(f'{__file__}/../..')TEST_DIR = f'{BASE_DIR}/test'BOTS_DIR = f'{BASE_DIR}/bots'BUILD_VM_MEMORY_MB = 4096@contextmanagerdef create_machine(image: str) -> Iterator[machine_virtual.VirtMachine]:subprocess.check_call([os.path.join(BOTS_DIR, "image-download"), image])network = machine_virtual.VirtNetwork(image=image)machine = machine_virtual.VirtMachine(image=image, networking=network.host(restrict=True),memory_mb=BUILD_VM_MEMORY_MB)try:machine.boot()yield machinefinally:machine.stop()def build_rpms(dist_tars: list[str], machine: machine_virtual.VirtMachine, *, verbose: bool, quick: bool) -> list[str]:"""build RPMs from a tarball in an machineReturn local rpm path list."""machine.upload(dist_tars, "/var/tmp/")main_tar = os.path.join("/var/tmp", os.path.basename(dist_tars[0]))# build srpmmachine.execute(fr"""su builder -c 'rpmbuild --define "_topdir /var/tmp/build" -ts "{main_tar}"'""")# build rpmsmock_opts = ("--verbose" if verbose else "") + (" --nocheck" if quick else "")machine.execute(fr"""su builder -c 'mock --no-clean --disablerepo=* --offline \--resultdir /var/tmp/build {mock_opts} \--rebuild /var/tmp/build/SRPMS/*.src.rpm'""", timeout=1800)# download rpmsvm_rpms = machine.execute("find /var/tmp/build -name '*.rpm' -not -name '*.src.rpm'").strip().split()destdir = os.path.abspath("tmp/rpms")if os.path.exists(destdir):shutil.rmtree(destdir)os.makedirs(destdir)rpms: list[str] = []for rpm in vm_rpms:machine.download(rpm, destdir)rpms.append(os.path.join(destdir, os.path.basename(rpm)))return rpms## Helper functions to build image-customize options for various steps#def build_install_package(dist_tars: list[str], image: str, *, quick: bool) -> list[str]:"""Default rpm/deb/arch package build/install"""# our images have distro cockpit packages pre-installed, remove themargs = ["--run-command"]if 'debian' in image or 'ubuntu' in image:# set up pbuilder hook to make install-testscmd = """set -exdpkg --purge cockpit cockpit-ws cockpit-bridge cockpit-systemmkdir -p /var/tmp/testsecho 'HOOKDIR="/etc/pbuilder-hooks"' >> /etc/pbuilderrcecho 'BINDMOUNTS="/var/tmp/tests"' >> /etc/pbuilderrc"""args.append(cmd)args += ["--upload", os.path.join(TEST_DIR, "pbuilder-hooks") + ":/etc/"]else:# subscription-manager-cockpit needs these, thus --nodepsargs.append(r"""if rpm -q cockpit-ws >/dev/null 2>&1then rpm --erase --nodeps --verbose cockpit cockpit-ws cockpit-bridge cockpit-systemfi""")# first in dist_tars is the main release; upload additional onesfor extra_tar in dist_tars[1:]:args += ["--upload", extra_tar + ":/var/tmp/"]args += ["--build", dist_tars[0]]# install the playgroundargs += ["--upload", os.path.join(BASE_DIR, "dist/playground") + ":/usr/local/share/cockpit/"]# install mock pam module; we don't want to ship it as package, so fish it out of the build rootargs.append("--run-command")if 'debian' in image or 'ubuntu' in image:args.append(r"find /var/tmp/tests -name mock-pam-conv-mod.so -exec cp {} /lib/x86_64-linux-gnu/security \;")elif 'arch' in image:args.append(r"find /var/lib/archbuild -name mock-pam-conv-mod.so -exec cp {} /usr/lib/security/ \;")else:args.append(r"find /var/lib/mock -name mock-pam-conv-mod.so -exec cp {} /usr/lib64/security/ \;")if not quick and ('debian' in image or 'ubuntu' in image):args.append("--run-command")suppress = {'initial-upload-closes-no-bugs', 'newer-standards-version'}# older OSes don't yet know about watch format v5if image in ["debian-trixie"]:suppress.add("missing-debian-watch-file-standard")# HACK: Older lintian does not know about woff-font-in-wrong-dir and# you cannot suppress unknown tags. So instead we suppress the no# unknown tags lintian rule.if image in ["debian-trixie", "ubuntu-2604"]:suppress.add("alien-tag")args.append(fr"""cd /var/tmp/build;runuser -u admin -- \env TMPDIR=/var/tmp lintian --fail-on warning,error \--tag-display-limit 0 --display-info \--suppress-tags {','.join(suppress)} \cockpit*.changes >&2""")return argsdef build_install_ostree(dist_tars: list[str], image: str, *, verbose: bool, quick: bool) -> list[str]:"""Special treatment of build/install on CoreOSOSTree image can't build packages, build them on corresponding OSes andinstall them into the OSTree and the cockpit/ws container."""with create_machine(testmap.get_build_image(image)) as machine:rpms = build_rpms(dist_tars, machine, verbose=verbose, quick=quick)args: list[str] = []for rpm in rpms:args += ["--upload", f"{rpm}:/var/tmp/"]args += ["--upload", os.path.join(BASE_DIR, "containers") + ":/var/tmp/","--script", os.path.join(TEST_DIR, "ostree.install"),"--upload", os.path.join(BASE_DIR, "dist/playground") + ":/usr/local/share/cockpit/"]return argsdef build_install_bootc(dist_tars: list[str], image: str, *, verbose: bool, quick: bool) -> list[str]:"""Special treatment of build/install on Bootcbootc image can't build packages, build them on corresponding OSes, build a new bootcimage with the rpms, and deploy it."""with create_machine(testmap.get_build_image(image)) as machine:rpms = build_rpms(dist_tars, machine, verbose=verbose, quick=quick)args = ["--run-command", "mkdir -p /var/tmp/rpms"]for rpm in rpms:if not any(r in rpm for r in [".src", "debug", "packagekit"]):args += ["--upload", f"{rpm}:/var/tmp/rpms/"]args += ["--upload", os.path.join(BASE_DIR, "dist/playground") + ":/var/tmp/","--script", os.path.join(TEST_DIR, "bootc.install"),]return argsdef build_install_container(dist_tars: list[str], image: str, *, verbose: bool, quick: bool) -> list[str]:"""Build VM with cockpit/ws containerThis can test an image with beibooting from a cockpit/ws container, withoutany installed cockpit packages."""builder = testmap.get_build_image_for_ws_container_inside_of(image)if builder is None:raise ValueError(f"Don't know how to build a container for {image}")with create_machine(builder) as machine:rpms = build_rpms(dist_tars, machine, verbose=verbose, quick=quick)args: list[str] = []for rpm in rpms:args += ["--upload", f"{rpm}:/var/tmp/"]args += [# copy the playground where ws-container.install expects it"--run-command", "mkdir -p /var/tmp/install/usr/share/cockpit/","--upload", os.path.join(BASE_DIR, "dist/playground") + ":/var/tmp/install/usr/share/cockpit/",# remove preinstalled rpms"--run-command", "dnf -C remove -y cockpit-bridge cockpit-ws","--upload", os.path.join(BASE_DIR, "containers") + ":/var/tmp/","--script", os.path.join(TEST_DIR, "ws-container.install"),]return argsdef validate_packages() -> list[str]:"""Post-install package checks"""# check for files that are shipped by more than one RPMreturn ["--run-command","""set -eufail=for f in $(find $(rpm -ql $(rpm -qa '*cockpit*') | sort | uniq -d) -maxdepth 0 -type f); do# -debugsource overlap is legit[ "${f#/usr/src/debug}" = "$f" ] || continueecho "ERROR: $f is shipped by multiple packages: $(rpm -qf $f)" >&2fail=1done[ -z "${fail}" ] || exit 1"""]def main() -> int:parser = argparse.ArgumentParser(description='Prepare testing environment, download images and build and install cockpit',formatter_class=argparse.ArgumentDefaultsHelpFormatter)parser.add_argument('-e', '--env', metavar='NAME=VAL', action='append',help="Add a line to /etc/environment")parser.add_argument('-d', '--debug', action='append_const', dest='env', const='COCKPIT_DEBUG=all',help="Equivalent to --env=COCKPIT_DEBUG=all")parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose progress details')parser.add_argument('-q', '--quick', action='store_true', help='Skip unit tests to build faster')parser.add_argument('-o', '--overlay', action='store_true',help='Install into existing test/image/ overlay instead of from pristine base image')parser.add_argument('--network', action='store_true',help='Enable internet access (only for local experiments, not for production!)')parser.add_argument('--container', action='store_true', help='Install cockpit/ws container instead of rpms')parser.add_argument('image', nargs='?', default=DEFAULT_IMAGE, help='The image to use')args = parser.parse_args()customize = [os.path.join(BOTS_DIR, "image-customize")]if not args.network:customize.append("--no-network")if not args.overlay:customize.append("--fresh")if args.verbose:customize.append("--verbose")if args.quick:customize.append("--quick")# esbuild likes to use a lot of memory.customize += ["--memory-mb", str(BUILD_VM_MEMORY_MB)]dist_tars = subprocess.check_output([f'{BASE_DIR}/tools/make-dist'], text=True).strip().splitlines()# Don't rebuild bundle if built with NODE_ENV=developmentif os.environ.get('NODE_ENV') == 'development':customize += ["--build-options", '--define "rebuild_bundle 0"']if args.image == "fedora-coreos":customize += build_install_ostree(dist_tars, args.image, verbose=args.verbose, quick=args.quick)elif args.image.endswith("-bootc"):customize += build_install_bootc(dist_tars, args.image, verbose=args.verbose, quick=args.quick)elif args.container:customize += build_install_container(dist_tars, args.image, verbose=args.verbose, quick=args.quick)else:customize += build_install_package(dist_tars, args.image, quick=args.quick)if not args.quick:customize += validate_packages()# post build/install test preparationcustomize += ["--script", os.path.join(TEST_DIR, "vm.install")]if args.env:customize += ['--run-command', r'printf "%s\n" >>/etc/environment ' + shlex.join(args.env)]customize.append(args.image)# show final command for easy copy&paste reproduction/debuggingif args.verbose:print(' '.join([shlex.quote(arg) for arg in customize]))return subprocess.call(customize)if __name__ == "__main__":sys.exit(main())