# To reproduce sgdisk behaviour in v1 of AIF-NG: # https://gist.github.com/herry13/5931cac426da99820de843477e41e89e # https://github.com/dcantrell/pyparted/blob/master/examples/query_device_capacity.py # TODO: Remember to replicate genfstab behaviour. import logging import os try: # https://stackoverflow.com/a/34812552/733214 # https://github.com/karelzak/util-linux/blob/master/libmount/python/test_mount_context.py#L6 import libmount as mount except ImportError: # We should never get here. util-linux is part of core (base) in Arch and uses "libmount". import pylibmount as mount ## import blkinfo import parted # https://www.gnu.org/software/parted/api/index.html from lxml import etree ## import aif.constants import aif.utils # TODO: https://serverfault.com/questions/356534/ssd-erase-block-size-lvm-pv-on-raw-device-alignment # TODO: caveats? https://gist.github.com/leodutra/8779d468e9062058a3e90008295d3ca6 # https://unix.stackexchange.com/questions/325886/bios-gpt-do-we-need-a-boot-flag _logger = logging.getLogger(__name__) class Disk(object): def __init__(self, disk_xml): self.xml = disk_xml _logger.debug('disk_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) self.id = self.xml.attrib['id'] self.devpath = os.path.realpath(self.xml.attrib['device']) self.is_lowformatted = None self.is_hiformatted = None self.is_partitioned = None self.partitions = None self._initDisk() def _initDisk(self): if self.devpath == 'auto': self.devpath = '/dev/{0}'.format(blkinfo.BlkDiskInfo().get_disks()[0]['kname']) if not os.path.isfile(self.devpath): _logger.error('Disk {0} does not exist; please specify an explicit device path'.format(self.devpath)) raise ValueError('Disk not found') self.table_type = self.xml.attrib.get('diskFormat', 'gpt').lower() if self.table_type in ('bios', 'mbr', 'dos'): self.table_type = 'msdos' validlabels = parted.getLabels() if self.table_type not in validlabels: _logger.error(('Disk format ({0}) is not valid for this architecture;' 'must be one of: {1}'.format(self.table_type, ', '.join(list(validlabels))))) raise ValueError('Invalid disk format') self.device = parted.getDevice(self.devpath) self.disk = parted.freshDisk(self.device, self.table_type) _logger.debug('Configured parted device for {0}.'.format(self.devpath)) self.is_lowformatted = False self.is_hiformatted = False self.is_partitioned = False self.partitions = [] return(None) def diskFormat(self): if self.is_lowformatted: return(None) # This is a safeguard. We do *not* want to low-format a disk that is mounted. aif.utils.checkMounted(self.devpath) self.disk.deleteAllPartitions() self.disk.commit() self.is_lowformatted = True self.is_partitioned = False return(None) def getPartitions(self): # For GPT, this *technically* should be 34 -- or, more precisely, 2048 (see FAQ in manual), but the alignment # optimizer fixes it for us automatically. # But for DOS tables, it's required. _logger.info('Establishing partitions for {0}'.format(self.devpath)) if self.table_type == 'msdos': start_sector = 2048 else: start_sector = 0 self.partitions = [] xml_partitions = self.xml.findall('part') for idx, part in enumerate(xml_partitions): partnum = idx + 1 if self.table_type == 'gpt': p = Partition(part, self.disk, start_sector, partnum, self.table_type) else: parttype = 'primary' if len(xml_partitions) > 4: if partnum == 4: parttype = 'extended' elif partnum > 4: parttype = 'logical' p = Partition(part, self.disk, start_sector, partnum, self.table_type, part_type = parttype) start_sector = p.end + 1 self.partitions.append(p) _logger.debug('Added partition {0}'.format(p.id)) return(None) def partFormat(self): if self.is_partitioned: return(None) if not self.is_lowformatted: self.diskFormat() # This is a safeguard. We do *not* want to partition a disk that is mounted. aif.utils.checkMounted(self.devpath) if not self.partitions: self.getPartitions() if not self.partitions: return(None) for p in self.partitions: self.disk.addPartition(partition = p, constraint = self.device.optimalAlignedConstraint) self.disk.commit() p.devpath = p.partition.path p.is_hiformatted = True self.is_partitioned = True return(None) class Partition(object): def __init__(self, part_xml, diskobj, start_sector, partnum, tbltype, part_type = None): if tbltype not in ('gpt', 'msdos'): _logger.error('Invalid tabletype specified: {0}. Must be one of: gpt,msdos.'.format(tbltype)) raise ValueError('Invalid tbltype.') if tbltype == 'msdos' and part_type not in ('primary', 'extended', 'logical'): _logger.error(('Table type msdos requires the part_type to be specified and must be one of: primary,' 'extended,logical (instead of: {0}).').format(part_type)) raise ValueError('The part_type must be specified for msdos tables') self.xml = part_xml _logger.debug('part_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) _logger.debug('Partition number: {0}'.format(partnum)) _logger.debug('Partition table type: {0}.'.format(tbltype)) self.id = self.xml.attrib['id'] self.flags = set() for f in self.xml.findall('partitionFlag'): if f.text in aif.constants.PARTED_FLAGS: self.flags.add(f.text) self.flags = sorted(list(self.flags)) self.partnum = partnum if tbltype == 'msdos': if partnum > 4: self.part_type = parted.PARTITION_LOGICAL else: if part_type == 'extended': self.part_type = parted.PARTITION_EXTENDED elif part_type == 'logical': self.part_type = parted.PARTITION_LOGICAL else: self.part_type = parted.PARTITION_NORMAL self.fs_type = self.xml.attrib['fsType'].lower() if self.fs_type not in aif.constants.PARTED_FSTYPES: _logger.error(('{0} is not a valid partition filesystem type; must be one of: ' '{1}').format(self.xml.attrib['fsType'], ', '.join(sorted(aif.constants.PARTED_FSTYPES)))) raise ValueError('Invalid partition filesystem type') self.disk = diskobj self.device = self.disk.device self.devpath = '{0}{1}'.format(self.device.path, self.partnum) _logger.debug('Assigned to disk: {0} ({1}) at path {2}'.format(self.disk.id, self.device, self.devpath)) self.is_hiformatted = False sizes = {} for s in ('start', 'stop'): x = dict(zip(('from_bgn', 'size', 'type'), aif.utils.convertSizeUnit(self.xml.attrib[s]))) sectors = x['size'] if x['type'] == '%': sectors = int(self.device.getLength() * (0.01 * x['size'])) else: sectors = int(aif.utils.size.convertStorage(x['size'], x['type'], target = 'B') / self.device.sectorSize) sizes[s] = (sectors, x['from_bgn']) if sizes['start'][1] is not None: if sizes['start'][1]: self.begin = sizes['start'][0] + 0 else: self.begin = self.device.getLength() - sizes['start'][0] else: self.begin = sizes['start'][0] + start_sector if sizes['stop'][1] is not None: if sizes['stop'][1]: self.end = sizes['stop'][0] + 0 else: # This *technically* should be - 34, at least for gpt, but the alignment optimizer fixes it for us. self.end = (self.device.getLength() - 1) - sizes['stop'][0] else: self.end = self.begin + sizes['stop'][0] _logger.debug('Size: sector {0} to {1}.'.format(self.begin, self.end)) # TECHNICALLY we could craft the Geometry object with "length = ...", but it doesn't let us be explicit # in configs. So we manually crunch the numbers and do it all at the end. self.geometry = parted.Geometry(device = self.device, start = self.begin, end = self.end) self.filesystem = parted.FileSystem(type = self.fs_type, geometry = self.geometry) self.partition = parted.Partition(disk = diskobj, type = self.part_type, geometry = self.geometry, fs = self.filesystem) for f in self.flags[:]: flag_id = aif.constants.PARTED_FLAG_IDX[f] if self.partition.isFlagAvailable(flag_id): self.partition.setFlag(flag_id) else: self.flags.remove(f) if tbltype == 'gpt' and self.xml.attrib.get('name'): # The name attribute setting is b0rk3n, so we operate on the underlying PedPartition object. # https://github.com/dcantrell/pyparted/issues/49#issuecomment-540096687 # https://github.com/dcantrell/pyparted/issues/65 # self.partition.name = self.xml.attrib.get('name') _pedpart = self.partition.getPedPartition() _pedpart.set_name(self.xml.attrib['name']) _logger.debug('Partition name: {0}'.format(self.xml.attrib['name'])) # # def detect(self): # pass # TODO; blkinfo?