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