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