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