# Test class and utilities for functional Linux-based tests # # Copyright (c) 2018 Red Hat, Inc. # # Author: # Cleber Rosa # # This work is licensed under the terms of the GNU GPL, version 2 or # later. See the COPYING file in the top-level directory. import os import shutil from avocado.utils import cloudinit, datadrainer, process, vmimage from avocado_qemu import LinuxSSHMixIn from avocado_qemu import QemuSystemTest if os.path.islink(os.path.dirname(os.path.dirname(__file__))): # The link to the avocado tests dir in the source code directory lnk = os.path.dirname(os.path.dirname(__file__)) #: The QEMU root source directory SOURCE_DIR = os.path.dirname(os.path.dirname(os.readlink(lnk))) else: SOURCE_DIR = BUILD_DIR class LinuxDistro: """Represents a Linux distribution Holds information of known distros. """ #: A collection of known distros and their respective image checksum KNOWN_DISTROS = { 'fedora': { '31': { 'x86_64': {'checksum': ('e3c1b309d9203604922d6e255c2c5d09' '8a309c2d46215d8fc026954f3c5c27a0'), 'pxeboot_url': ('https://archives.fedoraproject.org/' 'pub/archive/fedora/linux/releases/31/' 'Everything/x86_64/os/images/pxeboot/'), 'kernel_params': ('root=UUID=b1438b9b-2cab-4065-a99a-' '08a96687f73c ro no_timer_check ' 'net.ifnames=0 console=tty1 ' 'console=ttyS0,115200n8'), }, 'aarch64': {'checksum': ('1e18d9c0cf734940c4b5d5ec592facae' 'd2af0ad0329383d5639c997fdf16fe49'), 'pxeboot_url': 'https://archives.fedoraproject.org/' 'pub/archive/fedora/linux/releases/31/' 'Everything/aarch64/os/images/pxeboot/', 'kernel_params': ('root=UUID=b6950a44-9f3c-4076-a9c2-' '355e8475b0a7 ro earlyprintk=pl011,0x9000000' ' ignore_loglevel no_timer_check' ' printk.time=1 rd_NO_PLYMOUTH' ' console=ttyAMA0'), }, 'ppc64': {'checksum': ('7c3528b85a3df4b2306e892199a9e1e4' '3f991c506f2cc390dc4efa2026ad2f58')}, 's390x': {'checksum': ('4caaab5a434fd4d1079149a072fdc789' '1e354f834d355069ca982fdcaf5a122d')}, }, '32': { 'aarch64': {'checksum': ('b367755c664a2d7a26955bbfff985855' 'adfa2ca15e908baf15b4b176d68d3967'), 'pxeboot_url': ('http://dl.fedoraproject.org/pub/fedora/linux/' 'releases/32/Server/aarch64/os/images/' 'pxeboot/'), 'kernel_params': ('root=UUID=3df75b65-be8d-4db4-8655-' '14d95c0e90c5 ro no_timer_check net.ifnames=0' ' console=tty1 console=ttyS0,115200n8'), }, }, '33': { 'aarch64': {'checksum': ('e7f75cdfd523fe5ac2ca9eeece68edc1' 'a81f386a17f969c1d1c7c87031008a6b'), 'pxeboot_url': ('http://dl.fedoraproject.org/pub/fedora/linux/' 'releases/33/Server/aarch64/os/images/' 'pxeboot/'), 'kernel_params': ('root=UUID=d20b3ffa-6397-4a63-a734-' '1126a0208f8a ro no_timer_check net.ifnames=0' ' console=tty1 console=ttyS0,115200n8' ' console=tty0'), }, }, } } def __init__(self, name, version, arch): self.name = name self.version = version self.arch = arch try: info = self.KNOWN_DISTROS.get(name).get(version).get(arch) except AttributeError: # Unknown distro info = None self._info = info or {} @property def checksum(self): """Gets the cloud-image file checksum""" return self._info.get('checksum', None) @checksum.setter def checksum(self, value): self._info['checksum'] = value @property def pxeboot_url(self): """Gets the repository url where pxeboot files can be found""" return self._info.get('pxeboot_url', None) @property def default_kernel_params(self): """Gets the default kernel parameters""" return self._info.get('kernel_params', None) class LinuxTest(LinuxSSHMixIn, QemuSystemTest): """Facilitates having a cloud-image Linux based available. For tests that intend to interact with guests, this is a better choice to start with than the more vanilla `QemuSystemTest` class. """ distro = None username = 'root' password = 'password' smp = '2' memory = '1024' def _set_distro(self): distro_name = self.params.get( 'distro', default=self._get_unique_tag_val('distro')) if not distro_name: distro_name = 'fedora' distro_version = self.params.get( 'distro_version', default=self._get_unique_tag_val('distro_version')) if not distro_version: distro_version = '31' self.distro = LinuxDistro(distro_name, distro_version, self.arch) # The distro checksum behaves differently than distro name and # version. First, it does not respect a tag with the same # name, given that it's not expected to be used for filtering # (distro name versions are the natural choice). Second, the # order of precedence is: parameter, attribute and then value # from KNOWN_DISTROS. distro_checksum = self.params.get('distro_checksum', default=None) if distro_checksum: self.distro.checksum = distro_checksum def setUp(self, ssh_pubkey=None, network_device_type='virtio-net'): super().setUp() self.require_netdev('user') self._set_distro() self.vm.add_args('-smp', self.smp) self.vm.add_args('-m', self.memory) # The following network device allows for SSH connections self.vm.add_args('-netdev', 'user,id=vnet,hostfwd=:127.0.0.1:0-:22', '-device', '%s,netdev=vnet' % network_device_type) self.set_up_boot() if ssh_pubkey is None: ssh_pubkey, self.ssh_key = self.set_up_existing_ssh_keys() self.set_up_cloudinit(ssh_pubkey) def set_up_existing_ssh_keys(self): ssh_public_key = os.path.join(SOURCE_DIR, 'tests', 'keys', 'id_rsa.pub') source_private_key = os.path.join(SOURCE_DIR, 'tests', 'keys', 'id_rsa') ssh_dir = os.path.join(self.workdir, '.ssh') os.mkdir(ssh_dir, mode=0o700) ssh_private_key = os.path.join(ssh_dir, os.path.basename(source_private_key)) shutil.copyfile(source_private_key, ssh_private_key) os.chmod(ssh_private_key, 0o600) return (ssh_public_key, ssh_private_key) def download_boot(self): # Set the qemu-img binary. # If none is available, the test will cancel. vmimage.QEMU_IMG = super().get_qemu_img() self.log.info('Downloading/preparing boot image') # Fedora 31 only provides ppc64le images image_arch = self.arch if self.distro.name == 'fedora': if image_arch == 'ppc64': image_arch = 'ppc64le' try: boot = vmimage.get( self.distro.name, arch=image_arch, version=self.distro.version, checksum=self.distro.checksum, algorithm='sha256', cache_dir=self.cache_dirs[0], snapshot_dir=self.workdir) except: self.cancel('Failed to download/prepare boot image') return boot.path def prepare_cloudinit(self, ssh_pubkey=None): self.log.info('Preparing cloudinit image') try: cloudinit_iso = os.path.join(self.workdir, 'cloudinit.iso') pubkey_content = None if ssh_pubkey: with open(ssh_pubkey) as pubkey: pubkey_content = pubkey.read() cloudinit.iso(cloudinit_iso, self.name, username=self.username, password=self.password, # QEMU's hard coded usermode router address phone_home_host='10.0.2.2', phone_home_port=self.phone_server.server_port, authorized_key=pubkey_content) except Exception: self.cancel('Failed to prepare the cloudinit image') return cloudinit_iso def set_up_boot(self): path = self.download_boot() self.vm.add_args('-drive', 'file=%s' % path) def set_up_cloudinit(self, ssh_pubkey=None): self.phone_server = cloudinit.PhoneHomeServer(('0.0.0.0', 0), self.name) cloudinit_iso = self.prepare_cloudinit(ssh_pubkey) self.vm.add_args('-drive', 'file=%s,format=raw' % cloudinit_iso) def launch_and_wait(self, set_up_ssh_connection=True): self.vm.set_console() self.vm.launch() console_drainer = datadrainer.LineLogger(self.vm.console_socket.fileno(), logger=self.log.getChild('console')) console_drainer.start() self.log.info('VM launched, waiting for boot confirmation from guest') while not self.phone_server.instance_phoned_back: self.phone_server.handle_request() if set_up_ssh_connection: self.log.info('Setting up the SSH connection') self.ssh_connect(self.username, self.ssh_key)