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