# Copyright 2016 Andreas Florath (andreas@florath.net) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import os import random from struct import pack logger = logging.getLogger(__name__) # Details of the MBR object itself can be found in the inline # documentation. # # General design and implementation remarks: # o Because the whole GNU parted and co. (e.g. the python-parted that # is based on GNU parted) cannot be used because of the license: # everything falls under GPL2 (not LGPL2!) and therefore does not # fit into the Apache License here. # o It looks that there is no real alternative available (2016-06). # o The interface of python-parted is not that simple to handle - and # the initial try to use GNU (python-)parted was not that much # easier and shorter than this approach. # o When using tools (like fdisk or parted) they try to optimize the # alignment of partitions based on the data found on the host # system. These might be misleading and might lead to (very) poor # performance. # o These ready-to-use tools typically also change the CHS layout # based on the disk size. In case that the disk is enlarged (which # is a normal use case for golden images), the CHS layout of the # disk changes for those tools (and is not longer correct). # In the DIB implementation the CHS are chosen that way, that also # for very small disks the maximum heads/cylinder and sectors/track # is used: even if the disk size in increased, the CHS numbers will # not change. # o In the easy and straight forward way when only using one # partition, exactly 40 bytes (!) must be written - and the biggest # part of this data is fixed (same in all cases). # # Limitations and Incompatibilities # o With the help of this class it is possible to create an # arbitrarily number of extended partitions (tested with over 1000). # o There are limitations and shortcomings in the OS and in tools # handling these partitions. # o Under Linux the loop device is able to handle a limited number of # partitions. The module parameter max_loop can be set - the maximum # number might vary depending on the distribution and kernel build. # o Under Linux fdisk is able to handle 'only' 60 partitions. Only # those are listed, can be changed or written. # o Under Linux GNU parted can handle about 60 partitions. # # Be sure only to pass in the number of partitions that the host OS # and target OS are able to handle. class MBR(object): """MBR Disk / Partition Table Layout Primary partitions are created first - and must also be passed in first. The extended partition layout is done in the way, that there is one entry in the MBR (the last) that uses the whole disk. EBR (extended boot records) are used to describe the partitions themselves. This has the advantage, that the same procedure can be used for all partitions and arbitrarily many partitions can be created in the same way (the EBR is placed as block 0 in each partition itself). In conjunction with a fixed and 'fits all' partition alignment the major design focus is maximum performance for the installed image (vs. minimal size). Because of the chosen default alignment of 1MiB there will be (1MiB - 512B) unused disk space for the MBR and also the same size unused in every partition. Assuming that 512 byte blocks are used, the resulting layout for extended partitions looks like (blocks offset in extended partition given): ======== ============================================== Offset Description ======== ============================================== 0 MBR - 2047 blocks unused 2048 EBR for partition 1 - 2047 blocks unused 4096 Start of data for partition 1 ... ... X EBR for partition N - 2047 blocks unused X+2048 Start of data for partition N ======== ============================================== Direct (native) writing of MBR, EBR (partition table) is implemented - no other partitioning library or tools is used - to be sure to get the correct CHS and alignment for a wide range of host systems. """ # Design & Implementation details: # o A 'block' is a storage unit on disk. It is similar (equal) to a # sector - but with LBA addressing. # o It is assumed that a disk block has that number of bytes bytes_per_sector = 512 # o CHS is the 'good and very old way' specifying blocks. # When passing around these numbers, they are also ordered like 'CHS': # (cylinder, head, sector). # o The computation from LBA to CHS is not unique (it is based # on the 'real' (or assumed) number of heads/cylinder and # sectors/track), these are the assumed numbers. Please note # that these are also the maximum numbers: heads_per_cylinder = 254 sectors_per_track = 63 max_cylinders = 1023 # o There is the need for some offsets that are defined in the # MBR/EBR domain. MBR_offset_disk_id = 440 MBR_offset_signature = 510 MBR_offset_first_partition_table_entry = 446 MBR_partition_type_extended_chs = 0x5 MBR_partition_type_extended_lba = 0xF MBR_signature = 0xAA55 def __init__(self, name, disk_size, alignment): """Initialize a disk partitioning MBR object. The name is the (existing) name of the disk. The disk_size is the (used) size of the disk. It must be a proper multiple of the disk bytes per sector (currently 512) """ logger.info("Create MBR disk partitioning object") assert disk_size % MBR.bytes_per_sector == 0 self.disk_size = disk_size self.disk_size_in_blocks \ = self.disk_size // MBR.bytes_per_sector self.alignment_blocks = alignment // MBR.bytes_per_sector # Because the extended partitions are a chain of blocks, when # creating a new partition, the reference in the already # existing EBR must be updated. This holds a reference to the # latest EBR. (A special case is the first: when it points to # 0 (MBR) there is no need to update the reference.) self.disk_block_last_ref = 0 self.name = name self.partition_abs_start = None self.partition_abs_next_free = None # Start of partition number self.partition_number = 0 self.primary_partitions_created = 0 self.extended_partitions_created = 0 def __enter__(self): # Open existing file for writing (r+) self.image_fd = open(self.name, "r+b") self.write_mbr() self.write_mbr_signature(0) self.partition_abs_start = self.align(1) self.partition_abs_next_free \ = self.partition_abs_start return self def __exit__(self, exc_type, exc_value, traceback): self.image_fd.flush() os.fsync(self.image_fd.fileno()) self.image_fd.close() def lba2chs(self, lba): """Converts a LBA block number to CHS If the LBA block number is bigger than the max (1023, 63, 254) the maximum is returned. """ if lba > MBR.heads_per_cylinder * MBR.sectors_per_track \ * MBR.max_cylinders: return MBR.max_cylinders, MBR.heads_per_cylinder, \ MBR.sectors_per_track cylinder = lba // (MBR.heads_per_cylinder * MBR.sectors_per_track) head = (lba // MBR.sectors_per_track) % MBR.heads_per_cylinder sector = (lba % MBR.sectors_per_track) + 1 logger.debug("Convert LBA to CHS [%d] -> [%d, %d, %d]", lba, cylinder, head, sector) return cylinder, head, sector def encode_chs(self, cylinders, heads, sectors): """Encodes a CHS triple into disk format""" # Head - nothing to convert assert heads <= MBR.heads_per_cylinder eh = heads # Sector assert sectors <= MBR.sectors_per_track es = sectors # top two bits are set in cylinder conversion # Cylinder assert cylinders <= MBR.max_cylinders ec = cylinders % 256 # lower part hc = cylinders // 4 # extract top two bits and es = es | hc # pass them into the top two bits of the sector logger.debug("Encode CHS to disk format [%d %d %d] " "-> [%02x %02x %02x]", cylinders, heads, sectors, eh, es, ec) return eh, es, ec def write_mbr(self): """Write MBR This method writes the MBR to disk. It creates a random disk id as well that it creates the extended partition (as first partition) which uses the whole disk. """ disk_id = random.randint(0, 0xFFFFFFFF) self.image_fd.seek(MBR.MBR_offset_disk_id) self.image_fd.write(pack(" 0: raise RuntimeError("All primary partitions must be " "given first") if primaryflag: return self.add_primary_partition(bootflag, size, ptype) if self.extended_partitions_created == 0: # When this is the first extended partition, the extended # partition entry has to be written. self.partition_abs_start = self.partition_abs_next_free self.write_partition_entry( False, 0, self.partition_number, MBR.MBR_partition_type_extended_lba, self.partition_abs_next_free, self.disk_size_in_blocks - self.partition_abs_next_free) self.partition_number = 4 return self.add_extended_partition(bootflag, size, ptype) def free(self): """Returns the free (not yet partitioned) size""" return self.disk_size \ - (self.partition_abs_next_free + self.align(1)) \ * MBR.bytes_per_sector