1#!/usr/bin/env python3
2#
3# Copyright (c) 2014 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8# DESCRIPTION
9# This script is used to test public autobuilder images on remote hardware.
10# The script is called from a machine that is able download the images from the remote images repository and to connect to the test hardware.
11#
12# test-remote-image --image-type core-image-sato --repo-link http://192.168.10.2/images --required-packages rpm psplash
13#
14# Translation: Build the 'rpm' and 'pslash' packages and test a remote core-image-sato image using the http://192.168.10.2/images repository.
15#
16# You can also use the '-h' option to see some help information.
17
18import os
19import sys
20import argparse
21import logging
22import shutil
23from abc import ABCMeta, abstractmethod
24
25# Add path to scripts/lib in sys.path;
26scripts_path = os.path.abspath(os.path.dirname(os.path.abspath(sys.argv[0])))
27lib_path = scripts_path + '/lib'
28sys.path = sys.path + [lib_path]
29
30import scriptpath
31import argparse_oe
32
33# Add meta/lib to sys.path
34scriptpath.add_oe_lib_path()
35
36import oeqa.utils.ftools as ftools
37from oeqa.utils.commands import runCmd, bitbake, get_bb_var
38
39# Add all lib paths relative to BBPATH to sys.path; this is used to find and import the target controllers.
40for path in get_bb_var('BBPATH').split(":"):
41    sys.path.insert(0, os.path.abspath(os.path.join(path, 'lib')))
42
43# In order to import modules that contain target controllers, we need the bitbake libraries in sys.path .
44bitbakepath = scriptpath.add_bitbake_lib_path()
45if not bitbakepath:
46    sys.stderr.write("Unable to find bitbake by searching parent directory of this script or PATH\n")
47    sys.exit(1)
48
49# create a logger
50def logger_create():
51    log = logging.getLogger('hwauto')
52    log.setLevel(logging.DEBUG)
53
54    fh = logging.FileHandler(filename='hwauto.log', mode='w')
55    fh.setLevel(logging.DEBUG)
56
57    ch = logging.StreamHandler(sys.stdout)
58    ch.setLevel(logging.INFO)
59
60    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
61    fh.setFormatter(formatter)
62    ch.setFormatter(formatter)
63
64    log.addHandler(fh)
65    log.addHandler(ch)
66
67    return log
68
69# instantiate the logger
70log = logger_create()
71
72
73# Define and return the arguments parser for the script
74def get_args_parser():
75    description = "This script is used to run automated runtime tests using remotely published image files. You should prepare the build environment just like building local images and running the tests."
76    parser = argparse_oe.ArgumentParser(description=description)
77    parser.add_argument('--image-types', required=True, action="store", nargs='*', dest="image_types", default=None, help='The image types to test(ex: core-image-minimal).')
78    parser.add_argument('--repo-link', required=True, action="store", type=str, dest="repo_link", default=None, help='The link to the remote images repository.')
79    parser.add_argument('--required-packages', required=False, action="store", nargs='*', dest="required_packages", default=None, help='Required packages for the tests. They will be built before the testing begins.')
80    parser.add_argument('--targetprofile', required=False, action="store", nargs=1, dest="targetprofile", default='AutoTargetProfile', help='The target profile to be used.')
81    parser.add_argument('--repoprofile', required=False, action="store", nargs=1, dest="repoprofile", default='PublicAB', help='The repo profile to be used.')
82    parser.add_argument('--skip-download', required=False, action="store_true", dest="skip_download", default=False, help='Skip downloading the images completely. This needs the correct files to be present in the directory specified by the target profile.')
83    return parser
84
85class BaseTargetProfile(object, metaclass=ABCMeta):
86    """
87    This class defines the meta profile for a specific target (MACHINE type + image type).
88    """
89
90    def __init__(self, image_type):
91        self.image_type = image_type
92
93        self.kernel_file = None
94        self.rootfs_file = None
95        self.manifest_file = None
96        self.extra_download_files = []          # Extra files (full name) to be downloaded. They should be situated in repo_link
97
98    # This method is used as the standard interface with the target profile classes.
99    # It returns a dictionary containing a list of files and their meaning/description.
100    def get_files_dict(self):
101        files_dict = {}
102
103        if self.kernel_file:
104            files_dict['kernel_file'] = self.kernel_file
105        else:
106            log.error('The target profile did not set a kernel file.')
107            sys.exit(1)
108
109        if self.rootfs_file:
110            files_dict['rootfs_file'] = self.rootfs_file
111        else:
112            log.error('The target profile did not set a rootfs file.')
113            sys.exit(1)
114
115        if self.manifest_file:
116            files_dict['manifest_file'] = self.manifest_file
117        else:
118            log.error('The target profile did not set a manifest file.')
119            sys.exit(1)
120
121        for idx, f in enumerate(self.extra_download_files):
122            files_dict['extra_download_file' + str(idx)] = f
123
124        return files_dict
125
126class AutoTargetProfile(BaseTargetProfile):
127
128    def __init__(self, image_type):
129        super(AutoTargetProfile, self).__init__(image_type)
130        self.image_name = get_bb_var('IMAGE_LINK_NAME', target=image_type)
131        self.kernel_type = get_bb_var('KERNEL_IMAGETYPE', target=image_type)
132        self.controller = self.get_controller()
133
134        self.set_kernel_file()
135        self.set_rootfs_file()
136        self.set_manifest_file()
137        self.set_extra_download_files()
138
139    # Get the controller object that will be used by bitbake.
140    def get_controller(self):
141        from oeqa.controllers.testtargetloader import TestTargetLoader
142
143        target_controller = get_bb_var('TEST_TARGET')
144        bbpath = get_bb_var('BBPATH').split(':')
145
146        if target_controller == "qemu":
147            from oeqa.targetcontrol import QemuTarget
148            controller = QemuTarget
149        else:
150            testtargetloader = TestTargetLoader()
151            controller = testtargetloader.get_controller_module(target_controller, bbpath)
152        return controller
153
154    def set_kernel_file(self):
155        postconfig = "QA_GET_MACHINE = \"${MACHINE}\""
156        machine = get_bb_var('QA_GET_MACHINE', postconfig=postconfig)
157        self.kernel_file = self.kernel_type + '-' + machine + '.bin'
158
159    def set_rootfs_file(self):
160        image_fstypes = get_bb_var('IMAGE_FSTYPES').split(' ')
161        # Get a matching value between target's IMAGE_FSTYPES and the image fstypes suppoerted by the target controller.
162        fstype = self.controller.match_image_fstype(d=None, image_fstypes=image_fstypes)
163        if fstype:
164            self.rootfs_file = self.image_name + '.' + fstype
165        else:
166            log.error("Could not get a compatible image fstype. Check that IMAGE_FSTYPES and the target controller's supported_image_fstypes fileds have common values.")
167            sys.exit(1)
168
169    def set_manifest_file(self):
170        self.manifest_file = self.image_name + ".manifest"
171
172    def set_extra_download_files(self):
173        self.extra_download_files = self.get_controller_extra_files()
174        if not self.extra_download_files:
175            self.extra_download_files = []
176
177    def get_controller_extra_files(self):
178        controller = self.get_controller()
179        return controller.get_extra_files()
180
181
182class BaseRepoProfile(object, metaclass=ABCMeta):
183    """
184    This class defines the meta profile for an images repository.
185    """
186
187    def __init__(self, repolink, localdir):
188        self.localdir = localdir
189        self.repolink = repolink
190
191    # The following abstract methods are the interfaces to the repository profile classes derived from this abstract class.
192
193    # This method should check the file named 'file_name' if it is different than the upstream one.
194    # Should return False if the image is the same as the upstream and True if it differs.
195    @abstractmethod
196    def check_old_file(self, file_name):
197        pass
198
199    # This method should fetch file_name and create a symlink to localname if set.
200    @abstractmethod
201    def fetch(self, file_name, localname=None):
202        pass
203
204class PublicAB(BaseRepoProfile):
205
206    def __init__(self, repolink, localdir=None):
207        super(PublicAB, self).__init__(repolink, localdir)
208        if localdir is None:
209            self.localdir = os.path.join(os.environ['BUILDDIR'], 'PublicABMirror')
210
211    # Not yet implemented. Always returning True.
212    def check_old_file(self, file_name):
213        return True
214
215    def get_repo_path(self):
216        path = '/machines/'
217
218        postconfig = "QA_GET_MACHINE = \"${MACHINE}\""
219        machine = get_bb_var('QA_GET_MACHINE', postconfig=postconfig)
220        if 'qemu' in machine:
221            path += 'qemu/'
222
223        postconfig = "QA_GET_DISTRO = \"${DISTRO}\""
224        distro = get_bb_var('QA_GET_DISTRO', postconfig=postconfig)
225        path += distro.replace('poky', machine) + '/'
226        return path
227
228
229    def fetch(self, file_name, localname=None):
230        repo_path = self.get_repo_path()
231        link = self.repolink + repo_path + file_name
232
233        self.wget(link, self.localdir, localname)
234
235    def wget(self, link, localdir, localname=None, extraargs=None):
236        wget_cmd = '/usr/bin/env wget -t 2 -T 30 -nv --passive-ftp --no-check-certificate '
237
238        if localname:
239            wget_cmd += ' -O ' + localname + ' '
240
241        if extraargs:
242            wget_cmd += ' ' + extraargs + ' '
243
244        wget_cmd += " -P %s '%s'" % (localdir, link)
245        runCmd(wget_cmd)
246
247class HwAuto():
248
249    def __init__(self, image_types, repolink, required_packages, targetprofile, repoprofile, skip_download):
250        log.info('Initializing..')
251        self.image_types = image_types
252        self.repolink = repolink
253        self.required_packages = required_packages
254        self.targetprofile = targetprofile
255        self.repoprofile = repoprofile
256        self.skip_download = skip_download
257        self.repo = self.get_repo_profile(self.repolink)
258
259    # Get the repository profile; for now we only look inside this module.
260    def get_repo_profile(self, *args, **kwargs):
261        repo = getattr(sys.modules[__name__], self.repoprofile)(*args, **kwargs)
262        log.info("Using repo profile: %s" % repo.__class__.__name__)
263        return repo
264
265    # Get the target profile; for now we only look inside this module.
266    def get_target_profile(self, *args, **kwargs):
267        target = getattr(sys.modules[__name__], self.targetprofile)(*args, **kwargs)
268        log.info("Using target profile: %s" % target.__class__.__name__)
269        return target
270
271    # Run the testimage task on a build while redirecting DEPLOY_DIR_IMAGE to repo.localdir, where the images are downloaded.
272    def runTestimageBuild(self, image_type):
273        log.info("Running the runtime tests for %s.." % image_type)
274        postconfig = "DEPLOY_DIR_IMAGE = \"%s\"" % self.repo.localdir
275        result = bitbake("%s -c testimage" % image_type, ignore_status=True, postconfig=postconfig)
276        testimage_results = ftools.read_file(os.path.join(get_bb_var("T", image_type), "log.do_testimage"))
277        log.info('Runtime tests results for %s:' % image_type)
278        print(testimage_results)
279        return result
280
281    # Start the procedure!
282    def run(self):
283        if self.required_packages:
284            # Build the required packages for the tests
285            log.info("Building the required packages: %s ." % ', '.join(map(str, self.required_packages)))
286            result = bitbake(self.required_packages, ignore_status=True)
287            if result.status != 0:
288                log.error("Could not build required packages: %s. Output: %s" % (self.required_packages, result.output))
289                sys.exit(1)
290
291            # Build the package repository meta data.
292            log.info("Building the package index.")
293            result = bitbake("package-index", ignore_status=True)
294            if result.status != 0:
295                log.error("Could not build 'package-index'. Output: %s" % result.output)
296                sys.exit(1)
297
298        # Create the directory structure for the images to be downloaded
299        log.info("Creating directory structure %s" % self.repo.localdir)
300        if not os.path.exists(self.repo.localdir):
301            os.makedirs(self.repo.localdir)
302
303        # For each image type, download the needed files and run the tests.
304        noissuesfound = True
305        for image_type in self.image_types:
306            if self.skip_download:
307                log.info("Skipping downloading the images..")
308            else:
309                target = self.get_target_profile(image_type)
310                files_dict = target.get_files_dict()
311                log.info("Downloading files for %s" % image_type)
312                for f in files_dict:
313                    if self.repo.check_old_file(files_dict[f]):
314                        filepath = os.path.join(self.repo.localdir, files_dict[f])
315                        if os.path.exists(filepath):
316                            os.remove(filepath)
317                        self.repo.fetch(files_dict[f])
318
319            result = self.runTestimageBuild(image_type)
320            if result.status != 0:
321                noissuesfound = False
322
323        if noissuesfound:
324            log.info('Finished. No issues found.')
325        else:
326            log.error('Finished. Some runtime tests have failed. Returning non-0 status code.')
327            sys.exit(1)
328
329
330
331def main():
332
333    parser = get_args_parser()
334    args = parser.parse_args()
335
336    hwauto = HwAuto(image_types=args.image_types, repolink=args.repo_link, required_packages=args.required_packages, targetprofile=args.targetprofile, repoprofile=args.repoprofile, skip_download=args.skip_download)
337
338    hwauto.run()
339
340if __name__ == "__main__":
341    try:
342        ret = main()
343    except Exception:
344        ret = 1
345        import traceback
346        traceback.print_exc()
347    sys.exit(ret)
348