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