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