From patchwork Wed Apr 6 15:31:49 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?q?Juraj_Linke=C5=A1?= X-Patchwork-Id: 109333 X-Patchwork-Delegate: thomas@monjalon.net Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 3BE56A0509; Wed, 6 Apr 2022 17:32:43 +0200 (CEST) Received: from [217.70.189.124] (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 704994292F; Wed, 6 Apr 2022 17:32:03 +0200 (CEST) Received: from lb.pantheon.sk (lb.pantheon.sk [46.229.239.20]) by mails.dpdk.org (Postfix) with ESMTP id 3235A428E9 for ; Wed, 6 Apr 2022 17:32:00 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by lb.pantheon.sk (Postfix) with ESMTP id 7DB06184FE9; Wed, 6 Apr 2022 17:31:59 +0200 (CEST) X-Virus-Scanned: amavisd-new at siecit.sk Received: from lb.pantheon.sk ([127.0.0.1]) by localhost (lb.pantheon.sk [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id 4IqTEZmPDOev; Wed, 6 Apr 2022 17:31:57 +0200 (CEST) Received: from entguard.lab.pantheon.local (unknown [46.229.239.141]) by lb.pantheon.sk (Postfix) with ESMTP id AA07A184FF8; Wed, 6 Apr 2022 17:31:55 +0200 (CEST) From: =?utf-8?q?Juraj_Linke=C5=A1?= To: thomas@monjalon.net, david.marchand@redhat.com, Honnappa.Nagarahalli@arm.com, ohilyard@iol.unh.edu, lijuan.tu@intel.com Cc: dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= Subject: [RFC PATCH v1 6/8] dts: merge DTS vm_images/create_vm_image.py to DPDK Date: Wed, 6 Apr 2022 15:31:49 +0000 Message-Id: <20220406153151.2917346-7-juraj.linkes@pantheon.tech> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220406153151.2917346-1-juraj.linkes@pantheon.tech> References: <20220406153151.2917346-1-juraj.linkes@pantheon.tech> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org --- dts/vm_images/create_vm_image.py | 474 +++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100755 dts/vm_images/create_vm_image.py diff --git a/dts/vm_images/create_vm_image.py b/dts/vm_images/create_vm_image.py new file mode 100755 index 0000000000..8a010c99ac --- /dev/null +++ b/dts/vm_images/create_vm_image.py @@ -0,0 +1,474 @@ +#!/usr/bin/python3 + +import argparse +import enum +import os +import subprocess +from sys import stderr +from typing import List, Optional, Set, Tuple +import xml.etree.ElementTree as ET +import platform + +DOCKER_IMAGE_NAME = "dts_vm_builder" + + +# From https://libguestfs.org/guestfs.3.html#guestfs_inspect_get_distro +class OsFamily(enum.Enum): + ALPINE = "alpinelinux" + ALT = "altlinux" + ARCH = "archlinux" + BUILDROOT_DERIVED = "buildroot" + CENTOS = "centos" + CIRROS = "cirros" + COREOS = "coreos" + DEBIAN = "debian" + FEDORA = "fedora" + FREEBSD = "freebsd" + FREEDOS = "freedos" + FRUNGALWARE = "frugalware" + GENTOO = "gentoo" + KALI = "kalilinux" + KYLIN = "kylin" + MINT = "linuxmint" + MAGEIA = "mageia" + MANDRIVA = "mandriva" + MEEGO = "meego" + MSDOS = "msdos" + NEOKYLIN = "neokylin" + NETBSD = "netbsd" + OPENBSD = "openbsd" + OPENMANDRIVA = "openmandriva" + OPENSUSE = "opensuse" + ORACLE = "oraclelinux" + PARDUS = "pardus" + PLD = "pldlinux" + RHEL_BASED = "redhat-based" + RHEL = "rhel" + ROCKY = "rocky" + SCIENTIFIC_LINUX = "scientificlinux" + SLACKWARE = "slackware" + SLES = "sles" + SUSE_BASED = "suse-based" + TTY_LINUX = "ttylinux" + UBUNTU = "ubuntu" + VOID = "voidlinux" + WINDOWS = "windows" + + UNKNOWN = "unknown" + + def __str__(self): + return self.value + + +# The Os Families that are supported +SUPPORTED_OS_FAMILIES = { + OsFamily.CENTOS, + OsFamily.DEBIAN, + OsFamily.FEDORA, + OsFamily.RHEL_BASED, + OsFamily.RHEL, + OsFamily.UBUNTU, +} + + +# From https://libguestfs.org/guestfs.3.html#guestfs_file_architecture +class Arch(enum.Enum): + aarch64 = "aarch64" + i386 = "i386" + ia64 = "ia64" + ppc = "ppc" + ppc64 = "ppc64" + ppc64le = "ppc64le" + riscv32 = "riscv32" + riscv64 = "riscv64" + riscv128 = "riscv128" + s390 = "s390" + s390x = "s390x" + sparc = "sparc" + sparc64 = "sparc64" + x86_64 = "x86_64" + + def __str__(self): + return self.value + + +# The supported architectures +SUPPORTED_ARCHITECTURES = {Arch.x86_64, Arch.aarch64, Arch.ppc64} + + +def validate_filepath(parser: argparse.ArgumentParser, filepath: str) -> str: + if not os.path.isabs(filepath): + filepath = os.path.abspath(filepath) + + if os.path.exists(filepath): + return filepath + else: + parser.error(f"Path {filepath} not found") + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + + # Base image file + parser.add_argument("base_image", type=lambda f: validate_filepath(parser, f)) + + # Where to write the vm image to + parser.add_argument("output_path") + + # What to set the root password to + parser.add_argument( + "--root_password", help="The new root password for the vm", default="dts" + ) + + # Whether to run virt-customize in debug mode + parser.add_argument("--debug", action="store_true", default=False) + + return parser.parse_args() + + +def run_subprocess( + os_family_tags: Set[OsFamily], + base_image_path: str, + output_path: str, + root_password: str, + debug_mode: bool, + arch: Arch, +): + copy_base_image_to_output_path(base_image_path, output_path) + + print("Building under emulation") + + # Check if the docker container already exists + docker_process = subprocess.run( + f"docker image ls {DOCKER_IMAGE_NAME}", capture_output=True, shell=True + ) + + if docker_process.returncode != 0: + error("Unable to check for presence of docker image") + + if not len(docker_process.stdout.splitlines()) >= 2: # image does not exist + subprocess.run(f"./make_build_container.sh") + + docker_command = [ + "docker", + "run", + # The container needs to access QEMU/KVM + "--privileged", + "-d", + "--platform", + ] + + if arch == Arch.x86_64: + docker_command += ("linux/amd64",) + elif arch == Arch.ppc64le: + docker_command += ("linux/ppc64le",) + elif arch == Arch.aarch64: + docker_command += ("linux/arm64",) + else: + error(f"Please add {arch} to the if chain selecting the docker platform") + + docker_command += ("-v $(pwd):/vm_folder",) + + if debug_mode: + docker_command += ( + "-e", + "LIBGUESTFS_DEBUG=1", + "-e", + "LIBGUESTFS_TRACE=1", + ) + + # Run cat so it doesn't terminate until we stop it + docker_command += f"-it {DOCKER_IMAGE_NAME}:{arch}", "cat" + + # if debug_mode: + print("Running:") + print(" ".join(docker_command)) + print("\n\n") + + docker_process = subprocess.run( + " ".join(docker_command), shell=True, capture_output=True + ) + + if docker_process.returncode != 0: + print(docker_process.stderr) + print(docker_process.stdout) + error("Unable to run docker container, try --debug") + + container_id = docker_process.stdout.strip().decode() + + if debug_mode: + print(f"Docker container is {container_id}") + + virt_customize_command = get_virt_customize_command( + os_family_tags, output_path, root_password + ) + + vm_build_command = ["docker", "exec", "-w", "/vm_folder"] + + if debug_mode: + vm_build_command += ( + "-e", + "LIBGUESTFS_DEBUG=1", + "-e", + "LIBGUESTFS_TRACE=1", + ) + + vm_build_command += ( + "-it", + container_id, + ) + + vm_build_command += (virt_customize_command,) + + # if debug_mode: + print(" ".join(vm_build_command)) + + vm_build_process = subprocess.run(" ".join(vm_build_command), shell=True) + + if vm_build_process.returncode == 0: + # Shut down the build container + subprocess.run(f"docker kill {container_id}", shell=True) + + print(vm_build_process.returncode) + + +def run_command_in_docker_container( + container_id: str, command: str, debug_mode: bool, **kwargs +) -> subprocess.CompletedProcess: + docker_command = "docker exec " + + if debug_mode: + docker_command += f"-e LIBGUESTFS_DEBUG=1 -e LIBGUESTFS_TRACE=1" + + docker_command += f"-w /vm_folder -t {container_id} {command}" + return subprocess.run(docker_command, **kwargs) + + +def copy_base_image_to_output_path(base_image_path: str, output_path: str): + real_base_image_path: str = os.path.realpath(base_image_path) + real_output_path: str = os.path.realpath(output_path) + + if ( + real_base_image_path != real_output_path + ): # do not copy if they are the same path + subprocess.run( + ["cp", real_base_image_path, real_output_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +def get_virt_customize_command( + os_family_tags: Set[OsFamily], output_path: str, root_password: str +) -> str: + commands = [ + f"virt-customize -a {output_path} --root-password password:{root_password} --update" + ] + + commands = commands + get_enable_additional_repos_commands(os_family_tags) + + packages = get_packages_for_os_family(os_family_tags) + packagelist = ",".join(packages) + commands += (f"--install {packagelist}",) + commands += (f"--run-command {get_install_meson_command(os_family_tags)}",) + commands += (f"--run-command {get_setup_hugepages_command(os_family_tags)}",) + commands += (f"--run-command {get_hugepage_mount_command(os_family_tags)}",) + commands = commands + get_security_enforcement_disable_command( + os_family_tags, output_path + ) + return " ".join(commands) + + +def get_enable_additional_repos_commands(os_family_tags: Set[OsFamily]): + if OsFamily.RHEL in os_family_tags and OsFamily.FEDORA not in os_family_tags: + packages = [ + "yum-utils", + "epel-release", + ] + + packagelist = ",".join(packages) + + return [ + f"--install {packagelist}", + f"--run-command 'yum-config-manager --enable powertools'", + ] + elif OsFamily.DEBIAN in os_family_tags: + return [] + + +def get_packages_for_os_family(os_family_tags: Set[OsFamily]) -> List[str]: + if OsFamily.DEBIAN in os_family_tags: + return [ + "make", + "gcc", + "g++", + "libc-dev", + "libc6-dev", + "ninja-build", + "pkg-config", + "libnuma-dev", + "python3-pyelftools", + "abigail-tools", + "git", + "librdmacm-dev", + "librdmacm1", + "rdma-core", + "libelf-dev", + "libmnl-dev", + "libpcap-dev", + "libcrypto++-dev", + "libjansson-dev", + "libatomic1", + "python3-pip", + "python3-setuptools", + "python3-wheel", + "iperf", + "chrony", + ] + elif OsFamily.RHEL in os_family_tags: + return [ + "make", + "gcc", + "pkg-config", + "ninja-build", + "numactl-libs", + "python3-pyelftools", + "libabigail-devel", + "git", + "librdmacm", + "librdmacm-utils", + "rdma-core", + "elfutils-libelf-devel", + "libmnl-devel", + "libpcap-devel", + "cryptopp-devel", + "jansson-devel", + "libatomic", + "python3-pip", + "python3-setuptools", + "python3-wheel", + ] + else: + error(f"Unable to get packages for {os_family_tags} OS family.") + + +def get_install_meson_command(os_family_tags: Set[OsFamily]) -> str: + if OsFamily.DEBIAN in os_family_tags or OsFamily.RHEL in os_family_tags: + # the "--trusted-host" flags are included because the date on the system will be Jan 1, 1970 due to the way + # guestfs-tools starts the vm. This breaks pip's ssl, so making these hosts trusted fixes that. + return '"python3 -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org meson"' + else: + error(f"Unknown command to install meson for {os_family_tags}") + + +def get_setup_hugepages_command(os_family_tags: Set[OsFamily]) -> str: + if OsFamily.DEBIAN in os_family_tags or OsFamily.RHEL in os_family_tags: + return ( + '"sed -i -r \'s/GRUB_CMDLINE_LINUX_DEFAULT=\\"([^\\"]+)\\"/' + 'GRUB_CMDLINE_LINUX_DEFAULT=\\"\\1 default_hugepagesz=2M hugepagesz=2M' + ' hugepages=1375 hugepagesz=1G hugepages=8\\"/\' /etc/default/grub"' + ) + else: + error(f"Unknown command to setup hugepages for {os_family_tags}") + + +def get_hugepage_mount_command(os_family_tags: Set[OsFamily]) -> str: + if OsFamily.DEBIAN in os_family_tags or OsFamily.RHEL in os_family_tags: + return '"mkdir -p /dev/huge && mount nodev -t hugetlbfs -o rw,pagesize=2M /dev/huge/ && umount /dev/huge"' + else: + error(f"Unknown hugepage mount command for {os_family_tags}") + + +def get_security_enforcement_disable_command( + os_family_tags: Set[OsFamily], output_path: str +) -> List[str]: + if OsFamily.RHEL in os_family_tags: + return [f"--run-command 'echo \"SELINUX=disabled\" > /etc/selinux/config'"] + else: + return [] + + +def get_os_family_tags(distribution: OsFamily) -> Set[OsFamily]: + tags: Set[OsFamily] = {distribution} + + # This is not an if-elif-else chain to reduce duplicate code. This way, + # for example, a specialized ubuntu distribution may first be tagged + # ubuntu, then all the ubuntu tags will be applied to it. The most + # specific distros should be placed first. + + if OsFamily.UBUNTU in tags: + tags.add(OsFamily.DEBIAN) + + if OsFamily.FEDORA in tags: + tags.add(OsFamily.CENTOS) + + if OsFamily.CENTOS in tags: + tags.add(OsFamily.RHEL) + + if OsFamily.RHEL in tags: + tags.add(OsFamily.RHEL) + + return tags + + +def check_being_run_as_root(): + proc = subprocess.run(["whoami"], capture_output=True) + if "root".encode() not in proc.stdout: + error("This program must be run as root.") + + +def get_image_info(base_image_path: str) -> (OsFamily, Arch): + command = [ + "virt-inspector", + # Otherwise it will show everything installed via the package manager + "--no-applications", + # We don't need to icon for the distro + "--no-icon", + "-a", + base_image_path, + ] + + print(" ".join(command)) + + proc = subprocess.run(command, capture_output=True) + if proc.returncode != 0: + print(proc.stdout) + print(proc.stderr) + error("Unable to inspect base image") + + tree = ET.fromstring(proc.stdout) + distro = OsFamily(tree.findtext("operatingsystem/distro")) + arch = Arch(tree.findtext("operatingsystem/arch")) + + return distro, arch + + +def main(): + args = parse_arguments() + check_being_run_as_root() + distro, arch = get_image_info(args.base_image) + + if distro not in SUPPORTED_OS_FAMILIES: + error(f"Unsupported distro {distro}") + + if arch not in SUPPORTED_ARCHITECTURES: + error(f"Unsupported architecture {arch}") + + os_family_tags = get_os_family_tags(distro) + run_subprocess( + os_family_tags, + args.base_image, + args.output_path, + args.root_password, + args.debug, + arch, + ) + + +def error(message: str): + print(message, file=stderr) + exit(1) + + +if __name__ == "__main__": + main()