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