1# 2# Copyright (c) 2013, Intel Corporation. 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6# DESCRIPTION 7# This implements the 'direct' imager plugin class for 'wic' 8# 9# AUTHORS 10# Tom Zanussi <tom.zanussi (at] linux.intel.com> 11# 12 13import logging 14import os 15import random 16import shutil 17import tempfile 18import uuid 19 20from time import strftime 21 22from oe.path import copyhardlinktree 23 24from wic import WicError 25from wic.filemap import sparse_copy 26from wic.ksparser import KickStart, KickStartError 27from wic.pluginbase import PluginMgr, ImagerPlugin 28from wic.misc import get_bitbake_var, exec_cmd, exec_native_cmd 29 30logger = logging.getLogger('wic') 31 32class DirectPlugin(ImagerPlugin): 33 """ 34 Install a system into a file containing a partitioned disk image. 35 36 An image file is formatted with a partition table, each partition 37 created from a rootfs or other OpenEmbedded build artifact and dd'ed 38 into the virtual disk. The disk image can subsequently be dd'ed onto 39 media and used on actual hardware. 40 """ 41 name = 'direct' 42 43 def __init__(self, wks_file, rootfs_dir, bootimg_dir, kernel_dir, 44 native_sysroot, oe_builddir, options): 45 try: 46 self.ks = KickStart(wks_file) 47 except KickStartError as err: 48 raise WicError(str(err)) 49 50 # parse possible 'rootfs=name' items 51 self.rootfs_dir = dict(rdir.split('=') for rdir in rootfs_dir.split(' ')) 52 self.bootimg_dir = bootimg_dir 53 self.kernel_dir = kernel_dir 54 self.native_sysroot = native_sysroot 55 self.oe_builddir = oe_builddir 56 57 self.debug = options.debug 58 self.outdir = options.outdir 59 self.compressor = options.compressor 60 self.bmap = options.bmap 61 self.no_fstab_update = options.no_fstab_update 62 self.updated_fstab_path = None 63 64 self.name = "%s-%s" % (os.path.splitext(os.path.basename(wks_file))[0], 65 strftime("%Y%m%d%H%M")) 66 self.workdir = self.setup_workdir(options.workdir) 67 self._image = None 68 self.ptable_format = self.ks.bootloader.ptable 69 self.parts = self.ks.partitions 70 71 # as a convenience, set source to the boot partition source 72 # instead of forcing it to be set via bootloader --source 73 for part in self.parts: 74 if not self.ks.bootloader.source and part.mountpoint == "/boot": 75 self.ks.bootloader.source = part.source 76 break 77 78 image_path = self._full_path(self.workdir, self.parts[0].disk, "direct") 79 self._image = PartitionedImage(image_path, self.ptable_format, 80 self.parts, self.native_sysroot) 81 82 def setup_workdir(self, workdir): 83 if workdir: 84 if os.path.exists(workdir): 85 raise WicError("Internal workdir '%s' specified in wic arguments already exists!" % (workdir)) 86 87 os.makedirs(workdir) 88 return workdir 89 else: 90 return tempfile.mkdtemp(dir=self.outdir, prefix='tmp.wic.') 91 92 def do_create(self): 93 """ 94 Plugin entry point. 95 """ 96 try: 97 self.create() 98 self.assemble() 99 self.finalize() 100 self.print_info() 101 finally: 102 self.cleanup() 103 104 def update_fstab(self, image_rootfs): 105 """Assume partition order same as in wks""" 106 if not image_rootfs: 107 return 108 109 fstab_path = image_rootfs + "/etc/fstab" 110 if not os.path.isfile(fstab_path): 111 return 112 113 with open(fstab_path) as fstab: 114 fstab_lines = fstab.readlines() 115 116 updated = False 117 for part in self.parts: 118 if not part.realnum or not part.mountpoint \ 119 or part.mountpoint == "/": 120 continue 121 122 if part.use_uuid: 123 if part.fsuuid: 124 # FAT UUID is different from others 125 if len(part.fsuuid) == 10: 126 device_name = "UUID=%s-%s" % \ 127 (part.fsuuid[2:6], part.fsuuid[6:]) 128 else: 129 device_name = "UUID=%s" % part.fsuuid 130 else: 131 device_name = "PARTUUID=%s" % part.uuid 132 elif part.use_label: 133 device_name = "LABEL=%s" % part.label 134 else: 135 # mmc device partitions are named mmcblk0p1, mmcblk0p2.. 136 prefix = 'p' if part.disk.startswith('mmcblk') else '' 137 device_name = "/dev/%s%s%d" % (part.disk, prefix, part.realnum) 138 139 opts = part.fsopts if part.fsopts else "defaults" 140 line = "\t".join([device_name, part.mountpoint, part.fstype, 141 opts, "0", "0"]) + "\n" 142 143 fstab_lines.append(line) 144 updated = True 145 146 if updated: 147 self.updated_fstab_path = os.path.join(self.workdir, "fstab") 148 with open(self.updated_fstab_path, "w") as f: 149 f.writelines(fstab_lines) 150 151 def _full_path(self, path, name, extention): 152 """ Construct full file path to a file we generate. """ 153 return os.path.join(path, "%s-%s.%s" % (self.name, name, extention)) 154 155 # 156 # Actual implemention 157 # 158 def create(self): 159 """ 160 For 'wic', we already have our build artifacts - we just create 161 filesystems from the artifacts directly and combine them into 162 a partitioned image. 163 """ 164 if not self.no_fstab_update: 165 self.update_fstab(self.rootfs_dir.get("ROOTFS_DIR")) 166 167 for part in self.parts: 168 # get rootfs size from bitbake variable if it's not set in .ks file 169 if not part.size: 170 # and if rootfs name is specified for the partition 171 image_name = self.rootfs_dir.get(part.rootfs_dir) 172 if image_name and os.path.sep not in image_name: 173 # Bitbake variable ROOTFS_SIZE is calculated in 174 # Image._get_rootfs_size method from meta/lib/oe/image.py 175 # using IMAGE_ROOTFS_SIZE, IMAGE_ROOTFS_ALIGNMENT, 176 # IMAGE_OVERHEAD_FACTOR and IMAGE_ROOTFS_EXTRA_SPACE 177 rsize_bb = get_bitbake_var('ROOTFS_SIZE', image_name) 178 if rsize_bb: 179 part.size = int(round(float(rsize_bb))) 180 181 self._image.prepare(self) 182 self._image.layout_partitions() 183 self._image.create() 184 185 def assemble(self): 186 """ 187 Assemble partitions into disk image 188 """ 189 self._image.assemble() 190 191 def finalize(self): 192 """ 193 Finalize the disk image. 194 195 For example, prepare the image to be bootable by e.g. 196 creating and installing a bootloader configuration. 197 """ 198 source_plugin = self.ks.bootloader.source 199 disk_name = self.parts[0].disk 200 if source_plugin: 201 plugin = PluginMgr.get_plugins('source')[source_plugin] 202 plugin.do_install_disk(self._image, disk_name, self, self.workdir, 203 self.oe_builddir, self.bootimg_dir, 204 self.kernel_dir, self.native_sysroot) 205 206 full_path = self._image.path 207 # Generate .bmap 208 if self.bmap: 209 logger.debug("Generating bmap file for %s", disk_name) 210 python = os.path.join(self.native_sysroot, 'usr/bin/python3-native/python3') 211 bmaptool = os.path.join(self.native_sysroot, 'usr/bin/bmaptool') 212 exec_native_cmd("%s %s create %s -o %s.bmap" % \ 213 (python, bmaptool, full_path, full_path), self.native_sysroot) 214 # Compress the image 215 if self.compressor: 216 logger.debug("Compressing disk %s with %s", disk_name, self.compressor) 217 exec_cmd("%s %s" % (self.compressor, full_path)) 218 219 def print_info(self): 220 """ 221 Print the image(s) and artifacts used, for the user. 222 """ 223 msg = "The new image(s) can be found here:\n" 224 225 extension = "direct" + {"gzip": ".gz", 226 "bzip2": ".bz2", 227 "xz": ".xz", 228 None: ""}.get(self.compressor) 229 full_path = self._full_path(self.outdir, self.parts[0].disk, extension) 230 msg += ' %s\n\n' % full_path 231 232 msg += 'The following build artifacts were used to create the image(s):\n' 233 for part in self.parts: 234 if part.rootfs_dir is None: 235 continue 236 if part.mountpoint == '/': 237 suffix = ':' 238 else: 239 suffix = '["%s"]:' % (part.mountpoint or part.label) 240 rootdir = part.rootfs_dir 241 msg += ' ROOTFS_DIR%s%s\n' % (suffix.ljust(20), rootdir) 242 243 msg += ' BOOTIMG_DIR: %s\n' % self.bootimg_dir 244 msg += ' KERNEL_DIR: %s\n' % self.kernel_dir 245 msg += ' NATIVE_SYSROOT: %s\n' % self.native_sysroot 246 247 logger.info(msg) 248 249 @property 250 def rootdev(self): 251 """ 252 Get root device name to use as a 'root' parameter 253 in kernel command line. 254 255 Assume partition order same as in wks 256 """ 257 for part in self.parts: 258 if part.mountpoint == "/": 259 if part.uuid: 260 return "PARTUUID=%s" % part.uuid 261 else: 262 suffix = 'p' if part.disk.startswith('mmcblk') else '' 263 return "/dev/%s%s%-d" % (part.disk, suffix, part.realnum) 264 265 def cleanup(self): 266 if self._image: 267 self._image.cleanup() 268 269 # Move results to the output dir 270 if not os.path.exists(self.outdir): 271 os.makedirs(self.outdir) 272 273 for fname in os.listdir(self.workdir): 274 path = os.path.join(self.workdir, fname) 275 if os.path.isfile(path): 276 shutil.move(path, os.path.join(self.outdir, fname)) 277 278 # remove work directory when it is not in debugging mode 279 if not self.debug: 280 shutil.rmtree(self.workdir, ignore_errors=True) 281 282# Overhead of the MBR partitioning scheme (just one sector) 283MBR_OVERHEAD = 1 284 285# Overhead of the GPT partitioning scheme 286GPT_OVERHEAD = 34 287 288# Size of a sector in bytes 289SECTOR_SIZE = 512 290 291class PartitionedImage(): 292 """ 293 Partitioned image in a file. 294 """ 295 296 def __init__(self, path, ptable_format, partitions, native_sysroot=None): 297 self.path = path # Path to the image file 298 self.numpart = 0 # Number of allocated partitions 299 self.realpart = 0 # Number of partitions in the partition table 300 self.primary_part_num = 0 # Number of primary partitions (msdos) 301 self.extendedpart = 0 # Create extended partition before this logical partition (msdos) 302 self.extended_size_sec = 0 # Size of exteded partition (msdos) 303 self.logical_part_cnt = 0 # Number of total logical paritions (msdos) 304 self.offset = 0 # Offset of next partition (in sectors) 305 self.min_size = 0 # Minimum required disk size to fit 306 # all partitions (in bytes) 307 self.ptable_format = ptable_format # Partition table format 308 # Disk system identifier 309 self.identifier = random.SystemRandom().randint(1, 0xffffffff) 310 311 self.partitions = partitions 312 self.partimages = [] 313 # Size of a sector used in calculations 314 self.sector_size = SECTOR_SIZE 315 self.native_sysroot = native_sysroot 316 num_real_partitions = len([p for p in self.partitions if not p.no_table]) 317 318 # calculate the real partition number, accounting for partitions not 319 # in the partition table and logical partitions 320 realnum = 0 321 for part in self.partitions: 322 if part.no_table: 323 part.realnum = 0 324 else: 325 realnum += 1 326 if self.ptable_format == 'msdos' and realnum > 3 and num_real_partitions > 4: 327 part.realnum = realnum + 1 328 continue 329 part.realnum = realnum 330 331 # generate parition and filesystem UUIDs 332 for part in self.partitions: 333 if not part.uuid and part.use_uuid: 334 if self.ptable_format == 'gpt': 335 part.uuid = str(uuid.uuid4()) 336 else: # msdos partition table 337 part.uuid = '%08x-%02d' % (self.identifier, part.realnum) 338 if not part.fsuuid: 339 if part.fstype == 'vfat' or part.fstype == 'msdos': 340 part.fsuuid = '0x' + str(uuid.uuid4())[:8].upper() 341 else: 342 part.fsuuid = str(uuid.uuid4()) 343 else: 344 #make sure the fsuuid for vfat/msdos align with format 0xYYYYYYYY 345 if part.fstype == 'vfat' or part.fstype == 'msdos': 346 if part.fsuuid.upper().startswith("0X"): 347 part.fsuuid = '0x' + part.fsuuid.upper()[2:].rjust(8,"0") 348 else: 349 part.fsuuid = '0x' + part.fsuuid.upper().rjust(8,"0") 350 351 def prepare(self, imager): 352 """Prepare an image. Call prepare method of all image partitions.""" 353 for part in self.partitions: 354 # need to create the filesystems in order to get their 355 # sizes before we can add them and do the layout. 356 part.prepare(imager, imager.workdir, imager.oe_builddir, 357 imager.rootfs_dir, imager.bootimg_dir, 358 imager.kernel_dir, imager.native_sysroot, 359 imager.updated_fstab_path) 360 361 # Converting kB to sectors for parted 362 part.size_sec = part.disk_size * 1024 // self.sector_size 363 364 def layout_partitions(self): 365 """ Layout the partitions, meaning calculate the position of every 366 partition on the disk. The 'ptable_format' parameter defines the 367 partition table format and may be "msdos". """ 368 369 logger.debug("Assigning %s partitions to disks", self.ptable_format) 370 371 # The number of primary and logical partitions. Extended partition and 372 # partitions not listed in the table are not included. 373 num_real_partitions = len([p for p in self.partitions if not p.no_table]) 374 375 # Go through partitions in the order they are added in .ks file 376 for num in range(len(self.partitions)): 377 part = self.partitions[num] 378 379 if self.ptable_format == 'msdos' and part.part_name: 380 raise WicError("setting custom partition name is not " \ 381 "implemented for msdos partitions") 382 383 if self.ptable_format == 'msdos' and part.part_type: 384 # The --part-type can also be implemented for MBR partitions, 385 # in which case it would map to the 1-byte "partition type" 386 # filed at offset 3 of the partition entry. 387 raise WicError("setting custom partition type is not " \ 388 "implemented for msdos partitions") 389 390 # Get the disk where the partition is located 391 self.numpart += 1 392 if not part.no_table: 393 self.realpart += 1 394 395 if self.numpart == 1: 396 if self.ptable_format == "msdos": 397 overhead = MBR_OVERHEAD 398 elif self.ptable_format == "gpt": 399 overhead = GPT_OVERHEAD 400 401 # Skip one sector required for the partitioning scheme overhead 402 self.offset += overhead 403 404 if self.ptable_format == "msdos": 405 if self.primary_part_num > 3 or \ 406 (self.extendedpart == 0 and self.primary_part_num >= 3 and num_real_partitions > 4): 407 part.type = 'logical' 408 # Reserve a sector for EBR for every logical partition 409 # before alignment is performed. 410 if part.type == 'logical': 411 self.offset += 2 412 413 align_sectors = 0 414 if part.align: 415 # If not first partition and we do have alignment set we need 416 # to align the partition. 417 # FIXME: This leaves a empty spaces to the disk. To fill the 418 # gaps we could enlargea the previous partition? 419 420 # Calc how much the alignment is off. 421 align_sectors = self.offset % (part.align * 1024 // self.sector_size) 422 423 if align_sectors: 424 # If partition is not aligned as required, we need 425 # to move forward to the next alignment point 426 align_sectors = (part.align * 1024 // self.sector_size) - align_sectors 427 428 logger.debug("Realignment for %s%s with %s sectors, original" 429 " offset %s, target alignment is %sK.", 430 part.disk, self.numpart, align_sectors, 431 self.offset, part.align) 432 433 # increase the offset so we actually start the partition on right alignment 434 self.offset += align_sectors 435 436 if part.offset is not None: 437 offset = part.offset // self.sector_size 438 439 if offset * self.sector_size != part.offset: 440 raise WicError("Could not place %s%s at offset %d with sector size %d" % (part.disk, self.numpart, part.offset, self.sector_size)) 441 442 delta = offset - self.offset 443 if delta < 0: 444 raise WicError("Could not place %s%s at offset %d: next free sector is %d (delta: %d)" % (part.disk, self.numpart, part.offset, self.offset, delta)) 445 446 logger.debug("Skipping %d sectors to place %s%s at offset %dK", 447 delta, part.disk, self.numpart, part.offset) 448 449 self.offset = offset 450 451 part.start = self.offset 452 self.offset += part.size_sec 453 454 if not part.no_table: 455 part.num = self.realpart 456 else: 457 part.num = 0 458 459 if self.ptable_format == "msdos" and not part.no_table: 460 if part.type == 'logical': 461 self.logical_part_cnt += 1 462 part.num = self.logical_part_cnt + 4 463 if self.extendedpart == 0: 464 # Create extended partition as a primary partition 465 self.primary_part_num += 1 466 self.extendedpart = part.num 467 else: 468 self.extended_size_sec += align_sectors 469 self.extended_size_sec += part.size_sec + 2 470 else: 471 self.primary_part_num += 1 472 part.num = self.primary_part_num 473 474 logger.debug("Assigned %s to %s%d, sectors range %d-%d size %d " 475 "sectors (%d bytes).", part.mountpoint, part.disk, 476 part.num, part.start, self.offset - 1, part.size_sec, 477 part.size_sec * self.sector_size) 478 479 # Once all the partitions have been layed out, we can calculate the 480 # minumim disk size 481 self.min_size = self.offset 482 if self.ptable_format == "gpt": 483 self.min_size += GPT_OVERHEAD 484 485 self.min_size *= self.sector_size 486 487 def _create_partition(self, device, parttype, fstype, start, size): 488 """ Create a partition on an image described by the 'device' object. """ 489 490 # Start is included to the size so we need to substract one from the end. 491 end = start + size - 1 492 logger.debug("Added '%s' partition, sectors %d-%d, size %d sectors", 493 parttype, start, end, size) 494 495 cmd = "parted -s %s unit s mkpart %s" % (device, parttype) 496 if fstype: 497 cmd += " %s" % fstype 498 cmd += " %d %d" % (start, end) 499 500 return exec_native_cmd(cmd, self.native_sysroot) 501 502 def create(self): 503 logger.debug("Creating sparse file %s", self.path) 504 with open(self.path, 'w') as sparse: 505 os.ftruncate(sparse.fileno(), self.min_size) 506 507 logger.debug("Initializing partition table for %s", self.path) 508 exec_native_cmd("parted -s %s mklabel %s" % 509 (self.path, self.ptable_format), self.native_sysroot) 510 511 logger.debug("Set disk identifier %x", self.identifier) 512 with open(self.path, 'r+b') as img: 513 img.seek(0x1B8) 514 img.write(self.identifier.to_bytes(4, 'little')) 515 516 logger.debug("Creating partitions") 517 518 for part in self.partitions: 519 if part.num == 0: 520 continue 521 522 if self.ptable_format == "msdos" and part.num == self.extendedpart: 523 # Create an extended partition (note: extended 524 # partition is described in MBR and contains all 525 # logical partitions). The logical partitions save a 526 # sector for an EBR just before the start of a 527 # partition. The extended partition must start one 528 # sector before the start of the first logical 529 # partition. This way the first EBR is inside of the 530 # extended partition. Since the extended partitions 531 # starts a sector before the first logical partition, 532 # add a sector at the back, so that there is enough 533 # room for all logical partitions. 534 self._create_partition(self.path, "extended", 535 None, part.start - 2, 536 self.extended_size_sec) 537 538 if part.fstype == "swap": 539 parted_fs_type = "linux-swap" 540 elif part.fstype == "vfat": 541 parted_fs_type = "fat32" 542 elif part.fstype == "msdos": 543 parted_fs_type = "fat16" 544 if not part.system_id: 545 part.system_id = '0x6' # FAT16 546 else: 547 # Type for ext2/ext3/ext4/btrfs 548 parted_fs_type = "ext2" 549 550 # Boot ROM of OMAP boards require vfat boot partition to have an 551 # even number of sectors. 552 if part.mountpoint == "/boot" and part.fstype in ["vfat", "msdos"] \ 553 and part.size_sec % 2: 554 logger.debug("Subtracting one sector from '%s' partition to " 555 "get even number of sectors for the partition", 556 part.mountpoint) 557 part.size_sec -= 1 558 559 self._create_partition(self.path, part.type, 560 parted_fs_type, part.start, part.size_sec) 561 562 if part.part_name: 563 logger.debug("partition %d: set name to %s", 564 part.num, part.part_name) 565 exec_native_cmd("sgdisk --change-name=%d:%s %s" % \ 566 (part.num, part.part_name, 567 self.path), self.native_sysroot) 568 569 if part.part_type: 570 logger.debug("partition %d: set type UID to %s", 571 part.num, part.part_type) 572 exec_native_cmd("sgdisk --typecode=%d:%s %s" % \ 573 (part.num, part.part_type, 574 self.path), self.native_sysroot) 575 576 if part.uuid and self.ptable_format == "gpt": 577 logger.debug("partition %d: set UUID to %s", 578 part.num, part.uuid) 579 exec_native_cmd("sgdisk --partition-guid=%d:%s %s" % \ 580 (part.num, part.uuid, self.path), 581 self.native_sysroot) 582 583 if part.label and self.ptable_format == "gpt": 584 logger.debug("partition %d: set name to %s", 585 part.num, part.label) 586 exec_native_cmd("parted -s %s name %d %s" % \ 587 (self.path, part.num, part.label), 588 self.native_sysroot) 589 590 if part.active: 591 flag_name = "legacy_boot" if self.ptable_format == 'gpt' else "boot" 592 logger.debug("Set '%s' flag for partition '%s' on disk '%s'", 593 flag_name, part.num, self.path) 594 exec_native_cmd("parted -s %s set %d %s on" % \ 595 (self.path, part.num, flag_name), 596 self.native_sysroot) 597 if part.system_id: 598 exec_native_cmd("sfdisk --part-type %s %s %s" % \ 599 (self.path, part.num, part.system_id), 600 self.native_sysroot) 601 602 def cleanup(self): 603 pass 604 605 def assemble(self): 606 logger.debug("Installing partitions") 607 608 for part in self.partitions: 609 source = part.source_file 610 if source: 611 # install source_file contents into a partition 612 sparse_copy(source, self.path, seek=part.start * self.sector_size) 613 614 logger.debug("Installed %s in partition %d, sectors %d-%d, " 615 "size %d sectors", source, part.num, part.start, 616 part.start + part.size_sec - 1, part.size_sec) 617 618 partimage = self.path + '.p%d' % part.num 619 bb.utils.rename(source, partimage) 620 self.partimages.append(partimage) 621