Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python -tt |
| 2 | # |
| 3 | # Copyright (c) 2009, 2010, 2011 Intel, Inc. |
| 4 | # Copyright (c) 2007, 2008 Red Hat, Inc. |
| 5 | # Copyright (c) 2008 Daniel P. Berrange |
| 6 | # Copyright (c) 2008 David P. Huff |
| 7 | # |
| 8 | # This program is free software; you can redistribute it and/or modify it |
| 9 | # under the terms of the GNU General Public License as published by the Free |
| 10 | # Software Foundation; version 2 of the License |
| 11 | # |
| 12 | # This program is distributed in the hope that it will be useful, but |
| 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| 14 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 15 | # for more details. |
| 16 | # |
| 17 | # You should have received a copy of the GNU General Public License along |
| 18 | # with this program; if not, write to the Free Software Foundation, Inc., 59 |
| 19 | # Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 20 | |
| 21 | import os |
| 22 | from wic import msger |
| 23 | from wic.utils.errors import ImageError |
| 24 | from wic.utils.oe.misc import exec_cmd, exec_native_cmd |
| 25 | |
| 26 | # Overhead of the MBR partitioning scheme (just one sector) |
| 27 | MBR_OVERHEAD = 1 |
| 28 | |
| 29 | # Overhead of the GPT partitioning scheme |
| 30 | GPT_OVERHEAD = 34 |
| 31 | |
| 32 | # Size of a sector in bytes |
| 33 | SECTOR_SIZE = 512 |
| 34 | |
| 35 | class Image(object): |
| 36 | """ |
| 37 | Generic base object for an image. |
| 38 | |
| 39 | An Image is a container for a set of DiskImages and associated |
| 40 | partitions. |
| 41 | """ |
| 42 | def __init__(self, native_sysroot=None): |
| 43 | self.disks = {} |
| 44 | self.partitions = [] |
| 45 | # Size of a sector used in calculations |
| 46 | self.sector_size = SECTOR_SIZE |
| 47 | self._partitions_layed_out = False |
| 48 | self.native_sysroot = native_sysroot |
| 49 | |
| 50 | def __add_disk(self, disk_name): |
| 51 | """ Add a disk 'disk_name' to the internal list of disks. Note, |
| 52 | 'disk_name' is the name of the disk in the target system |
| 53 | (e.g., sdb). """ |
| 54 | |
| 55 | if disk_name in self.disks: |
| 56 | # We already have this disk |
| 57 | return |
| 58 | |
| 59 | assert not self._partitions_layed_out |
| 60 | |
| 61 | self.disks[disk_name] = \ |
| 62 | {'disk': None, # Disk object |
| 63 | 'numpart': 0, # Number of allocate partitions |
| 64 | 'realpart': 0, # Number of partitions in the partition table |
| 65 | 'partitions': [], # Indexes to self.partitions |
| 66 | 'offset': 0, # Offset of next partition (in sectors) |
| 67 | # Minimum required disk size to fit all partitions (in bytes) |
| 68 | 'min_size': 0, |
| 69 | 'ptable_format': "msdos"} # Partition table format |
| 70 | |
| 71 | def add_disk(self, disk_name, disk_obj): |
| 72 | """ Add a disk object which have to be partitioned. More than one disk |
| 73 | can be added. In case of multiple disks, disk partitions have to be |
| 74 | added for each disk separately with 'add_partition()". """ |
| 75 | |
| 76 | self.__add_disk(disk_name) |
| 77 | self.disks[disk_name]['disk'] = disk_obj |
| 78 | |
| 79 | def __add_partition(self, part): |
| 80 | """ This is a helper function for 'add_partition()' which adds a |
| 81 | partition to the internal list of partitions. """ |
| 82 | |
| 83 | assert not self._partitions_layed_out |
| 84 | |
| 85 | self.partitions.append(part) |
| 86 | self.__add_disk(part['disk_name']) |
| 87 | |
| 88 | def add_partition(self, size, disk_name, mountpoint, source_file=None, fstype=None, |
| 89 | label=None, fsopts=None, boot=False, align=None, no_table=False, |
| 90 | part_type=None, uuid=None): |
| 91 | """ Add the next partition. Prtitions have to be added in the |
| 92 | first-to-last order. """ |
| 93 | |
| 94 | ks_pnum = len(self.partitions) |
| 95 | |
| 96 | # Converting kB to sectors for parted |
| 97 | size = size * 1024 / self.sector_size |
| 98 | |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame^] | 99 | part = {'ks_pnum': ks_pnum, # Partition number in the KS file |
| 100 | 'size': size, # In sectors |
| 101 | 'mountpoint': mountpoint, # Mount relative to chroot |
| 102 | 'source_file': source_file, # partition contents |
| 103 | 'fstype': fstype, # Filesystem type |
| 104 | 'fsopts': fsopts, # Filesystem mount options |
| 105 | 'label': label, # Partition label |
| 106 | 'disk_name': disk_name, # physical disk name holding partition |
| 107 | 'device': None, # kpartx device node for partition |
| 108 | 'num': None, # Partition number |
| 109 | 'boot': boot, # Bootable flag |
| 110 | 'align': align, # Partition alignment |
| 111 | 'no_table' : no_table, # Partition does not appear in partition table |
| 112 | 'part_type' : part_type, # Partition type |
| 113 | 'uuid': uuid} # Partition UUID |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 114 | |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame^] | 115 | self.__add_partition(part) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 116 | |
| 117 | def layout_partitions(self, ptable_format="msdos"): |
| 118 | """ Layout the partitions, meaning calculate the position of every |
| 119 | partition on the disk. The 'ptable_format' parameter defines the |
| 120 | partition table format and may be "msdos". """ |
| 121 | |
| 122 | msger.debug("Assigning %s partitions to disks" % ptable_format) |
| 123 | |
| 124 | if self._partitions_layed_out: |
| 125 | return |
| 126 | |
| 127 | self._partitions_layed_out = True |
| 128 | |
| 129 | # Go through partitions in the order they are added in .ks file |
| 130 | for num in range(len(self.partitions)): |
| 131 | part = self.partitions[num] |
| 132 | |
| 133 | if not self.disks.has_key(part['disk_name']): |
| 134 | raise ImageError("No disk %s for partition %s" \ |
| 135 | % (part['disk_name'], part['mountpoint'])) |
| 136 | |
| 137 | if ptable_format == 'msdos' and part['part_type']: |
| 138 | # The --part-type can also be implemented for MBR partitions, |
| 139 | # in which case it would map to the 1-byte "partition type" |
| 140 | # filed at offset 3 of the partition entry. |
| 141 | raise ImageError("setting custom partition type is not " \ |
| 142 | "implemented for msdos partitions") |
| 143 | |
| 144 | # Get the disk where the partition is located |
| 145 | disk = self.disks[part['disk_name']] |
| 146 | disk['numpart'] += 1 |
| 147 | if not part['no_table']: |
| 148 | disk['realpart'] += 1 |
| 149 | disk['ptable_format'] = ptable_format |
| 150 | |
| 151 | if disk['numpart'] == 1: |
| 152 | if ptable_format == "msdos": |
| 153 | overhead = MBR_OVERHEAD |
| 154 | elif ptable_format == "gpt": |
| 155 | overhead = GPT_OVERHEAD |
| 156 | |
| 157 | # Skip one sector required for the partitioning scheme overhead |
| 158 | disk['offset'] += overhead |
| 159 | |
| 160 | if disk['realpart'] > 3: |
| 161 | # Reserve a sector for EBR for every logical partition |
| 162 | # before alignment is performed. |
| 163 | if ptable_format == "msdos": |
| 164 | disk['offset'] += 1 |
| 165 | |
| 166 | |
| 167 | if part['align']: |
| 168 | # If not first partition and we do have alignment set we need |
| 169 | # to align the partition. |
| 170 | # FIXME: This leaves a empty spaces to the disk. To fill the |
| 171 | # gaps we could enlargea the previous partition? |
| 172 | |
| 173 | # Calc how much the alignment is off. |
| 174 | align_sectors = disk['offset'] % (part['align'] * 1024 / self.sector_size) |
| 175 | |
| 176 | if align_sectors: |
| 177 | # If partition is not aligned as required, we need |
| 178 | # to move forward to the next alignment point |
| 179 | align_sectors = (part['align'] * 1024 / self.sector_size) - align_sectors |
| 180 | |
| 181 | msger.debug("Realignment for %s%s with %s sectors, original" |
| 182 | " offset %s, target alignment is %sK." % |
| 183 | (part['disk_name'], disk['numpart'], align_sectors, |
| 184 | disk['offset'], part['align'])) |
| 185 | |
| 186 | # increase the offset so we actually start the partition on right alignment |
| 187 | disk['offset'] += align_sectors |
| 188 | |
| 189 | part['start'] = disk['offset'] |
| 190 | disk['offset'] += part['size'] |
| 191 | |
| 192 | part['type'] = 'primary' |
| 193 | if not part['no_table']: |
| 194 | part['num'] = disk['realpart'] |
| 195 | else: |
| 196 | part['num'] = 0 |
| 197 | |
| 198 | if disk['ptable_format'] == "msdos": |
| 199 | if disk['realpart'] > 3: |
| 200 | part['type'] = 'logical' |
| 201 | part['num'] = disk['realpart'] + 1 |
| 202 | |
| 203 | disk['partitions'].append(num) |
| 204 | msger.debug("Assigned %s to %s%d, sectors range %d-%d size %d " |
| 205 | "sectors (%d bytes)." \ |
| 206 | % (part['mountpoint'], part['disk_name'], part['num'], |
| 207 | part['start'], part['start'] + part['size'] - 1, |
| 208 | part['size'], part['size'] * self.sector_size)) |
| 209 | |
| 210 | # Once all the partitions have been layed out, we can calculate the |
| 211 | # minumim disk sizes. |
| 212 | for disk in self.disks.values(): |
| 213 | disk['min_size'] = disk['offset'] |
| 214 | if disk['ptable_format'] == "gpt": |
| 215 | disk['min_size'] += GPT_OVERHEAD |
| 216 | |
| 217 | disk['min_size'] *= self.sector_size |
| 218 | |
| 219 | def __create_partition(self, device, parttype, fstype, start, size): |
| 220 | """ Create a partition on an image described by the 'device' object. """ |
| 221 | |
| 222 | # Start is included to the size so we need to substract one from the end. |
| 223 | end = start + size - 1 |
| 224 | msger.debug("Added '%s' partition, sectors %d-%d, size %d sectors" % |
| 225 | (parttype, start, end, size)) |
| 226 | |
| 227 | cmd = "parted -s %s unit s mkpart %s" % (device, parttype) |
| 228 | if fstype: |
| 229 | cmd += " %s" % fstype |
| 230 | cmd += " %d %d" % (start, end) |
| 231 | |
| 232 | return exec_native_cmd(cmd, self.native_sysroot) |
| 233 | |
| 234 | def __format_disks(self): |
| 235 | self.layout_partitions() |
| 236 | |
| 237 | for dev in self.disks.keys(): |
| 238 | disk = self.disks[dev] |
| 239 | msger.debug("Initializing partition table for %s" % \ |
| 240 | (disk['disk'].device)) |
| 241 | exec_native_cmd("parted -s %s mklabel %s" % \ |
| 242 | (disk['disk'].device, disk['ptable_format']), |
| 243 | self.native_sysroot) |
| 244 | |
| 245 | msger.debug("Creating partitions") |
| 246 | |
| 247 | for part in self.partitions: |
| 248 | if part['num'] == 0: |
| 249 | continue |
| 250 | |
| 251 | disk = self.disks[part['disk_name']] |
| 252 | if disk['ptable_format'] == "msdos" and part['num'] == 5: |
| 253 | # Create an extended partition (note: extended |
| 254 | # partition is described in MBR and contains all |
| 255 | # logical partitions). The logical partitions save a |
| 256 | # sector for an EBR just before the start of a |
| 257 | # partition. The extended partition must start one |
| 258 | # sector before the start of the first logical |
| 259 | # partition. This way the first EBR is inside of the |
| 260 | # extended partition. Since the extended partitions |
| 261 | # starts a sector before the first logical partition, |
| 262 | # add a sector at the back, so that there is enough |
| 263 | # room for all logical partitions. |
| 264 | self.__create_partition(disk['disk'].device, "extended", |
| 265 | None, part['start'] - 1, |
| 266 | disk['offset'] - part['start'] + 1) |
| 267 | |
| 268 | if part['fstype'] == "swap": |
| 269 | parted_fs_type = "linux-swap" |
| 270 | elif part['fstype'] == "vfat": |
| 271 | parted_fs_type = "fat32" |
| 272 | elif part['fstype'] == "msdos": |
| 273 | parted_fs_type = "fat16" |
| 274 | elif part['fstype'] == "ontrackdm6aux3": |
| 275 | parted_fs_type = "ontrackdm6aux3" |
| 276 | else: |
| 277 | # Type for ext2/ext3/ext4/btrfs |
| 278 | parted_fs_type = "ext2" |
| 279 | |
| 280 | # Boot ROM of OMAP boards require vfat boot partition to have an |
| 281 | # even number of sectors. |
| 282 | if part['mountpoint'] == "/boot" and part['fstype'] in ["vfat", "msdos"] \ |
| 283 | and part['size'] % 2: |
| 284 | msger.debug("Substracting one sector from '%s' partition to " \ |
| 285 | "get even number of sectors for the partition" % \ |
| 286 | part['mountpoint']) |
| 287 | part['size'] -= 1 |
| 288 | |
| 289 | self.__create_partition(disk['disk'].device, part['type'], |
| 290 | parted_fs_type, part['start'], part['size']) |
| 291 | |
| 292 | if part['part_type']: |
| 293 | msger.debug("partition %d: set type UID to %s" % \ |
| 294 | (part['num'], part['part_type'])) |
| 295 | exec_native_cmd("sgdisk --typecode=%d:%s %s" % \ |
| 296 | (part['num'], part['part_type'], |
| 297 | disk['disk'].device), self.native_sysroot) |
| 298 | |
| 299 | if part['uuid']: |
| 300 | msger.debug("partition %d: set UUID to %s" % \ |
| 301 | (part['num'], part['uuid'])) |
| 302 | exec_native_cmd("sgdisk --partition-guid=%d:%s %s" % \ |
| 303 | (part['num'], part['uuid'], disk['disk'].device), |
| 304 | self.native_sysroot) |
| 305 | |
| 306 | if part['boot']: |
| 307 | flag_name = "legacy_boot" if disk['ptable_format'] == 'gpt' else "boot" |
| 308 | msger.debug("Set '%s' flag for partition '%s' on disk '%s'" % \ |
| 309 | (flag_name, part['num'], disk['disk'].device)) |
| 310 | exec_native_cmd("parted -s %s set %d %s on" % \ |
| 311 | (disk['disk'].device, part['num'], flag_name), |
| 312 | self.native_sysroot) |
| 313 | |
| 314 | # Parted defaults to enabling the lba flag for fat16 partitions, |
| 315 | # which causes compatibility issues with some firmware (and really |
| 316 | # isn't necessary). |
| 317 | if parted_fs_type == "fat16": |
| 318 | if disk['ptable_format'] == 'msdos': |
| 319 | msger.debug("Disable 'lba' flag for partition '%s' on disk '%s'" % \ |
| 320 | (part['num'], disk['disk'].device)) |
| 321 | exec_native_cmd("parted -s %s set %d lba off" % \ |
| 322 | (disk['disk'].device, part['num']), |
| 323 | self.native_sysroot) |
| 324 | |
| 325 | def cleanup(self): |
| 326 | if self.disks: |
| 327 | for dev in self.disks: |
| 328 | disk = self.disks[dev] |
| 329 | try: |
| 330 | disk['disk'].cleanup() |
| 331 | except: |
| 332 | pass |
| 333 | |
| 334 | def assemble(self, image_file): |
| 335 | msger.debug("Installing partitions") |
| 336 | |
| 337 | for part in self.partitions: |
| 338 | source = part['source_file'] |
| 339 | if source: |
| 340 | # install source_file contents into a partition |
| 341 | cmd = "dd if=%s of=%s bs=%d seek=%d count=%d conv=notrunc" % \ |
| 342 | (source, image_file, self.sector_size, |
| 343 | part['start'], part['size']) |
| 344 | exec_cmd(cmd) |
| 345 | |
| 346 | msger.debug("Installed %s in partition %d, sectors %d-%d, " |
| 347 | "size %d sectors" % \ |
| 348 | (source, part['num'], part['start'], |
| 349 | part['start'] + part['size'] - 1, part['size'])) |
| 350 | |
| 351 | os.rename(source, image_file + '.p%d' % part['num']) |
| 352 | |
| 353 | def create(self): |
| 354 | for dev in self.disks.keys(): |
| 355 | disk = self.disks[dev] |
| 356 | disk['disk'].create() |
| 357 | |
| 358 | self.__format_disks() |
| 359 | |
| 360 | return |