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
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 root:root >%s/initrd.cpio " \
195                     % (initrd_dir, cr_workdir), as_shell=True)
196            exec_cmd("gzip -f -9 %s/initrd.cpio" % cr_workdir, as_shell=True)
197            shutil.rmtree(initrd_dir)
198
199        return initrd
200
201    @classmethod
202    def do_configure_partition(cls, part, source_params, creator, cr_workdir,
203                               oe_builddir, bootimg_dir, kernel_dir,
204                               native_sysroot):
205        """
206        Called before do_prepare_partition(), creates loader-specific config
207        """
208        isodir = "%s/ISO/" % cr_workdir
209
210        if os.path.exists(isodir):
211            shutil.rmtree(isodir)
212
213        install_cmd = "install -d %s " % isodir
214        exec_cmd(install_cmd)
215
216        # Overwrite the name of the created image
217        logger.debug(source_params)
218        if 'image_name' in source_params and \
219                    source_params['image_name'].strip():
220            creator.name = source_params['image_name'].strip()
221            logger.debug("The name of the image is: %s", creator.name)
222
223    @classmethod
224    def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
225                             oe_builddir, bootimg_dir, kernel_dir,
226                             rootfs_dir, native_sysroot):
227        """
228        Called to do the actual content population for a partition i.e. it
229        'prepares' the partition to be incorporated into the image.
230        In this case, prepare content for a bootable ISO image.
231        """
232
233        isodir = "%s/ISO" % cr_workdir
234
235        if part.rootfs_dir is None:
236            if not 'ROOTFS_DIR' in rootfs_dir:
237                raise WicError("Couldn't find --rootfs-dir, exiting.")
238            rootfs_dir = rootfs_dir['ROOTFS_DIR']
239        else:
240            if part.rootfs_dir in rootfs_dir:
241                rootfs_dir = rootfs_dir[part.rootfs_dir]
242            elif part.rootfs_dir:
243                rootfs_dir = part.rootfs_dir
244            else:
245                raise WicError("Couldn't find --rootfs-dir=%s connection "
246                               "or it is not a valid path, exiting." %
247                               part.rootfs_dir)
248
249        if not os.path.isdir(rootfs_dir):
250            rootfs_dir = get_bitbake_var("IMAGE_ROOTFS")
251        if not os.path.isdir(rootfs_dir):
252            raise WicError("Couldn't find IMAGE_ROOTFS, exiting.")
253
254        part.rootfs_dir = rootfs_dir
255        deploy_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
256        img_iso_dir = get_bitbake_var("ISODIR")
257
258        # Remove the temporary file created by part.prepare_rootfs()
259        if os.path.isfile(part.source_file):
260            os.remove(part.source_file)
261
262        # Support using a different initrd other than default
263        if source_params.get('initrd'):
264            initrd = source_params['initrd']
265            if not deploy_dir:
266                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
267            cp_cmd = "cp %s/%s %s" % (deploy_dir, initrd, cr_workdir)
268            exec_cmd(cp_cmd)
269        else:
270            # Prepare initial ramdisk
271            initrd = "%s/initrd" % deploy_dir
272            if not os.path.isfile(initrd):
273                initrd = "%s/initrd" % img_iso_dir
274            if not os.path.isfile(initrd):
275                initrd = cls._build_initramfs_path(rootfs_dir, cr_workdir)
276
277        install_cmd = "install -m 0644 %s %s/initrd" % (initrd, isodir)
278        exec_cmd(install_cmd)
279
280        # Remove the temporary file created by _build_initramfs_path function
281        if os.path.isfile("%s/initrd.cpio.gz" % cr_workdir):
282            os.remove("%s/initrd.cpio.gz" % cr_workdir)
283
284        # Install bzImage
285        install_cmd = "install -m 0644 %s/bzImage %s/bzImage" % \
286                      (kernel_dir, isodir)
287        exec_cmd(install_cmd)
288
289        #Create bootloader for efi boot
290        try:
291            target_dir = "%s/EFI/BOOT" % isodir
292            if os.path.exists(target_dir):
293                shutil.rmtree(target_dir)
294
295            os.makedirs(target_dir)
296
297            if source_params['loader'] == 'grub-efi':
298                # Builds bootx64.efi/bootia32.efi if ISODIR didn't exist or
299                # didn't contains it
300                target_arch = get_bitbake_var("TARGET_SYS")
301                if not target_arch:
302                    raise WicError("Coludn't find target architecture")
303
304                if re.match("x86_64", target_arch):
305                    grub_src_image = "grub-efi-bootx64.efi"
306                    grub_dest_image = "bootx64.efi"
307                elif re.match('i.86', target_arch):
308                    grub_src_image = "grub-efi-bootia32.efi"
309                    grub_dest_image = "bootia32.efi"
310                else:
311                    raise WicError("grub-efi is incompatible with target %s" %
312                                   target_arch)
313
314                grub_target = os.path.join(target_dir, grub_dest_image)
315                if not os.path.isfile(grub_target):
316                    grub_src = os.path.join(deploy_dir, grub_src_image)
317                    if not os.path.exists(grub_src):
318                        raise WicError("Grub loader %s is not found in %s. "
319                                       "Please build grub-efi first" % (grub_src_image, deploy_dir))
320                    shutil.copy(grub_src, grub_target)
321
322                if not os.path.isfile(os.path.join(target_dir, "boot.cfg")):
323                    cls.do_configure_grubefi(part, creator, target_dir)
324
325            else:
326                raise WicError("unrecognized bootimg-efi loader: %s" %
327                               source_params['loader'])
328        except KeyError:
329            raise WicError("bootimg-efi requires a loader, none specified")
330
331        # Create efi.img that contains bootloader files for EFI booting
332        # if ISODIR didn't exist or didn't contains it
333        if os.path.isfile("%s/efi.img" % img_iso_dir):
334            install_cmd = "install -m 0644 %s/efi.img %s/efi.img" % \
335                (img_iso_dir, isodir)
336            exec_cmd(install_cmd)
337        else:
338            du_cmd = "du -bks %s/EFI" % isodir
339            out = exec_cmd(du_cmd)
340            blocks = int(out.split()[0])
341            # Add some extra space for file system overhead
342            blocks += 100
343            logger.debug("Added 100 extra blocks to %s to get to %d "
344                         "total blocks", part.mountpoint, blocks)
345
346            # dosfs image for EFI boot
347            bootimg = "%s/efi.img" % isodir
348
349            dosfs_cmd = 'mkfs.vfat -n "EFIimg" -S 512 -C %s %d' \
350                        % (bootimg, blocks)
351            exec_native_cmd(dosfs_cmd, native_sysroot)
352
353            mmd_cmd = "mmd -i %s ::/EFI" % bootimg
354            exec_native_cmd(mmd_cmd, native_sysroot)
355
356            mcopy_cmd = "mcopy -i %s -s %s/EFI/* ::/EFI/" \
357                        % (bootimg, isodir)
358            exec_native_cmd(mcopy_cmd, native_sysroot)
359
360            chmod_cmd = "chmod 644 %s" % bootimg
361            exec_cmd(chmod_cmd)
362
363        # Prepare files for legacy boot
364        syslinux_dir = get_bitbake_var("STAGING_DATADIR")
365        if not syslinux_dir:
366            raise WicError("Couldn't find STAGING_DATADIR, exiting.")
367
368        if os.path.exists("%s/isolinux" % isodir):
369            shutil.rmtree("%s/isolinux" % isodir)
370
371        install_cmd = "install -d %s/isolinux" % isodir
372        exec_cmd(install_cmd)
373
374        cls.do_configure_syslinux(creator, cr_workdir)
375
376        install_cmd = "install -m 444 %s/syslinux/ldlinux.sys " % syslinux_dir
377        install_cmd += "%s/isolinux/ldlinux.sys" % isodir
378        exec_cmd(install_cmd)
379
380        install_cmd = "install -m 444 %s/syslinux/isohdpfx.bin " % syslinux_dir
381        install_cmd += "%s/isolinux/isohdpfx.bin" % isodir
382        exec_cmd(install_cmd)
383
384        install_cmd = "install -m 644 %s/syslinux/isolinux.bin " % syslinux_dir
385        install_cmd += "%s/isolinux/isolinux.bin" % isodir
386        exec_cmd(install_cmd)
387
388        install_cmd = "install -m 644 %s/syslinux/ldlinux.c32 " % syslinux_dir
389        install_cmd += "%s/isolinux/ldlinux.c32" % isodir
390        exec_cmd(install_cmd)
391
392        #create ISO image
393        iso_img = "%s/tempiso_img.iso" % cr_workdir
394        iso_bootimg = "isolinux/isolinux.bin"
395        iso_bootcat = "isolinux/boot.cat"
396        efi_img = "efi.img"
397
398        mkisofs_cmd = "mkisofs -V %s " % part.label
399        mkisofs_cmd += "-o %s -U " % iso_img
400        mkisofs_cmd += "-J -joliet-long -r -iso-level 2 -b %s " % iso_bootimg
401        mkisofs_cmd += "-c %s -no-emul-boot -boot-load-size 4 " % iso_bootcat
402        mkisofs_cmd += "-boot-info-table -eltorito-alt-boot "
403        mkisofs_cmd += "-eltorito-platform 0xEF -eltorito-boot %s " % efi_img
404        mkisofs_cmd += "-no-emul-boot %s " % isodir
405
406        logger.debug("running command: %s", mkisofs_cmd)
407        exec_native_cmd(mkisofs_cmd, native_sysroot)
408
409        shutil.rmtree(isodir)
410
411        du_cmd = "du -Lbks %s" % iso_img
412        out = exec_cmd(du_cmd)
413        isoimg_size = int(out.split()[0])
414
415        part.size = isoimg_size
416        part.source_file = iso_img
417
418    @classmethod
419    def do_install_disk(cls, disk, disk_name, creator, workdir, oe_builddir,
420                        bootimg_dir, kernel_dir, native_sysroot):
421        """
422        Called after all partitions have been prepared and assembled into a
423        disk image.  In this case, we insert/modify the MBR using isohybrid
424        utility for booting via BIOS from disk storage devices.
425        """
426
427        iso_img = "%s.p1" % disk.path
428        full_path = creator._full_path(workdir, disk_name, "direct")
429        full_path_iso = creator._full_path(workdir, disk_name, "iso")
430
431        isohybrid_cmd = "isohybrid -u %s" % iso_img
432        logger.debug("running command: %s", isohybrid_cmd)
433        exec_native_cmd(isohybrid_cmd, native_sysroot)
434
435        # Replace the image created by direct plugin with the one created by
436        # mkisofs command. This is necessary because the iso image created by
437        # mkisofs has a very specific MBR is system area of the ISO image, and
438        # direct plugin adds and configures an another MBR.
439        logger.debug("Replaceing the image created by direct plugin\n")
440        os.remove(disk.path)
441        shutil.copy2(iso_img, full_path_iso)
442        shutil.copy2(full_path_iso, full_path)
443