1# Test class and utilities for functional Linux-based tests 2# 3# Copyright (c) 2018 Red Hat, Inc. 4# 5# Author: 6# Cleber Rosa <crosa@redhat.com> 7# 8# This work is licensed under the terms of the GNU GPL, version 2 or 9# later. See the COPYING file in the top-level directory. 10 11import os 12import shutil 13 14from avocado.utils import cloudinit, datadrainer, process, vmimage 15 16from . import LinuxSSHMixIn 17from . import QemuSystemTest 18 19if os.path.islink(os.path.dirname(os.path.dirname(__file__))): 20 # The link to the avocado tests dir in the source code directory 21 lnk = os.path.dirname(os.path.dirname(__file__)) 22 #: The QEMU root source directory 23 SOURCE_DIR = os.path.dirname(os.path.dirname(os.readlink(lnk))) 24else: 25 SOURCE_DIR = BUILD_DIR 26 27class LinuxDistro: 28 """Represents a Linux distribution 29 30 Holds information of known distros. 31 """ 32 #: A collection of known distros and their respective image checksum 33 KNOWN_DISTROS = { 34 'fedora': { 35 '31': { 36 'x86_64': 37 {'checksum': ('e3c1b309d9203604922d6e255c2c5d09' 38 '8a309c2d46215d8fc026954f3c5c27a0'), 39 'pxeboot_url': ('https://archives.fedoraproject.org/' 40 'pub/archive/fedora/linux/releases/31/' 41 'Everything/x86_64/os/images/pxeboot/'), 42 'kernel_params': ('root=UUID=b1438b9b-2cab-4065-a99a-' 43 '08a96687f73c ro no_timer_check ' 44 'net.ifnames=0 console=tty1 ' 45 'console=ttyS0,115200n8'), 46 }, 47 'aarch64': 48 {'checksum': ('1e18d9c0cf734940c4b5d5ec592facae' 49 'd2af0ad0329383d5639c997fdf16fe49'), 50 'pxeboot_url': 'https://archives.fedoraproject.org/' 51 'pub/archive/fedora/linux/releases/31/' 52 'Everything/aarch64/os/images/pxeboot/', 53 'kernel_params': ('root=UUID=b6950a44-9f3c-4076-a9c2-' 54 '355e8475b0a7 ro earlyprintk=pl011,0x9000000' 55 ' ignore_loglevel no_timer_check' 56 ' printk.time=1 rd_NO_PLYMOUTH' 57 ' console=ttyAMA0'), 58 }, 59 'ppc64': 60 {'checksum': ('7c3528b85a3df4b2306e892199a9e1e4' 61 '3f991c506f2cc390dc4efa2026ad2f58')}, 62 's390x': 63 {'checksum': ('4caaab5a434fd4d1079149a072fdc789' 64 '1e354f834d355069ca982fdcaf5a122d')}, 65 }, 66 '32': { 67 'aarch64': 68 {'checksum': ('b367755c664a2d7a26955bbfff985855' 69 'adfa2ca15e908baf15b4b176d68d3967'), 70 'pxeboot_url': ('http://dl.fedoraproject.org/pub/fedora/linux/' 71 'releases/32/Server/aarch64/os/images/' 72 'pxeboot/'), 73 'kernel_params': ('root=UUID=3df75b65-be8d-4db4-8655-' 74 '14d95c0e90c5 ro no_timer_check net.ifnames=0' 75 ' console=tty1 console=ttyS0,115200n8'), 76 }, 77 }, 78 '33': { 79 'aarch64': 80 {'checksum': ('e7f75cdfd523fe5ac2ca9eeece68edc1' 81 'a81f386a17f969c1d1c7c87031008a6b'), 82 'pxeboot_url': ('http://dl.fedoraproject.org/pub/fedora/linux/' 83 'releases/33/Server/aarch64/os/images/' 84 'pxeboot/'), 85 'kernel_params': ('root=UUID=d20b3ffa-6397-4a63-a734-' 86 '1126a0208f8a ro no_timer_check net.ifnames=0' 87 ' console=tty1 console=ttyS0,115200n8' 88 ' console=tty0'), 89 }, 90 }, 91 } 92 } 93 94 def __init__(self, name, version, arch): 95 self.name = name 96 self.version = version 97 self.arch = arch 98 try: 99 info = self.KNOWN_DISTROS.get(name).get(version).get(arch) 100 except AttributeError: 101 # Unknown distro 102 info = None 103 self._info = info or {} 104 105 @property 106 def checksum(self): 107 """Gets the cloud-image file checksum""" 108 return self._info.get('checksum', None) 109 110 @checksum.setter 111 def checksum(self, value): 112 self._info['checksum'] = value 113 114 @property 115 def pxeboot_url(self): 116 """Gets the repository url where pxeboot files can be found""" 117 return self._info.get('pxeboot_url', None) 118 119 @property 120 def default_kernel_params(self): 121 """Gets the default kernel parameters""" 122 return self._info.get('kernel_params', None) 123 124 125class LinuxTest(LinuxSSHMixIn, QemuSystemTest): 126 """Facilitates having a cloud-image Linux based available. 127 128 For tests that intend to interact with guests, this is a better choice 129 to start with than the more vanilla `QemuSystemTest` class. 130 """ 131 132 distro = None 133 username = 'root' 134 password = 'password' 135 smp = '2' 136 memory = '1024' 137 138 def _set_distro(self): 139 distro_name = self.params.get( 140 'distro', 141 default=self._get_unique_tag_val('distro')) 142 if not distro_name: 143 distro_name = 'fedora' 144 145 distro_version = self.params.get( 146 'distro_version', 147 default=self._get_unique_tag_val('distro_version')) 148 if not distro_version: 149 distro_version = '31' 150 151 self.distro = LinuxDistro(distro_name, distro_version, self.arch) 152 153 # The distro checksum behaves differently than distro name and 154 # version. First, it does not respect a tag with the same 155 # name, given that it's not expected to be used for filtering 156 # (distro name versions are the natural choice). Second, the 157 # order of precedence is: parameter, attribute and then value 158 # from KNOWN_DISTROS. 159 distro_checksum = self.params.get('distro_checksum', 160 default=None) 161 if distro_checksum: 162 self.distro.checksum = distro_checksum 163 164 def setUp(self, ssh_pubkey=None, network_device_type='virtio-net'): 165 super().setUp() 166 self.require_netdev('user') 167 self._set_distro() 168 self.vm.add_args('-smp', self.smp) 169 self.vm.add_args('-m', self.memory) 170 # The following network device allows for SSH connections 171 self.vm.add_args('-netdev', 'user,id=vnet,hostfwd=:127.0.0.1:0-:22', 172 '-device', '%s,netdev=vnet' % network_device_type) 173 self.set_up_boot() 174 if ssh_pubkey is None: 175 ssh_pubkey, self.ssh_key = self.set_up_existing_ssh_keys() 176 self.set_up_cloudinit(ssh_pubkey) 177 178 def set_up_existing_ssh_keys(self): 179 ssh_public_key = os.path.join(SOURCE_DIR, 'tests', 'keys', 'id_rsa.pub') 180 source_private_key = os.path.join(SOURCE_DIR, 'tests', 'keys', 'id_rsa') 181 ssh_dir = os.path.join(self.workdir, '.ssh') 182 os.mkdir(ssh_dir, mode=0o700) 183 ssh_private_key = os.path.join(ssh_dir, 184 os.path.basename(source_private_key)) 185 shutil.copyfile(source_private_key, ssh_private_key) 186 os.chmod(ssh_private_key, 0o600) 187 return (ssh_public_key, ssh_private_key) 188 189 def download_boot(self): 190 # Set the qemu-img binary. 191 # If none is available, the test will cancel. 192 vmimage.QEMU_IMG = super().get_qemu_img() 193 194 self.log.info('Downloading/preparing boot image') 195 # Fedora 31 only provides ppc64le images 196 image_arch = self.arch 197 if self.distro.name == 'fedora': 198 if image_arch == 'ppc64': 199 image_arch = 'ppc64le' 200 201 try: 202 boot = vmimage.get( 203 self.distro.name, arch=image_arch, version=self.distro.version, 204 checksum=self.distro.checksum, 205 algorithm='sha256', 206 cache_dir=self.cache_dirs[0], 207 snapshot_dir=self.workdir) 208 except: 209 self.cancel('Failed to download/prepare boot image') 210 return boot.path 211 212 def prepare_cloudinit(self, ssh_pubkey=None): 213 self.log.info('Preparing cloudinit image') 214 try: 215 cloudinit_iso = os.path.join(self.workdir, 'cloudinit.iso') 216 pubkey_content = None 217 if ssh_pubkey: 218 with open(ssh_pubkey) as pubkey: 219 pubkey_content = pubkey.read() 220 cloudinit.iso(cloudinit_iso, self.name, 221 username=self.username, 222 password=self.password, 223 # QEMU's hard coded usermode router address 224 phone_home_host='10.0.2.2', 225 phone_home_port=self.phone_server.server_port, 226 authorized_key=pubkey_content) 227 except Exception: 228 self.cancel('Failed to prepare the cloudinit image') 229 return cloudinit_iso 230 231 def set_up_boot(self): 232 path = self.download_boot() 233 self.vm.add_args('-drive', 'file=%s' % path) 234 235 def set_up_cloudinit(self, ssh_pubkey=None): 236 self.phone_server = cloudinit.PhoneHomeServer(('0.0.0.0', 0), 237 self.name) 238 cloudinit_iso = self.prepare_cloudinit(ssh_pubkey) 239 self.vm.add_args('-drive', 'file=%s,format=raw' % cloudinit_iso) 240 241 def launch_and_wait(self, set_up_ssh_connection=True): 242 self.vm.set_console() 243 self.vm.launch() 244 console_drainer = datadrainer.LineLogger(self.vm.console_socket.fileno(), 245 logger=self.log.getChild('console')) 246 console_drainer.start() 247 self.log.info('VM launched, waiting for boot confirmation from guest') 248 while not self.phone_server.instance_phoned_back: 249 self.phone_server.handle_request() 250 251 if set_up_ssh_connection: 252 self.log.info('Setting up the SSH connection') 253 self.ssh_connect(self.username, self.ssh_key) 254