1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# DESCRIPTION
7# This implements the 'isoimage-isohybrid' source plugin class for 'wic'
8#
9# AUTHORS
10# Mihaly Varga <mihaly.varga (at] ni.com>
11
12import glob
13import logging
14import os
15import re
16import shutil
17
18from wic import WicError
19from wic.engine import get_custom_config
20from wic.pluginbase import SourcePlugin
21from wic.misc import exec_cmd, exec_native_cmd, get_bitbake_var
22
23logger = logging.getLogger('wic')
24
25class IsoImagePlugin(SourcePlugin):
26    """
27    Create a bootable ISO image
28
29    This plugin creates a hybrid, legacy and EFI bootable ISO image. The
30    generated image can be used on optical media as well as USB media.
31
32    Legacy boot uses syslinux and EFI boot uses grub or gummiboot (not
33    implemented yet) as bootloader. The plugin creates the directories required
34    by bootloaders and populates them by creating and configuring the
35    bootloader files.
36
37    Example kickstart file:
38    part /boot --source isoimage-isohybrid --sourceparams="loader=grub-efi, \\
39    image_name= IsoImage" --ondisk cd --label LIVECD
40    bootloader  --timeout=10  --append=" "
41
42    In --sourceparams "loader" specifies the bootloader used for booting in EFI
43    mode, while "image_name" specifies the name of the generated image. In the
44    example above, wic creates an ISO image named IsoImage-cd.direct (default
45    extension added by direct imeger plugin) and a file named IsoImage-cd.iso
46    """
47
48    name = 'isoimage-isohybrid'
49
50    @classmethod
51    def do_configure_syslinux(cls, creator, cr_workdir):
52        """
53        Create loader-specific (syslinux) config
54        """
55        splash = os.path.join(cr_workdir, "ISO/boot/splash.jpg")
56        if os.path.exists(splash):
57            splashline = "menu background splash.jpg"
58        else:
59            splashline = ""
60
61        bootloader = creator.ks.bootloader
62
63        syslinux_conf = ""
64        syslinux_conf += "PROMPT 0\n"
65        syslinux_conf += "TIMEOUT %s \n" % (bootloader.timeout or 10)
66        syslinux_conf += "\n"
67        syslinux_conf += "ALLOWOPTIONS 1\n"
68        syslinux_conf += "SERIAL 0 115200\n"
69        syslinux_conf += "\n"
70        if splashline:
71            syslinux_conf += "%s\n" % splashline
72        syslinux_conf += "DEFAULT boot\n"
73        syslinux_conf += "LABEL boot\n"
74
75        kernel = get_bitbake_var("KERNEL_IMAGETYPE")
76        if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
77            if get_bitbake_var("INITRAMFS_IMAGE"):
78                kernel = "%s-%s.bin" % \
79                    (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
80
81        syslinux_conf += "KERNEL /" + kernel + "\n"
82        syslinux_conf += "APPEND initrd=/initrd LABEL=boot %s\n" \
83                             % bootloader.append
84
85        logger.debug("Writing syslinux config %s/ISO/isolinux/isolinux.cfg",
86                     cr_workdir)
87
88        with open("%s/ISO/isolinux/isolinux.cfg" % cr_workdir, "w") as cfg:
89            cfg.write(syslinux_conf)
90
91    @classmethod
92    def do_configure_grubefi(cls, part, creator, target_dir):
93        """
94        Create loader-specific (grub-efi) config
95        """
96        configfile = creator.ks.bootloader.configfile
97        if configfile:
98            grubefi_conf = get_custom_config(configfile)
99            if grubefi_conf:
100                logger.debug("Using custom configuration file %s for grub.cfg",
101                             configfile)
102            else:
103                raise WicError("configfile is specified "
104                               "but failed to get it from %s", configfile)
105        else:
106            splash = os.path.join(target_dir, "splash.jpg")
107            if os.path.exists(splash):
108                splashline = "menu background splash.jpg"
109            else:
110                splashline = ""
111
112            bootloader = creator.ks.bootloader
113
114            grubefi_conf = ""
115            grubefi_conf += "serial --unit=0 --speed=115200 --word=8 "
116            grubefi_conf += "--parity=no --stop=1\n"
117            grubefi_conf += "default=boot\n"
118            grubefi_conf += "timeout=%s\n" % (bootloader.timeout or 10)
119            grubefi_conf += "\n"
120            grubefi_conf += "search --set=root --label %s " % part.label
121            grubefi_conf += "\n"
122            grubefi_conf += "menuentry 'boot'{\n"
123
124            kernel = get_bitbake_var("KERNEL_IMAGETYPE")
125            if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
126                if get_bitbake_var("INITRAMFS_IMAGE"):
127                    kernel = "%s-%s.bin" % \
128                        (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
129
130            grubefi_conf += "linux /%s rootwait %s\n" \
131                            % (kernel, bootloader.append)
132            grubefi_conf += "initrd /initrd \n"
133            grubefi_conf += "}\n"
134
135            if splashline:
136                grubefi_conf += "%s\n" % splashline
137
138        cfg_path = os.path.join(target_dir, "grub.cfg")
139        logger.debug("Writing grubefi config %s", cfg_path)
140
141        with open(cfg_path, "w") as cfg:
142            cfg.write(grubefi_conf)
143
144    @staticmethod
145    def _build_initramfs_path(rootfs_dir, cr_workdir):
146        """
147        Create path for initramfs image
148        """
149
150        initrd = get_bitbake_var("INITRD_LIVE") or get_bitbake_var("INITRD")
151        if not initrd:
152            initrd_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
153            if not initrd_dir:
154                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting.")
155
156            image_name = get_bitbake_var("IMAGE_BASENAME")
157            if not image_name:
158                raise WicError("Couldn't find IMAGE_BASENAME, exiting.")
159
160            image_type = get_bitbake_var("INITRAMFS_FSTYPES")
161            if not image_type:
162                raise WicError("Couldn't find INITRAMFS_FSTYPES, exiting.")
163
164            machine = os.path.basename(initrd_dir)
165
166            pattern = '%s/%s*%s.%s' % (initrd_dir, image_name, machine, image_type)
167            files = glob.glob(pattern)
168            if files:
169                initrd = files[0]
170
171        if not initrd or not os.path.exists(initrd):
172            # Create initrd from rootfs directory
173            initrd = "%s/initrd.cpio.gz" % cr_workdir
174            initrd_dir = "%s/INITRD" % cr_workdir
175            shutil.copytree("%s" % rootfs_dir, \
176                            "%s" % initrd_dir, symlinks=True)
177
178            if os.path.isfile("%s/init" % rootfs_dir):
179                shutil.copy2("%s/init" % rootfs_dir, "%s/init" % initrd_dir)
180            elif os.path.lexists("%s/init" % rootfs_dir):
181                os.symlink(os.readlink("%s/init" % rootfs_dir), \
182                            "%s/init" % initrd_dir)
183            elif os.path.isfile("%s/sbin/init" % rootfs_dir):
184                shutil.copy2("%s/sbin/init" % rootfs_dir, \
185                            "%s" % initrd_dir)
186            elif os.path.lexists("%s/sbin/init" % rootfs_dir):
187                os.symlink(os.readlink("%s/sbin/init" % rootfs_dir), \
188                            "%s/init" % initrd_dir)
189            else:
190                raise WicError("Couldn't find or build initrd, exiting.")
191
192            exec_cmd("cd %s && find . | cpio -o -H newc -R root:root >%s/initrd.cpio " \
193                     % (initrd_dir, cr_workdir), as_shell=True)
194            exec_cmd("gzip -f -9 %s/initrd.cpio" % cr_workdir, as_shell=True)
195            shutil.rmtree(initrd_dir)
196
197        return initrd
198
199    @classmethod
200    def do_configure_partition(cls, part, source_params, creator, cr_workdir,
201                               oe_builddir, bootimg_dir, kernel_dir,
202                               native_sysroot):
203        """
204        Called before do_prepare_partition(), creates loader-specific config
205        """
206        isodir = "%s/ISO/" % cr_workdir
207
208        if os.path.exists(isodir):
209            shutil.rmtree(isodir)
210
211        install_cmd = "install -d %s " % isodir
212        exec_cmd(install_cmd)
213
214        # Overwrite the name of the created image
215        logger.debug(source_params)
216        if 'image_name' in source_params and \
217                    source_params['image_name'].strip():
218            creator.name = source_params['image_name'].strip()
219            logger.debug("The name of the image is: %s", creator.name)
220
221    @staticmethod
222    def _install_payload(source_params, iso_dir):
223        """
224        Copies contents of payload directory (as specified in 'payload_dir' param) into iso_dir
225        """
226
227        if source_params.get('payload_dir'):
228            payload_dir = source_params['payload_dir']
229
230            logger.debug("Payload directory: %s", payload_dir)
231            shutil.copytree(payload_dir, iso_dir, symlinks=True, dirs_exist_ok=True)
232
233    @classmethod
234    def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
235                             oe_builddir, bootimg_dir, kernel_dir,
236                             rootfs_dir, native_sysroot):
237        """
238        Called to do the actual content population for a partition i.e. it
239        'prepares' the partition to be incorporated into the image.
240        In this case, prepare content for a bootable ISO image.
241        """
242
243        isodir = "%s/ISO" % cr_workdir
244
245        cls._install_payload(source_params, isodir)
246
247        if part.rootfs_dir is None:
248            if not 'ROOTFS_DIR' in rootfs_dir:
249                raise WicError("Couldn't find --rootfs-dir, exiting.")
250            rootfs_dir = rootfs_dir['ROOTFS_DIR']
251        else:
252            if part.rootfs_dir in rootfs_dir:
253                rootfs_dir = rootfs_dir[part.rootfs_dir]
254            elif part.rootfs_dir:
255                rootfs_dir = part.rootfs_dir
256            else:
257                raise WicError("Couldn't find --rootfs-dir=%s connection "
258                               "or it is not a valid path, exiting." %
259                               part.rootfs_dir)
260
261        if not os.path.isdir(rootfs_dir):
262            rootfs_dir = get_bitbake_var("IMAGE_ROOTFS")
263        if not os.path.isdir(rootfs_dir):
264            raise WicError("Couldn't find IMAGE_ROOTFS, exiting.")
265
266        part.rootfs_dir = rootfs_dir
267        deploy_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
268        img_iso_dir = get_bitbake_var("ISODIR")
269
270        # Remove the temporary file created by part.prepare_rootfs()
271        if os.path.isfile(part.source_file):
272            os.remove(part.source_file)
273
274        # Support using a different initrd other than default
275        if source_params.get('initrd'):
276            initrd = source_params['initrd']
277            if not deploy_dir:
278                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
279            cp_cmd = "cp %s/%s %s" % (deploy_dir, initrd, cr_workdir)
280            exec_cmd(cp_cmd)
281        else:
282            # Prepare initial ramdisk
283            initrd = "%s/initrd" % deploy_dir
284            if not os.path.isfile(initrd):
285                initrd = "%s/initrd" % img_iso_dir
286            if not os.path.isfile(initrd):
287                initrd = cls._build_initramfs_path(rootfs_dir, cr_workdir)
288
289        install_cmd = "install -m 0644 %s %s/initrd" % (initrd, isodir)
290        exec_cmd(install_cmd)
291
292        # Remove the temporary file created by _build_initramfs_path function
293        if os.path.isfile("%s/initrd.cpio.gz" % cr_workdir):
294            os.remove("%s/initrd.cpio.gz" % cr_workdir)
295
296        kernel = get_bitbake_var("KERNEL_IMAGETYPE")
297        if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
298            if get_bitbake_var("INITRAMFS_IMAGE"):
299                kernel = "%s-%s.bin" % \
300                    (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
301
302        install_cmd = "install -m 0644 %s/%s %s/%s" % \
303                      (kernel_dir, kernel, isodir, kernel)
304        exec_cmd(install_cmd)
305
306        #Create bootloader for efi boot
307        try:
308            target_dir = "%s/EFI/BOOT" % isodir
309            if os.path.exists(target_dir):
310                shutil.rmtree(target_dir)
311
312            os.makedirs(target_dir)
313
314            if source_params['loader'] == 'grub-efi':
315                # Builds bootx64.efi/bootia32.efi if ISODIR didn't exist or
316                # didn't contains it
317                target_arch = get_bitbake_var("TARGET_SYS")
318                if not target_arch:
319                    raise WicError("Coludn't find target architecture")
320
321                if re.match("x86_64", target_arch):
322                    grub_src_image = "grub-efi-bootx64.efi"
323                    grub_dest_image = "bootx64.efi"
324                elif re.match('i.86', target_arch):
325                    grub_src_image = "grub-efi-bootia32.efi"
326                    grub_dest_image = "bootia32.efi"
327                else:
328                    raise WicError("grub-efi is incompatible with target %s" %
329                                   target_arch)
330
331                grub_target = os.path.join(target_dir, grub_dest_image)
332                if not os.path.isfile(grub_target):
333                    grub_src = os.path.join(deploy_dir, grub_src_image)
334                    if not os.path.exists(grub_src):
335                        raise WicError("Grub loader %s is not found in %s. "
336                                       "Please build grub-efi first" % (grub_src_image, deploy_dir))
337                    shutil.copy(grub_src, grub_target)
338
339                if not os.path.isfile(os.path.join(target_dir, "boot.cfg")):
340                    cls.do_configure_grubefi(part, creator, target_dir)
341
342            else:
343                raise WicError("unrecognized bootimg-efi loader: %s" %
344                               source_params['loader'])
345        except KeyError:
346            raise WicError("bootimg-efi requires a loader, none specified")
347
348        # Create efi.img that contains bootloader files for EFI booting
349        # if ISODIR didn't exist or didn't contains it
350        if os.path.isfile("%s/efi.img" % img_iso_dir):
351            install_cmd = "install -m 0644 %s/efi.img %s/efi.img" % \
352                (img_iso_dir, isodir)
353            exec_cmd(install_cmd)
354        else:
355            # Default to 100 blocks of extra space for file system overhead
356            esp_extra_blocks = int(source_params.get('esp_extra_blocks', '100'))
357
358            du_cmd = "du -bks %s/EFI" % isodir
359            out = exec_cmd(du_cmd)
360            blocks = int(out.split()[0])
361            blocks += esp_extra_blocks
362            logger.debug("Added 100 extra blocks to %s to get to %d "
363                         "total blocks", part.mountpoint, blocks)
364
365            # dosfs image for EFI boot
366            bootimg = "%s/efi.img" % isodir
367
368            esp_label = source_params.get('esp_label', 'EFIimg')
369
370            dosfs_cmd = 'mkfs.vfat -n \'%s\' -S 512 -C %s %d' \
371                        % (esp_label, bootimg, blocks)
372            exec_native_cmd(dosfs_cmd, native_sysroot)
373
374            mmd_cmd = "mmd -i %s ::/EFI" % bootimg
375            exec_native_cmd(mmd_cmd, native_sysroot)
376
377            mcopy_cmd = "mcopy -i %s -s %s/EFI/* ::/EFI/" \
378                        % (bootimg, isodir)
379            exec_native_cmd(mcopy_cmd, native_sysroot)
380
381            chmod_cmd = "chmod 644 %s" % bootimg
382            exec_cmd(chmod_cmd)
383
384        # Prepare files for legacy boot
385        syslinux_dir = get_bitbake_var("STAGING_DATADIR")
386        if not syslinux_dir:
387            raise WicError("Couldn't find STAGING_DATADIR, exiting.")
388
389        if os.path.exists("%s/isolinux" % isodir):
390            shutil.rmtree("%s/isolinux" % isodir)
391
392        install_cmd = "install -d %s/isolinux" % isodir
393        exec_cmd(install_cmd)
394
395        cls.do_configure_syslinux(creator, cr_workdir)
396
397        install_cmd = "install -m 444 %s/syslinux/ldlinux.sys " % syslinux_dir
398        install_cmd += "%s/isolinux/ldlinux.sys" % isodir
399        exec_cmd(install_cmd)
400
401        install_cmd = "install -m 444 %s/syslinux/isohdpfx.bin " % syslinux_dir
402        install_cmd += "%s/isolinux/isohdpfx.bin" % isodir
403        exec_cmd(install_cmd)
404
405        install_cmd = "install -m 644 %s/syslinux/isolinux.bin " % syslinux_dir
406        install_cmd += "%s/isolinux/isolinux.bin" % isodir
407        exec_cmd(install_cmd)
408
409        install_cmd = "install -m 644 %s/syslinux/ldlinux.c32 " % syslinux_dir
410        install_cmd += "%s/isolinux/ldlinux.c32" % isodir
411        exec_cmd(install_cmd)
412
413        #create ISO image
414        iso_img = "%s/tempiso_img.iso" % cr_workdir
415        iso_bootimg = "isolinux/isolinux.bin"
416        iso_bootcat = "isolinux/boot.cat"
417        efi_img = "efi.img"
418
419        mkisofs_cmd = "mkisofs -V %s " % part.label
420        mkisofs_cmd += "-o %s -U " % iso_img
421        mkisofs_cmd += "-J -joliet-long -r -iso-level 2 -b %s " % iso_bootimg
422        mkisofs_cmd += "-c %s -no-emul-boot -boot-load-size 4 " % iso_bootcat
423        mkisofs_cmd += "-boot-info-table -eltorito-alt-boot "
424        mkisofs_cmd += "-eltorito-platform 0xEF -eltorito-boot %s " % efi_img
425        mkisofs_cmd += "-no-emul-boot %s " % isodir
426
427        logger.debug("running command: %s", mkisofs_cmd)
428        exec_native_cmd(mkisofs_cmd, native_sysroot)
429
430        shutil.rmtree(isodir)
431
432        du_cmd = "du -Lbks %s" % iso_img
433        out = exec_cmd(du_cmd)
434        isoimg_size = int(out.split()[0])
435
436        part.size = isoimg_size
437        part.source_file = iso_img
438
439    @classmethod
440    def do_install_disk(cls, disk, disk_name, creator, workdir, oe_builddir,
441                        bootimg_dir, kernel_dir, native_sysroot):
442        """
443        Called after all partitions have been prepared and assembled into a
444        disk image.  In this case, we insert/modify the MBR using isohybrid
445        utility for booting via BIOS from disk storage devices.
446        """
447
448        iso_img = "%s.p1" % disk.path
449        full_path = creator._full_path(workdir, disk_name, "direct")
450        full_path_iso = creator._full_path(workdir, disk_name, "iso")
451
452        isohybrid_cmd = "isohybrid -u %s" % iso_img
453        logger.debug("running command: %s", isohybrid_cmd)
454        exec_native_cmd(isohybrid_cmd, native_sysroot)
455
456        # Replace the image created by direct plugin with the one created by
457        # mkisofs command. This is necessary because the iso image created by
458        # mkisofs has a very specific MBR is system area of the ISO image, and
459        # direct plugin adds and configures an another MBR.
460        logger.debug("Replaceing the image created by direct plugin\n")
461        os.remove(disk.path)
462        shutil.copy2(iso_img, full_path_iso)
463        shutil.copy2(full_path_iso, full_path)
464