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