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